パラメータ化されたクエリとdjango ormのパフォーマンス

django ormを使用するとパフォーマンス大幅に低下するので、ormのさまざまな使用方法を検討して、解決策を探し始めました。 私がやったこと-ロールを参照してください。





django ormを使用して通常のコードはどのように書かれていますか?



原則として、このピースは特定の機能(ビューなど)に入り、パラメーターを受け取り、これらのパラメーターに基づいて結果を形成します。



例として、次の基本的な状況を考えてみましょう。現在のユーザーが属しているグループの名前のリストを取得したいです。 そもそも頭に浮かぶ最も簡単で明白な方法は、関係を介してグループのリストを取得し、名前を見つけることです。



def myview(request): u = request.user a = [g.name for g in u.groups.all()] ...
      
      







ユーザーオブジェクトrequest.userはリクエストの予備処理の段階ですでに受信されていることに留意して、この作品のパフォーマンスがどうなるかを確認します。



テストグループを作成し、最初のユーザーをそれに参加させます。

 >>> u = User.objects.all()[0] >>> g = Group(name='thetest') >>> g.save() >>> u.groups.add(g) >>> u.groups.all() [<Group: thetest>]
      
      







今後のすべてのテストでこのケースを使用します。 すべてがシェルを介して行われるため、この段階で取得した変数uも使用します。



したがって、テスト番号1では、想定されたコードを実行します。 検索リストが本当に返されるかどうかを確認します。



 >>> a = [g.name for g in u.groups.all()] >>> a [u'thetest']
      
      







パフォーマンスを測定するには、1000回実行します。



 >>> def test1(): ... import datetime ... t1 = datetime.datetime.now() ... for i in xrange(1000): ... a = [g.name for g in u.groups.all()] ... t2 = datetime.datetime.now() ... print "%s" % (t2 - t1) ... >>> test1() 0:00:01.437324
      
      





私たちのサイクルの1000回転には約1.5秒かかり、リクエストごとに1.5ミリ秒かかりました。



経験豊富なジャングル作家は、おそらくこの作品が最適とはほど遠いという事実に鼻を突くためにすでに集まっているでしょう。 実際、グループオブジェクトを構築し、データベースから本当に必要なデータのみを取得することなく、同じアクションを実行するより最適なコードを一目で作成できます。



 >>> a = [g['name'] for g in u.groups.values('name')] >>> a [u'thetest']
      
      







さて、この作品を測定します。



 >>> def test2(): ... import datetime ... t1 = datetime.datetime.now() ... for i in xrange(1000): ... a = [g['name'] for g in u.groups.values('name')] ... t2 = datetime.datetime.now() ... print "%s" % (t2 - t1) ... >>> test2() 0:00:01.752529
      
      





不自然に思えますが、コードの2番目のバージョンは最初のバージョンよりも最適ではありませんか?



実際には、これがそうです。 値の呼び出し()と要求の追加分析での損失は、Groupオブジェクトの構築とそのすべてのフィールドの値の取得で節約できる可能性よりも高いことが判明しました。



しかし、すみません? 実際、リクエストを再設計および分析するたびに、ビューで常に同じリクエストを実行しこのリクエストが実行されるユーザーオブジェクトのみが異なる場合、なぜでしょうか?



残念なことに、djangoでは最初にリクエストを事前準備することはできません 。必要に応じて準備済みのリクエストを参照します。 対応する呼び出しはなく、クエリ生成構文はクエリパラメータとして特定の値のみを使用することを意味します。



ソースに少し登る必要があります。 この機会に、django_extensionsの開発者と彼らのすばらしいshell_plusチームに感謝したいと思います。



QuerySetオブジェクト(これは、例えばobjects.all()へのアクセス時に取得されるオブジェクトです)には、django.db.models.sql.query.Queryクラスのオブジェクトであるqueryプロパティがあります。 次にsql_with_params()メソッドがあります



このメソッドは、cursor.execute()に渡す準備が整った一連のパラメーター、つまりSQL式文字列と追加パラメーターを返します。 すばらしいのは、これらの非常に高度なパラメーターが、QuerySetの形成時にQuerySetに渡されるパラメーターであることです。



 >>> u.groups.all().values('name').query.sql_with_params() ('SELECT `auth_group`.`name` FROM `auth_group` INNER JOIN `auth_user_groups` ON (`auth_group`.`id` = `auth_user_groups`.`group_id`) WHERE `auth_user_groups`.`user_id` = %s ', (1,))
      
      







これで、準備済みのSQLクエリを取得し、それにさまざまなパラメーター値を代入すると、クエリの準備にリソースを無駄にせずにクエリを実行できます。



これを行うには、コミットするハックのすべての詳細を隠す特別なクラスを作成します。



 from django.db import connection from django.db.models.query import QuerySet,ValuesQuerySet import django from threading import local class PQuery(local): def __init__(self,query,connection=connection,**placeholders): self.query = query self.connection = connection self.placeholders = placeholders self.replaces = {} sql = None try: sql = self.query.query.sql_with_params() # 1.4 except AttributeError: sql = self.query.query.get_compiler(connection=self.connection).as_sql() # 1.3, lower? self.places = list(sql[1]) self.sql = sql[0] self.is_values = isinstance(query,ValuesQuerySet) self.cursor = None for i in xrange(len(self.places)): x = self.places[i] found = False for p in self.placeholders: v = self.placeholders[p] if x == v: found = True if not p in self.replaces: self.replaces[p] = [] self.replaces[p].append(i) if not found: raise AttributeError("The placeholder %(ph)s not found, please add some_name=%(ph)s to the list of constructor parameters" % { 'ph':repr(x) }) def execute(self,**kw): try: for k in kw: for i in self.replaces[k]: self.places[i] = kw[k] except KeyError,ex: raise TypeError("No such placeholder: %s" % k) if not self.cursor: self.cursor = self.connection.cursor() self.cursor.execute(self.sql,self.places) if not hasattr(self,'fldnms'): self.fldnms = [col[0] for col in self.cursor.description] if self.is_values: return [dict(zip(self.fldnms,row)) for row in self.cursor.fetchall()] return [self.query.model(**dict(zip(self.fldnms,row))) for row in self.cursor.fetchall()] def __call__(self,**kw): return self.execute(**kw) ParametrizedQuery = PQuery # compatibility issue
      
      





UPD:2012-08-06 19:20:00 MSK-マルチトレーディングとの互換性に関するコードを修正し、複雑なクエリを実行する際の小さなバグを修正し、使いやすさを改善しました。

以前のコードバージョン
 from django.db import connection from django.db.models.query import QuerySet,ValuesQuerySet class ParametrizedQuery: def __init__(self,query,connection=connection,**placeholders): self.query = query self.connection = connection self.placeholders = placeholders self.replaces = {} sql = self.query.query.sql_with_params() self.places = list(sql[1]) self.sql = sql[0] self.is_values = isinstance(query,ValuesQuerySet) self.cursor = None for p in self.placeholders: v = self.placeholders[p] self.replaces[p] = self.places.index(v) def execute(self,**kw): for k in kw: self.places[self.replaces[k]] = kw[k] if not self.cursor: self.cursor = self.connection.cursor() self.cursor.execute(self.sql,self.places) if not hasattr(self,'fldnms'): self.fldnms = [col[0] for col in self.cursor.description] if self.is_values: return [dict(zip(self.fldnms,row)) for row in self.cursor.fetchall()] return [self.query.model(**dict(zip(self.fldnms,row))) for row in self.cursor.fetchall()]
      
      







このクラスは何をしますか? 彼は要求を受け取り、準備されたSQLとパラメーターを選択します。 このようなリクエストを作成して、代用する各パラメーターが事前にわかっている特別な値を持つようにすることができます。 これらの値を使用して、実行中に渡された値を置き換える場所を検索します。



いくつかの追加の実装の詳細も、リソースの節約に役立ちます。





元のリクエストを少し変更して、そこで置換をスリップできるようにします。



 >>> q = Group.objects.filter(user__id=12345).values('name') >>> q.query.sql_with_params() ('SELECT `auth_group`.`name` FROM `auth_group` INNER JOIN `auth_user_groups` ON (`auth_group`.`id` = `auth_user_groups`.`group_id`) WHERE `auth_user_groups`.`user_id` = %s ', (12345,))
      
      







置換として値12345を使用します。



 >>> p = ParametrizedQuery(q,user_id=12345) >>> [g['name'] for g in p.execute(user_id=u.id)] [u'thetest']
      
      







p.execute()リクエストが実行されると、ユーザーIDの実際の値が12345の置換場所に置換されました。



次に、コードのパフォーマンスがどのように変化するかを見てみましょう。



 >>> def test3(): ... import datetime ... t1 = datetime.datetime.now() ... for i in xrange(1000): ... a = [g['name'] for g in p.execute(user_id=u.id)] ... t2 = datetime.datetime.now() ... print "%s" % (t2 - t1) ... >>> test3() 0:00:00.217270
      
      





これが結果です! クエリの実行時間は7倍に短縮されました



実際のコードで使用する方法は?



まず、準備されたリクエストを保存できる場所が必要です。 次に、ある時点で、この変数を入力する必要があります。 たとえば、機能コードの最初の実行時。 そして第三に、もちろん、クエリを直接実行する代わりに、パラメータ化されたクエリの呼び出しを使用します。



 def myview(request): if not hasattr(myview,'query'): myview.query = ParametrizedQuery(Group.objects.filter(user__id=12345).values('name'),user_id=12345) a = [g['name'] for g in myview.query.execute(user_id=request.user.id)] ...
      
      







すべてのコードが実行された場所:

-django.VERSION =(1、4、0、 'final'、0)

-mysql DBMS(django.db.backends.mysql)

-テーブルエンジン= MYISAM

-ローカルホスト経由の接続

-Python 2.7.2+(デフォルト、2011年10月4日、20:03:08)[GCC 4.6.1] on linux2

-Linuxホストseva 3.0.0-22-generic#36-Ubuntu SMP Tue Jun 12 17:13:04 UTC 2012 i686 athlon i386 GNU / Linux



専門家からのコメントを歓迎します。



All Articles