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

Djangoのデータベース層は、開発者がデータベースを最大限活用できるように、さまざまな方法を提供しています。このドキュメントでは、データベースアクセスを最適化する際にとるべき手順を、大まかにいくつかの見出しとしてまとめています。それぞれの見出しの下に、関連するドキュメントへのリンクを集約した上で、さまざまなヒントを追加しています。

まずはプロファイルを取る

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

要件に従って、速度またはメモリ、およびその両方を最適化することができます。片方を最適化することは、もう片方に悪影響を及ぼすことがありますが、互いに助けになることもあります。また、データベースプロセスによって行われる処理と Python のプロセスによる処理は (あなたにとって) 必ずしも同等のコストとはなりません。その優先順位とバランスを決めるのはあなた自身です。そして、その設計はアプリケーションやサーバーに依存するため、要求通りに設計するのもあなたの仕事です。

以下で紹介する項目すべてにおいて、あらゆる変更の後に忘れずに分析を行い、施した変更が有益だったこと、およびその恩恵が可読性の低下を十分上回ることを確認してください。以下の すべて の提案において、一般的な原則があなたの状況に当てはまらない可能性があること、それどころか逆効果になりかねない可能性さえあることに十分注意してください。

標準的な DB 最適化のテクニックを使う

以下のようなものが上げられます:

  • Indexes. プロファイリングでどのようなインデックスを追加すべきかを決定した 後に 、最優先事項として行うべきことです。Djangoからインデックスを追加するには、 Meta.indexesField.db_index を使用します。 filter()exclude()order_by() などで頻繁に参照するフィールドにインデックスを追加することで、参照を高速化させることができるかもしれません。ただし、最適なインデックスの決定は、各個のアプリケーションのデータベースに依存する複雑な問題であることに留意してください。インデックスを維持するためのオーバーヘッドは、クエリの高速化によるメリットよりも大きいかもしれません。
  • フィールドタイプの適切な使用。

以降は、上記の明確な対処は実行済みだという前提で進めていきます。このドキュメントの残りの部分では、不要な作業をしなくて済むように、Django をどのように使えばいいかを中心に説明します。またこのドキュメントでは、汎用キャッシュ のような高コストな最適化手法については説明しません。

QuerySet を理解する

QuerySet を理解することは、シンプルなコードでパフォーマンスを上げるために極めて重要です。特に:

QuerySet の評価を理解する

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

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

As well as caching of the whole QuerySet, there is caching of the result of attributes on ORM objects. In general, attributes that are not callable will be cached. For example, assuming the example blog models:

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

But in general, callable attributes cause DB lookups every time:

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

テンプレート上のコードを読む際には注意が必要です - テンプレートシステムは括弧を許容していませんが、呼び出し可能なオブジェクトは自動的に呼び出されるので、上記の区別が隠れてしまいます。

独自のプロパティにも注意が必要です - 必要なときにキャッシングを実装するのはあなた次第です。たとえば cached_property デコレータを使用します。

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

QuerySet のキャッシング処理を活用するため、with テンプレートタグの使用が推奨されます。

iterator() を使用する

多くのオブジェクトを扱う際には、QuerySet のキャッシング動作に多くのメモリが使われる可能性があります。この場合、iterator() が有用です。

explain() を使用する

QuerySet.explain() を使うと、使用されているインデックスや結合など、データベースがクエリをどのように実行しているのか、詳細な情報を得られます。この詳細情報は、より効率的になるようにクエリを書き換えたり、パフォーマンスを向上させるために追加できるインデックスを特定するのに役立ちます。

データベースの仕事を Python ではなくデータベースに行わせる

例えば:

必要な SQL を生成するのに不十分な場合は:

RawSQL を使用する

保守性は高くありませんが、より強力な方法は RawSQL 表現です。これにより、SQL を明示的にクエリに追加することができます。これでもまだ不十分な場合は:

素の SQL を使用する

モデルの取り出しおよび書き込みをするための独自の SQL を記述します。django.db.connection.queries を使い、Django があなたのために何を書いているのかを理解して、それを元に始めてください。

ユニークかつインデックス済みの列を使用して個別のオブジェクトを取得する

get() を使って個別オブジェクトを取得する際に、uniquedb_index が設定された列を使用するのには 2 つの理由があります。1 つは、データベースインデックスにより受け里が高速になるからです。加えて、複数のオブジェクトが検索にマッチするとクエリは遅くなります; 列にユニーク制限をかけることでこれを完全に防ぐことができます。

So using the example blog models:

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

上記は以下よりも高速です:

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

これは、id がデータベースによってインデックス化されていて、ユニークだと保証されているからです。

以下のようにすると非常に遅くなる恐れがあります:

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

まず第一に、headline はインデックス化されておらず、データベースのデータ取り出しを遅くします。

そして第二に、この検索では単一のオブジェクトが返されることは保証されません。クエリが 1 つ以上のオブジェクトと一致する場合、すべてのオブジェクトをデータベースから取り出して転送します。この余分な負荷は、100 とか 1000 といった多量のレコードが返されるときには相当な量になります。データベースが複数のサーバーによって構成される場合、ネットワークのオーバーヘッドと待ち時間が発生するため、この負荷はさらに大きくなります。

必要なものが分かっているときは一度にすべてを取り出す

すべての部分を必要とする単一のデータセットの異なる部分に対してデータベースを何度もヒットするのは、一般的に、1 つのクエリですべてを取得するよりも非効率です。 これは、1 つのクエリだけが必要なときにループ内でクエリを実行し、その結果何度もデータベースクエリを実行することになってしまう場合に、特に重要となります。そこで:

必要ないものを取り出さない

QuerySet.values()values_list() を使用する

単に値の dictlist がほしいだけで ORM モデルオブジェクトが必要ないときは、values() を適切に使用してください。テンプレートのコード内で、モデルオブジェクトを置き換えるのに役立ちます - 辞書がテンプレートで使われているものと同じ属性を持っている限り問題ありません。

QuerySet.defer()only() を使用する

データベースに使用しない(もしくはほとんど使用されない)列がある場合は、それらを読み込むことを防ぐために、 defer()only() を使用してください。もし除外した列を 使用する 場合、ORMは異なるクエリでそれらを取得する必要があるため、不適切な使用は悲観的な結果を招くに注意してください。

プロファイリングなしに、あまり積極的にフィールドの遅延を行わないようにしてください。たとえ、いくつかのカラムしか使用しないことになったとしても、データベースは結果の1行のために、非テキスト、非 VARCHAR データのほとんどをディスクから読み込まなければならないからです。 defer()only() メソッドは、多くのテキストデータの読み込みを避けられる場合や、Python に戻すのに多くの処理が必要なフィールドに対して最も有効なメソッドです。いつものように、まずはプロファイルを作成し、次に最適化を行ってください。

QuerySet.contains(obj) を使用する

...単に obj がクエリセットに存在するかどうかを知りたいだけなら、 if obj in queryset よりも適切です。

QuerySet.count() を使用する

...単に数を知りたいだけなら、 len(queryset) よりも適切です。

QuerySet.exists() を使用する

...単に1つ以上の結果が存在するかどうかを知りたいだけなら、 if queryset よりも適切です。

But:

contains()count()exists() を使いすぎない

もしクエリセットの他のデータが必要になったときは、すぐに評価を行います。

例えば、 User と多対多の関連をもつ Group モデルを考えたとき、以下のコードが最適です:

members = group.members.all()

if display_group_members:
    if members:
        if current_user in members:
            print("You and", len(members) - 1, "other users are members of this group.")
        else:
            print("There are", len(members), "members in this group.")

        for member in members:
            print(member.username)
    else:
        print("There are no members in this group.")

最適である理由:

  1. クエリセットは遅延評価されるので、 display_group_membersFalse の場合、データベースクエリは発行されない
  2. members 変数に group.members.all() を格納することで、クエリ結果のキャッシュを再利用できる。
  3. if members: の行は QuerySet.__bool__() を呼び出すことになり、これはデータベースで group.members.all() のクエリが処理されることになる。もし結果が得られない場合、 False を返され、そうでない場合は True が返される。
  4. if current_user in members: の行は、結果のキャッシュにユーザーが含まれるかどうかを確認するので、新たなデータベースクエリは発行されない。
  5. len(members) を使用することで QuerySet.__len__() が呼び出される。このとき、結果のキャッシュが再利用されるので、新たなデータベースクエリは発行されない。
  6. for member は結果のキャッシュを繰り返し処理する。

まとめると、このコードで発行されるデータベースクエリは0個または1個です。意図的なクエリの最適化は members 変数の使用のみです。 if に対して QuerySet.exists() を、 in に対して QuerySet.contains() を、カウントに QuerySet.count() を使うことで、それぞれに追加のクエリが発生します。

QuerySet.update()delete() を使用する

複数のオブジェクトを読み込んで値を設定し、それらを個別に保存するのではなく、QuerySet.update() を用いてSQLの一括UPDATE文を使用してください。同様に、可能であれば 一括削除 を使用してください。

ただし、これらの一括更新メソッドは、個々のインスタンスの save()delete() メソッドを呼び出すことはできないので、通常のデータベースオブジェクト signals に由来するものを含め、これらのメソッドに追加した独自の処理が実行されないことに注意してください。

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

外部キーの値だけが必要な場合は、関連するオブジェクト全体を取得してその主キーを取るのではなく、既に持っているオブジェクトにある外部キーの値を使います。つまり、次のようにします:

entry.blog_id

代わりに、次のようにします:

entry.blog.id

気にならないなら結果を並べ替えない

並び替えは無償ではありません。並び替える各フィールドは、データベースが処理しなくてはならない操作です。もしモデルに既定の順序付け (Meta.ordering) があって、それが必要ない場合は、order_by() をパラメータなしで呼び出し、 QuerySet でそれを除去してください。

Adding an index to your database may help to improve ordering performance.

一括メソッドを使用する

SQL文の数を減らすために一括メソッドを使用する

Create in bulk

オブジェクトを作成するとき、可能であれば bulk_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")

このメソッド には多くの注意事項があるので、自分のユースケースに適切であることを確認してください。

Update in bulk

オブジェクトを更新するとき、可能であれば bulk_create() を呼び出し、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()

このメソッド には多くの注意事項があるので、自分のユースケースに適切であることを確認してください。

Insert in bulk

When inserting objects into ManyToManyFields, use add() with multiple objects to reduce the number of SQL queries. For example:

my_band.members.add(me, my_friend)

...は、これよりも適切です:

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

...where Bands and Artists have a many-to-many relationship.

ManyToManyFields から異なるペアのオブジェクトを追加するとき、複数の through モデルインスタンスと Q 式を用いて、 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 は多対多の関連を持っています。このメソッド には多くの注意事項があるので、自分のユースケースに適切であることを確認してください。

Remove in bulk

ManyToManyFields からオブジェクトを取り除くするとき、可能であれば remove() を呼び出し、SQL文の数を減らしてください。例えば:

my_band.members.remove(me, my_friend)

...は、これよりも適切です:

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

...where Bands and Artists have a many-to-many relationship.

ManyToManyFields から異なるペアのオブジェクトを取り除くとき、複数の through モデルインスタンスと 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)

...where Pizza and Topping have a many-to-many relationship.