データベースアクセスの最適化¶
Djangoのデータベース層は、開発者がデータベースを最大限活用できるように、さまざまな方法を提供しています。このドキュメントでは、データベースアクセスを最適化する際にとるべき手順を、大まかにいくつかの見出しとしてまとめています。それぞれの見出しの下に、関連するドキュメントへのリンクを集約した上で、さまざまなヒントを追加しています。
まずはプロファイルを取る¶
一般的なプログラミング手法と同様に、これは言うまでもないことです。どんなクエリを実行し何がコストなのか を判別してください。QuerySet.explain()
を使用し、データベース上で特定の QuerySet
がどのように実行されるかを理解してください。また、django-debug-toolbar といった外部のプロジェクトや、データベースを直接監視するツールを使うのもいいでしょう。
要件に従って、速度またはメモリ、およびその両方を最適化することができます。片方を最適化することは、もう片方に悪影響を及ぼすことがありますが、互いに助けになることもあります。また、データベースプロセスによって行われる処理と Python のプロセスによる処理は (あなたにとって) 必ずしも同等のコストとはなりません。その優先順位とバランスを決めるのはあなた自身です。そして、その設計はアプリケーションやサーバーに依存するため、要求通りに設計するのもあなたの仕事です。
以下で紹介する項目すべてにおいて、あらゆる変更の後に忘れずに分析を行い、施した変更が有益だったこと、およびその恩恵が可読性の低下を十分上回ることを確認してください。以下の すべて の提案において、一般的な原則があなたの状況に当てはまらない可能性があること、それどころか逆効果になりかねない可能性さえあることに十分注意してください。
標準的な DB 最適化のテクニックを使う¶
以下のようなものが上げられます:
- Indexes. プロファイリングでどのようなインデックスを追加すべきかを決定した 後に 、最優先事項として行うべきことです。Djangoからインデックスを追加するには、
Meta.indexes
やField.db_index
を使用します。filter()
、exclude()
、order_by()
などで頻繁に参照するフィールドにインデックスを追加することで、参照を高速化させることができるかもしれません。ただし、最適なインデックスの決定は、各個のアプリケーションのデータベースに依存する複雑な問題であることに留意してください。インデックスを維持するためのオーバーヘッドは、クエリの高速化によるメリットよりも大きいかもしれません。
- フィールドタイプの適切な使用。
以降は、上記の明確な対処は実行済みだという前提で進めていきます。このドキュメントの残りの部分では、不要な作業をしなくて済むように、Django をどのように使えばいいかを中心に説明します。またこのドキュメントでは、汎用キャッシュ のような高コストな最適化手法については説明しません。
QuerySet
を理解する¶
QuerySet を理解することは、シンプルなコードでパフォーマンスを上げるために極めて重要です。特に:
QuerySet
の評価を理解する¶
パフォーマンスの問題を回避するには、以下を理解することが重要です:
- QuerySet は lazy であること。
- いつ QuerySet が評価されるのか <when-querysets-are-evaluated>`。
- どのように データがメモリ上に保持されるか。
キャッシュされる属性を理解する¶
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
デコレータを使用します。
iterator()
を使用する¶
多くのオブジェクトを扱う際には、QuerySet
のキャッシング動作に多くのメモリが使われる可能性があります。この場合、iterator()
が有用です。
explain()
を使用する¶
QuerySet.explain()
を使うと、使用されているインデックスや結合など、データベースがクエリをどのように実行しているのか、詳細な情報を得られます。この詳細情報は、より効率的になるようにクエリを書き換えたり、パフォーマンスを向上させるために追加できるインデックスを特定するのに役立ちます。
データベースの仕事を Python ではなくデータベースに行わせる¶
例えば:
- 最も基本的なレベルでは、filter や exclude を使ってデータベース内でフィルタリングを行います。
F expressions
を使い、同一モデル内で他のフィールドに基づくフィルタリングを行います。- データベース内の集計をするためアノテーション を使います。
必要な SQL を生成するのに不十分な場合は:
素の SQL を使用する¶
モデルの取り出しおよび書き込みをするための独自の SQL を記述します。django.db.connection.queries
を使い、Django があなたのために何を書いているのかを理解して、それを元に始めてください。
ユニークかつインデックス済みの列を使用して個別のオブジェクトを取得する¶
get()
を使って個別オブジェクトを取得する際に、unique
や db_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()
を使用する¶
単に値の dict
や list
がほしいだけで 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)
よりも適切です。
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.")
最適である理由:
- クエリセットは遅延評価されるので、
display_group_members
がFalse
の場合、データベースクエリは発行されない members
変数にgroup.members.all()
を格納することで、クエリ結果のキャッシュを再利用できる。if members:
の行はQuerySet.__bool__()
を呼び出すことになり、これはデータベースでgroup.members.all()
のクエリが処理されることになる。もし結果が得られない場合、False
を返され、そうでない場合はTrue
が返される。if current_user in members:
の行は、結果のキャッシュにユーザーが含まれるかどうかを確認するので、新たなデータベースクエリは発行されない。len(members)
を使用することでQuerySet.__len__()
が呼び出される。このとき、結果のキャッシュが再利用されるので、新たなデータベースクエリは発行されない。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)
ここで、 Pizza
と Topping
は多対多の関連を持っています。このメソッド
には多くの注意事項があるので、自分のユースケースに適切であることを確認してください。
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.