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

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

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

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

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

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

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

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

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

  • フィールドタイプの適切な使用。

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

QuerySet を理解する

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 のキャッシュ処理を活用するため、with テンプレートタグの使用が推奨されます。

iterator() を使用する

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

explain() を使用する

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

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

例えば:

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

RawSQL を使用する

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

素の SQL を使用する

モデルの取り出しおよび書き込みをするためのカスタム SQL を記述します。django.db.connection.queries を使い、Django があなたのために何を書いているのかを理解して、そこから始めましょう。

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

個々のオブジェクトを取得するために get() を使用する際に、uniquedb_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 つ以上のオブジェクトと一致する場合、すべてのオブジェクトをデータベースから取り出して転送します。この余分な負荷は、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 よりも適切です。

しかし:

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 は結果のキャッシュを繰り返し処理する。

このコードは合計で、データベースクエリを1回または0回実行します。ここで行った唯一の意図的な最適化は、 members 変数を使用することです。もし if に対して QuerySet.exists() を使用したり、in に対して QuerySet.contains() を使用したり、count に対して QuerySet.count() を使用したりした場合、それぞれ追加のクエリが発生します。

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

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

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

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

外部キーの値だけが必要な場合は、関連するオブジェクト全体を取得してそのプライマリキーを取得するのではなく、すでに取得したオブジェクトにある外部キーの値を使います。つまり、次のように書きます。

entry.blog_id

次のようには書かないようにします。

entry.blog.id

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

ソートには計算コストがかかります。データベースはソートする各フィールドに対して操作を行わなければなりません。モデルにデフォルトのソート順 (Meta.ordering) があり、それが不要な場合は、パラメータなしで order_by() を呼び出すことで QuerySet からそれを削除します。

データベースにインデックスを追加すると、並べ替えのパフォーマンスが向上する可能性があります。

一括メソッドを使用する

SQL文の数を減らすために一括(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")

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

一括で更新する

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

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

一括で挿入する

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

my_band.members.add(me, my_friend)

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

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

... ここで、 BandsArtists は多対多の関係です。

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

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 から異なるペアのオブジェクトを取り除くとき、複数の 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)

... ここで、 PizzaTopping は多対多の関係です。

Back to Top