Django prefetch_relatedを使ってDBアクセスを高速化する方法 前編

DBに何度もアクセスしてデータを取り出す事は非効率になるため、可能な限り、少ないクエリでデータを取得する必要があります。Djangoではselect_related()やprefetch_related()を使う事でクエリを最適化できます。

今回はprefetch_related()を使って高速化する方法を前編・後編に分けて解説します。

前編はprefetch_related()の基本的な使い方を、後編はPrefetchオブジェクトと組み合わせて使う方法を解説します。

公式ドキュメントのprefetch_related()の解説の中から、主に高速化に関する部分を解説しています。

prefetch_related(*lookups)

prefetch_related()を使うと、指定したlookupsそれぞれに対して関連するオブジェクトを自動的に取得したQuerySetを返します。

select_related() は単一値のリレーションシップ(外部キーと一対一)のみに制限されていますが、prefetch_relatedは多対多および一対多のオブジェクトも事前に読み込みます。

例)以下のようなモデルがあるとします。

from django.db import models

class Topping(models.Model):
    name = models.CharField(max_length=30)


class Pizza(models.Model):
    name = models.CharField(max_length=50)
    toppings = models.ManyToManyField(Topping)

    def __str__(self):
        return "%s (%s)" % (
            self.name,
            ", ".join(topping.name for topping in self.toppings.all()),
        )

以下を実行します。

>>> Pizza.objects.all()
["マルゲリータ (バジル, モッツァレラ)", "シーフード (イカ, サーモン)", ...]

問題は、Pizza.__str__() が self.toppings.all() を要求するたびデータベースにクエリを実行する必要があることです。

そのため、Pizza.objects.all() はPizzaのQuerySetの全ての要素に対してToppingsテーブルのクエリを実行します。

しかし、prefetch_related()を使えばクエリを2回に削減できます。これは、prefetch_related()により追加されるクエリが、QuerySetの評価が開始され1回目のクエリが実行された後に実行されるからです。

>>> Pizza.objects.all().prefetch_related('toppings')

このコードにより全てのPizzaとそれらに関連するtoppingsも取得し、キャッシュします。そして、self.toppings.all()を実行するときは、キャッシュしたデータを利用します。

外部参照データの外部参照データも可能です。

上記の例に以下のRestaurantモデルを追加します。

class Restaurant(models.Model):
    pizzas = models.ManyToManyField(Pizza, related_name='restaurants')
    best_pizza = models.ForeignKey(Pizza, related_name='championed_by', on_delete=models.CASCADE)

以下の様に書けます。

>>> Restaurant.objects.prefetch_related('pizzas__toppings')

これはレストランに所属するすべてのピザと、それぞれのピザに所属するすべてのトッピングを事前読み込みます。

これにより、レストラン用、ピザ用、トッピング用の計3回のクエリが実行されます。

ForeignKeyとManyToManyFieldの組み合わせでも可能です。クエリ数は上記と同様で3回です。

>>> Restaurant.objects.prefetch_related('best_pizza__toppings')

さらに、select_related()も使う事でクエリ数を2回まで減らせます

>>> Restaurant.objects.select_related('best_pizza').prefetch_related('best_pizza__toppings')

Restaurantデータとbest_pizzaデータを1度のクエリ実行で取得し、その後、best_pizzaに関連するtoppingsデータを取得するためにもう1回クエリを実行します。

クエリ数の確認方法

クエリ数を確認するにはDjango Debug Toolbarというプラグインがおすすめです。

ブラウザ上でクエリ数や実行したクエリを確認できるため非常に便利です。

まとめ

prefetch_related()は多対多および一対多のオブジェクトも事前に読み込みます。

prefetch_related()とselect_related()を組み合わせるとさらにクエリ数を減らせるケースもあるので両方使える様になっておくと良いです。クエリ数は速度に与える影響が大きいので、常にクエリ数に注意しながらコーディングしましょう!

後編は、Prefetchオブジェクトprefetch_related()を組み合わせた発展的な方法を解説します。

スポンサーリンク
スポンサーリンク