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

DBに何度もアクセスしてデータを取り出す事は非効率になるため、可能な限り少ないクエリで必要なデータを取得すべきです。

Djangoではselect_related()やprefetch_related()を活用する事でDBアクセスを最適化できます。

今回はselect_related()の使い方について解説します。

公式ドキュメントの参照ページ

select_related(*fields)

以下のコードは解説用のモデルです。City, Publisher, Person, Bookの4つのモデルがあります。

BookモデルにはPublisherモデルとPersonモデルを参照するフィールドがあります。

さらに、PersonモデルはCityモデルを参照するフィールドを持っています。

from django.db import models

class City(models.Model):
    # ...
    pass

class Publisher(models.Model):
    # ...
    pass

class Person(models.Model):
    # ...
    hometown = models.ForeignKey(
        City,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )

class Book(models.Model):
    # ...
    publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
    author = models.ForeignKey(Person, on_delete=models.CASCADE)
    pub_date = models.DateField()

select_related()は指定した外部キーのデータを含めたQuerySetを返します。

ForeignKeyと同様に、OneToOneFieldでもselect_related()が使えます。

次の例は、標準のルックアップとselect_related()のルックアップの違いです。

標準のルックアップ

# DBにアクセスして取得
b = Book.objects.get(id=1)

# publisherを取得するために再度DBにアクセスします
p = b.publisher

DBアクセス数は計2回です。

select_relatedのルックアップ

# DBにアクセスしてBookを取得します。 この時、指定したBookに関連するpublisherも同時に取得します。
b = Book.objects.select_related('publisher').get(id=1)

# publisherは既に取得済みのためDBにアクセスはしません。
e = b.publisher

DBアクセス数は計1回です。

select_related()は任意のオブジェクトのクエリセットに対しても使用できます。

from django.utils import timezone

publishers = set()

for b in Book.objects.filter(pub_date__gt=timezone.now()).select_related('publisher'):
    publishers.add(b.publisher)

select_related()を使うとBookのレコード数に関係なく発生するDBアクセス数は1回ですが、

select_related()を使わない場合、DBアクセス数はBookのレコード数×2回 です。

select_related()にはフィールドを複数指定できます。

Queryset.select_related('field1', 'field2')

filter()とselect_related()の順序は重要ではありません。以下は同等です。

Book.objects.filter(pub_date__gt=timezone.now()).select_related('publisher')

Book.objects.select_related('publisher').filter(pub_date__gt=timezone.now())

外部キーの外部キーを追跡

同様の方法で、外部キーの外部キーも追跡できます。

以下のコードを実行する事で、関連するPersonやCityをキャッシュします。

Book.objects.select_related(‘author__hometown’).get(id=1) 

# Bookに関連するPersonクラスのオブジェクトやCityクラスのオブジェクトも取得します。
b = Book.objects.select_related('author__hometown').get(id=1) # DBアクセスあり
a = b.author         # DBアクセス無し
h = a.hometown       # DBアクセス無し

DBアクセス数は計1回です。

一方、select_related()を使わない場合のDBアクセス数は計3回です。

b = Book.objects.get(id=1)  # DBアクセスあり
a = b.author         # DBアクセスあり
h = a.hometown       # DBアクセスあり

クエリ数を確認する方法

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

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

まとめ

select_related()は1回のクエリでForeignKeyやOneToOneFieldのデータもまとめて取得できます。

select_related()以外に、prefetch_related()もクエリ数を減らせる便利なメソッドなので両方使える様になっておくと良いです。

クエリ数は速度に与える影響が大きいので、常にクエリ数に注意しながらコーディングしましょう!

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