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

Django のデータベース層は、開発者がデータベースから多くを得ることを助ける色々 な方法を提供しています。このドキュメントは関連するドキュメントのリンクを集めて 色々なヒントを加え、データベース利用の最適化を試みる時に踏むべきステップを概略 する多くの見出しのもとに整理しています。

まずは計測

一般的なプログラミングの実践として、これは言うまでもないことです。 実行されるクエリと実行にかかるコスト を見てく ださい。また、 django-debug-toolbar のような外部のプロジェクトや、データベー スを直接モニタするツールを使っても良いでしょう。

必要に従って、速度を最適化するのか、メモリを最適化するのか、または両方なのかを 思い出してください。時には一方の最適化が他方にとって有害なものになりますが、ま た時には互いに助けになります。また、データベースプロセスでの処理は、 Python プ ロセスで行われる同じ量の処理と同じコストにならないかもしれません。優先度がどう か、バランスを取るべきか、全ての計測が必要か、を決定するのはあなたです。これら はアプリケーションとサーバに依存するでしょうから。

以下の全てにおいて、変更が利益を生んでいることを確かめるため、そして大きすぎる 利益がコードの可読性を下げていないかを確かめるため、変更を行うごとに計測するの を忘れないようにしてください。以下の示唆 ** 全て ** について言えることですが、 あなたの環境では一般的な原則が当てはまらないかもしれない、もしかすると逆かもし れないということに注意してください。

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

以下が含まれます:

  • インデックス。計測の結果インデックスを追加するべきだと決心した 後には これ を最も優先すべきです。インデックスを追加するには django.db.models.Field.db_index を使います。
  • 適切なフィールドの型を使います。

上記の明らかなことは既に行ってあると仮定します。このドキュメントの残りでは、必 要のないことをしないためにどのように Django を使うかに焦点を当てます。このド キュメントはまた、 汎用的なキャッシング のような高くつ く操作全般に適用するような最適化技術は扱いません。

クエリセットを理解する

クエリセット を理解することはシンプルなコードで 良いパフォーマンスを得るために欠かせません。とりわけ:

クエリセットの評価を理解する

パフォーマンスの問題を避けるには、次のことを理解することが大切です:

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

QuerySet 全体をキャッシュするのと同様、 ORM オブジェクトの属性の結果も キャッシュされます。一般的に呼び出し可能でない属性はキャッシュされます。例えば ブログでのモデル例 を前提にすると:

>>> entry = Entry.objects.get(id=1)
>>> entry.blog   # Blog オブジェクトはこの時点で検索されます
>>> entry.blog   # キャッシュバージョンが使われ、 DB アクセスしません

しかし一般的に、呼び出し可能な属性は毎回 DB 参照を行います:

>>> entry = Entry.objects.get(id=1)
>>> entry.authors.all()   # クエリが実行された
>>> entry.authors.all()   # クエリがまた実行された

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

自分のカスタムプロパティに注意してください。キャッシュを実装するかは開発者に任 されています。

with テンプレートタグを使う

QuerySet のキャッシング動作を利用するには、 with テンプレートタグ を使う必要があるかもしれません。

iterator() を使う

多くのオブジェクトを持っている時、 QuerySet のキャッシング動作は大量のメモ リ使用につながる可能性があります。この場合、 iterator() が助けになるかもしれませ ん。

Python よりもデータベースで行うべき動作

具体例として:

  • 最も基本的なレベルでは、 filter と exclude を使って データベースのフィルタリングをすることができます。

必要なSQLを生成するのにこれらで不十分なら:

QuerySet.extra() を使う

移植性は比較的低いですが、より強力なメソッドが extra() です。これはクエリに対する明 示的な SQL の追加を可能にします。これでもまだ十分強力じゃないなら:

生の SQL を使う

データを検索したりモデルを作成するためのカスタム SQL を自分で書いてください。 django.db.connection.queries を使うと、 Django が 書き出したクエリを見ることができますので、そこから始めましょう。

必要になると分かっているデータを一度に検索する

一般的に、一つのデータ「集合」の別々の部分のために、データベースを複数回叩くの は、一つのクエリで一度に検索するよりも効率が低いです。 もしクエリがループの中で実行されていて、それ故に多くのデータベースクエリを一つ だけにまとめることが必要なら、特に重要です。

必要ないものを検索しない

QuerySet.values()values_list() を使う

値の dictlist が必要なだけで、 ORM モデルオブジェクトが必要ないな ら、 values() を適切に使ってください。 これらはテンプレートコードのモデルオブジェクトを置き換えるのに便利です。辞書が テンプレートで使われているものと同じ属性を持っているなら、うまく行きます。

QuerySet.defer()only() を使う

必要のない (またはほとんどの場合に必要ない) データベースカラムがあると分かって いるなら、それらをロードしないように defer()only() を使ってください。 もしそのようなカラムを 使う 場合は、 ORM が個別のクエリで取りに行かなければ ならないことに注意してください。不適切にそれを使うなら、悲観的に考えましょう。

また、遅延フィールドを使ってモデルを構築する時に Django の中でいくらかの (小さ な追加の) オーバーヘッドが発生することに注意してください。計測なしでの遅延 フィールドの使用に積極的になりすぎないでください。わずかなカラムしか使っていな い時でも、データベースは結果の 1 つの行のためにほとんどの非 text 型、非 VARCHAR 型のデータをディスクから読み出さなければなりません。 defer()only() メソッドは多くのテキストデータや Python データに戻すために多くの処 理が必要となるフィールドのロードを避けるのにはとても便利です。例によって、まず は計測、それから最適化をしましょう。

QuerySet.count() を使う

数を数えたいだけなら、 len(queryset) を使うよりもこちらの方が良いです。

QuerySet.exists() を使う

少なくとも 1 つの結果が存在するかを調べたいだけなら、 if queryset よりもこ ちらの方が良いです。

しかし:

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

QuerySet からの他のデータが必要なら、それを評価しましょう。

例えば、 body 属性とユーザへの多対多のリレーションを持っている電子メールモ デルを考えましょう。以下のテンプレートコードは最適です:

{% if display_inbox %}
  {% with emails=user.emails.all %}
    {% if emails %}
      <p>You have {{ emails|length }} email(s)</p>
      {% for email in emails %}
        <p>{{ email.body }}</p>
      {% endfor %}
    {% else %}
      <p>No messages today.</p>
    {% endif %}
  {% endwith %}
{% endif %}

これが最適な理由は:

  1. QuerySets が遅延評価されるので display_inbox が False の場合にデータベース アクセスが発生しません。
  1. with を使うと、あとで使われるために user.email_all を変数に保存 します。再利用できるキャッシュが使われます。
  1. {% if emails %} 行は QuerySet.__nonzero__() の呼び出しを引き起こし ます。これは user.emails.all() がデータベースで実行されることにつながり ます。少なくとも一行目が ORM オブジェクトに変換されます。結果がなければ False を返し、あれば True を返します。
  1. {{ emails|length }}QuerySet.__len__() を呼びます。他のクエリを 実行せずにキャッシュの残りを満たします。
  1. for ループは既に満たされているキャッシュの上に繰り返されます。

全体として、このコードは 1 個か 0 個のデータベースクエリを実行します。 唯一の計画的な最適化の実施が with タグ の使用です。どの箇所で QuerySet.exists()QuerySet.count() を使っても追加のクエリの原因にな ります。

QuerySet.update()delete() を使う

オブジェクトを検索してロードし、何かの値をセットして個別に保存するよりも、 QuerySet.update() 経由でバルクの SQL UPDATE 文を使うほうが良いです。同 様に、可能な限り バルクでの削除 を使いましょ う。

しかしながら、これらのバルクアップデートメソッドは個別のインスタンスの save()delete() メソッドを呼べないことに注意してください。つまり、 通常のデータベースオブジェクト シグナル によって起動され るものも含め、開発者がこれらのメソッドに追加したいかなるカスタムの動作も実行さ れないことになります。

外部キーの値を直接使う

外部キーの値が必要なだけなら、関連するオブジェクトの全体を取得してそのプライマ リキーを得るのではなく、既に取得しているオブジェクトの外部キーの値を使いましょ う。例えばこのようにする代わりに:

entry.blog.id

こうします:

entry.blog_id

バルクでのインサート

複数のオブジェクトを作る時、可能であれば、 bulk_create() メソッドを使って SQL ク エリの数を減らしましょう。例えば:

Entry.objects.bulk_create([
    Entry(headline="Python 3.0 Released"),
    Entry(headline="Python 3.1 Planned")
])

が以下よりも望ましいです:

Entry.objects.create(headline="Python 3.0 Released")
Entry.objects.create(headline="Python 3.1 Planned")

このメソッドに関する注意 がたくさんありますので、ユースケースに合致しているか確認してください。

このことは ManyToManyFields にも当 てはまります。このようにすることは:

my_band.members.add(me, my_friend)

以下よりも良いです:

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

ここで BandsArtists は多対多の関係を持っているとします。