データベースアクセスの最適化—Djangoドキュメント

提供:Dev Guides
< DjangoDjango/docs/3.2.x/topics/db/optimization
移動先:案内検索

データベースアクセスの最適化

Djangoのデータベースレイヤーは、開発者がデータベースを最大限に活用するのに役立つさまざまな方法を提供します。 このドキュメントでは、関連するドキュメントへのリンクをまとめ、データベースの使用を最適化する際に実行する手順の概要を示すいくつかの見出しの下に整理されたさまざまなヒントを追加します。

最初にプロファイル

一般的なプログラミング手法として、これは言うまでもありません。 実行しているクエリとそのコストを確認してください。 QuerySet.explain()を使用して、特定のQuerySetがデータベースによってどのように実行されるかを理解します。 django-debug-toolbar のような外部プロジェクト、またはデータベースを直接監視するツールを使用することもできます。

要件に応じて、速度またはメモリ、あるいはその両方を最適化する場合があることに注意してください。 一方を最適化すると、もう一方に悪影響を与える場合もありますが、互いに助け合う場合もあります。 また、データベースプロセスによって実行される作業は、Pythonプロセスで実行される同じ量の作業と同じコスト(ユーザーにとって)ではない場合があります。 アプリケーションとサーバーによって異なるため、優先順位を決定し、バランスをとる必要がある場所を決定し、必要に応じてこれらすべてのプロファイルを作成するのはあなた次第です。

以下のすべてについて、変更がメリットであり、コードの可読性が低下することを考えると十分に大きなメリットであることを確認するために、変更のたびにプロファイルを作成することを忘れないでください。 以下の提案のすべてには、状況によっては一般原則が適用されない場合や、逆になる場合があるという警告があります。


標準のDB最適化手法を使用する

…含む:

  • インデックス。 これは最優先事項であり、に、追加するインデックスのプロファイリングから決定しました。 Meta.indexes または Field.db_index を使用して、Djangoからこれらを追加します。 filter()exclude()order_by()などを使用して頻繁にクエリを実行するフィールドにインデックスを追加することを検討してください。 インデックスはルックアップの高速化に役立つ可能性があるためです。 最適なインデックスを決定することは、特定のアプリケーションに依存する複雑なデータベース依存のトピックであることに注意してください。 インデックスを維持するためのオーバーヘッドは、クエリ速度の向上を上回る可能性があります。
  • フィールドタイプの適切な使用。

上記のことを行ったと想定します。 このドキュメントの残りの部分では、不要な作業を行わないようにDjangoを使用する方法に焦点を当てています。 このドキュメントでは、汎用キャッシングなど、すべてのコストのかかる操作に適用される他の最適化手法についても説明していません。


QuerySetを理解する

QuerySets を理解することは、単純なコードで優れたパフォーマンスを得るのに不可欠です。 特に:

QuerySetの評価を理解する

パフォーマンスの問題を回避するには、次のことを理解することが重要です。


キャッシュされた属性を理解する

QuerySet全体のキャッシュに加えて、ORMオブジェクトの属性の結果のキャッシュがあります。 通常、呼び出し可能ではない属性はキャッシュされます。 たとえば、サンプルのウェブログモデルを想定すると、次のようになります。

>>> entry = Entry.objects.get(id=1)
>>> entry.blog   # Blog object is retrieved at this point
>>> entry.blog   # cached version, no DB access

ただし、一般に、呼び出し可能な属性は毎回DBルックアップを引き起こします。

>>> entry = Entry.objects.get(id=1)
>>> entry.authors.all()   # query performed
>>> entry.authors.all()   # query performed again

テンプレートコードを読むときは注意してください-テンプレートシステムは括弧の使用を許可しませんが、呼び出し可能オブジェクトを自動的に呼び出し、上記の区別を隠します。

独自のカスタムプロパティに注意してください。たとえば、 cached_property デコレータを使用して、必要に応じてキャッシュを実装するのはあなた次第です。


withテンプレートタグを使用します

QuerySetのキャッシュ動作を利用するには、:ttag: `with` テンプレートタグを使用する必要がある場合があります。


iterator()を使用する

オブジェクトが多数ある場合、QuerySetのキャッシュ動作により、大量のメモリが使用される可能性があります。 この場合、 iterator()が役立つ場合があります。


explain()を使用する

QuerySet.explain()は、使用されるインデックスや結合など、データベースがクエリを実行する方法に関する詳細情報を提供します。 これらの詳細は、より効率的に書き換えられる可能性のあるクエリを見つけたり、パフォーマンスを向上させるために追加できるインデックスを特定したりするのに役立ちます。


Pythonではなくデータベースでデータベースを機能させる

例えば:

  • 最も基本的なレベルでは、 filterとexclude を使用して、データベースでフィルタリングを実行します。
  • F式を使用して、同じモデル内の他のフィールドに基づいてフィルタリングします。
  • annotateを使用して、データベースで集計を行います。

これらが必要なSQLを生成するのに十分でない場合:

RawSQLを使用する

移植性は劣りますが、より強力な方法は RawSQL 式です。これにより、一部のSQLをクエリに明示的に追加できます。 それでも十分に強力でない場合:


生のSQLを使用する

独自のカスタムSQLを作成して、データを取得したり、モデルにデータを入力したりしますdjango.db.connection.queriesを使用して、Djangoがあなたのために書いているものを見つけ、そこから始めます。


一意のインデックス付き列を使用して個々のオブジェクトを取得します

get()を使用して個々のオブジェクトを取得するときに、 unique または db_index の列を使用する理由は2つあります。 まず、基になるデータベースインデックスがあるため、クエリが高速になります。 また、複数のオブジェクトがルックアップに一致する場合、クエリの実行速度が大幅に低下する可能性があります。 列に一意の制約があると、これが発生しないことが保証されます。

したがって、サンプルのウェブログモデルを使用すると、次のようになります。

>>> entry = Entry.objects.get(id=10)

より速くなります:

>>> entry = Entry.objects.get(headline="News Item Title")

idはデータベースによってインデックスが付けられ、一意であることが保証されているためです。

次のことを行うと、非常に遅くなる可能性があります。

>>> entry = Entry.objects.get(headline__startswith="News")

まず、headlineにはインデックスが付けられていないため、基になるデータベースのフェッチが遅くなります。

次に、ルックアップは、1つのオブジェクトのみが返されることを保証するものではありません。 クエリが複数のオブジェクトに一致する場合、データベースからそれらすべてを取得して転送します。 数百または数千のレコードが返される場合、このペナルティは相当なものになる可能性があります。 データベースが別のサーバー上にある場合、ペナルティはさらに複雑になります。このサーバーでは、ネットワークのオーバーヘッドと遅延も要因になります。


必要になることがわかっている場合は、すべてを一度に取得します

すべての部分が必要となる単一の「セット」データのさまざまな部分に対してデータベースを複数回ヒットすることは、一般に、1つのクエリですべてを取得するよりも効率的ではありません。 これは、ループで実行されるクエリがある場合に特に重要です。そのため、1つだけが必要なときに、多くのデータベースクエリを実行することになります。 そう:

不要なものを取得しないでください

QuerySet.values()およびvalues_list()を使用します

dictまたはlistの値のみが必要で、ORMモデルオブジェクトが不要な場合は、 values()を適切に使用してください。 これらは、テンプレートコード内のモデルオブジェクトを置き換えるのに役立ちます。指定するdictが、テンプレートで使用されているものと同じ属性を持っている限り、問題ありません。


QuerySet.defer()およびonly()を使用します

defer()および only()を使用するのは、それらのロードを回避するために必要がない(またはほとんどの場合は必要ない)ことがわかっているデータベース列がある場合です。 do でそれらを使用する場合、ORMは別のクエリでそれらを取得する必要があるため、不適切に使用すると悲観的になることに注意してください。

データベースは、最終的に数列しか使用しない場合でも、結果の1行について、ディスクからほとんどの非テキスト、非VARCHARデータを読み取る必要があるため、プロファイリングせずにフィールドを延期することに積極的になりすぎないでください。 defer()およびonly()メソッドは、大量のテキストデータの読み込みを回避できる場合、またはPythonに戻すために多くの処理が必要になる可能性のあるフィールドの場合に最も役立ちます。 いつものように、最初にプロファイルを作成し、次に最適化します。


QuerySet.count()を使用する

len(queryset)を実行するのではなく、カウントのみが必要な場合。


QuerySet.exists()を使用する

if querysetではなく、少なくとも1つの結果が存在するかどうかだけを知りたい場合。

しかし:


count()とexists()を使いすぎないでください

QuerySetからの他のデータが必要な場合は、すぐに評価してください。

たとえば、subject属性とユーザーとの多対多の関係を持つEメールモデルを想定すると、次のコードが最適です。

if display_emails:
    emails = user.emails.all()
    if emails:
        print('You have', len(emails), 'emails:')
        for email in emails:
            print(email.subject)
    else:
        print('You do not have any emails.')

次の理由で最適です。

  1. QuerySetはレイジーであるため、display_emailsFalseの場合、これはデータベースクエリを実行しません。
  2. user.emails.all()emails変数に保存すると、その結果キャッシュを再利用できます。
  3. if emailsにより、QuerySet.__bool__()が呼び出され、user.emails.all()クエリがデータベースで実行されます。 結果がない場合はFalseを返し、それ以外の場合はTrueを返します。
  4. len(emails)を使用すると、QuerySet.__len__()が呼び出され、結果キャッシュが再利用されます。
  5. forループは、すでにいっぱいになっているキャッシュを繰り返し処理します。

合計で、このコードは1つまたは0のデータベースクエリを実行します。 実行される唯一の意図的な最適化は、emails変数を使用することです。 カウントにifまたはQuerySet.count()QuerySet.exists()を使用すると、それぞれ追加のクエリが発生します。


QuerySet.update()およびdelete()を使用します

大量のオブジェクトを取得し、いくつかの値を設定して個別に保存するのではなく、 QuerySet.update()を介して一括SQLUPDATEステートメントを使用します。 同様に、可能な場合は一括削除を実行します。

ただし、これらの一括更新メソッドは、個々のインスタンスのsave()またはdelete()メソッドを呼び出すことはできません。つまり、これらのメソッドに追加したカスタム動作は、駆動されるものも含めて実行されません。通常のデータベースオブジェクト signals から。


外部キー値を直接使用する

外部キー値のみが必要な場合は、関連するオブジェクト全体を取得してその主キーを取得するのではなく、取得したオブジェクトにすでに存在する外部キー値を使用します。 NS NS:

entry.blog_id

それ以外の:

entry.blog.id

気にしない場合は結果を注文しないでください

注文は無料ではありません。 注文する各フィールドは、データベースが実行する必要のある操作です。 モデルにデフォルトの順序( Meta.ordering )があり、それが不要な場合は、パラメーターなしで order_by()を呼び出して、QuerySetでモデルを削除します。 。

データベースにインデックスを追加すると、注文のパフォーマンスが向上する場合があります。


バルク方式を使用する

バルクメソッドを使用して、SQLステートメントの数を減らします。

まとめて作成

オブジェクトを作成するときは、可能であれば、 bullk_create()メソッドを使用してSQLクエリの数を減らしてください。 例えば:

Entry.objects.bulk_create([
    Entry(headline='This is a test'),
    Entry(headline='This is only a test'),
])

…より望ましい:

Entry.objects.create(headline='This is a test')
Entry.objects.create(headline='This is only a test')

このメソッドにはいくつかの警告があることに注意してください。したがって、ユースケースに適していることを確認してください。


一括更新

オブジェクトを更新するときは、可能であれば、 bullk_update()メソッドを使用してSQLクエリの数を減らしてください。 オブジェクトのリストまたはクエリセットが与えられた場合:

entries = Entry.objects.bulk_create([
    Entry(headline='This is a test'),
    Entry(headline='This is only a test'),
])

次の例:

entries[0].headline = 'This is not a test'
entries[1].headline = 'This is no longer a test'
Entry.objects.bulk_update(entries, ['headline'])

…より望ましい:

entries[0].headline = 'This is not a test'
entries[0].save()
entries[1].headline = 'This is no longer a test'
entries[1].save()

このメソッドにはいくつかの警告があることに注意してください。したがって、ユースケースに適していることを確認してください。


まとめて挿入

ManyToManyFields にオブジェクトを挿入する場合は、複数のオブジェクトで add()を使用して、SQLクエリの数を減らしてください。 例えば:

my_band.members.add(me, my_friend)

…より望ましい:

my_band.members.add(me)
my_band.members.add(my_friend)

…ここで、BandsArtistsは多対多の関係にあります。

異なるオブジェクトのペアを ManyToManyField に挿入する場合、またはカスタムからテーブルが定義されている場合は、 bullk_create()メソッドを使用してSQLクエリの数を減らします。 例えば:

PizzaToppingRelationship = Pizza.toppings.through
PizzaToppingRelationship.objects.bulk_create([
    PizzaToppingRelationship(pizza=my_pizza, topping=pepperoni),
    PizzaToppingRelationship(pizza=your_pizza, topping=pepperoni),
    PizzaToppingRelationship(pizza=your_pizza, topping=mushroom),
], ignore_conflicts=True)

…より望ましい:

my_pizza.toppings.add(pepperoni)
your_pizza.toppings.add(pepperoni, mushroom)

…ここで、PizzaToppingは多対多の関係にあります。 このメソッドにはいくつかの警告があることに注意してください。したがって、ユースケースに適していることを確認してください。


まとめて削除

ManyToManyFields からオブジェクトを削除する場合は、複数のオブジェクトで remove()を使用して、SQLクエリの数を減らしてください。 例えば:

my_band.members.remove(me, my_friend)

…より望ましい:

my_band.members.remove(me)
my_band.members.remove(my_friend)

…ここで、BandsArtistsは多対多の関係にあります。

ManyToManyFields からオブジェクトの異なるペアを削除する場合は、 Q 式で delete()を使用し、複数のからモデルインスタンスを使用して数を減らします。 SQLクエリの。 例えば:

from django.db.models import Q
PizzaToppingRelationship = Pizza.toppings.through
PizzaToppingRelationship.objects.filter(
    Q(pizza=my_pizza, topping=pepperoni) |
    Q(pizza=your_pizza, topping=pepperoni) |
    Q(pizza=your_pizza, topping=mushroom)
).delete()

…より望ましい:

my_pizza.toppings.remove(pepperoni)
your_pizza.toppings.remove(pepperoni, mushroom)

…ここで、PizzaToppingは多対多の関係にあります。