ホーム / 技術ブログ / Django ORM で N + 1 問題 を回避する

Django ORM で N + 1 問題 を回避する

投稿日:June 1, 2021 (火)

はじめに

この記事では、N + 1 問題について、備忘の目的で記録をします。

それに当たって参考にしたサイトは次のようなものです。このような情報を書き残して下さった皆さんに感謝します。

N + 1 問題とは

N+1 問題とは、ORM を使用しているときに発生しがちな問題で、

  • あるテーブルから、一覧に表示する N 件のレコードを取得するために SELECT を 1 回実行
  • 別のテーブルから、上で取得した N 件のレコードに紐づくデータを取得するために SELECT を各 1 回、つまり 合計 N 回実行

するために、総計 N + 1 回のクエリを実行する状態を言う(順番的には「1 + N 問題」といった方が適切かもしれない)。

Django で N + 1 問題を回避する方法

例えば、次のような Django モデルを考える。

from django.db import models

class Prefecture(models.Model):
  """
  都道府県テーブル
  """

    class Meta:
        db_table = 'prefecture'

    name = models.CharField(
        verbose_name='都道府県名'
    )

class Shop(models.Model):
    """
    店舗テーブル
    """

    class Meta:
        db_table = 'shop'

    name = models.CharField(
        verbose_name='店舗名'
    )
    prefecture = models.ForeignKey(
        Prefecture, on_delete=models.PROTECT,
    )

ここで、店舗一覧を取得し、その店舗の所在地の都道府県を表示するようなユースケースがあるとき、Django の ORM で次のような書き方をすると N + 1 問題が発生する。

from models import Prefecture, Shop

queryset = Shop.Objects.all()

for shop in queryset:
    print(f'店舗名:{shop.name} (所在地:{shop.prefecture.name})')

つまり、店舗一覧のデータを取得して(1 回)、その所在地の都道府県を取得するために、それぞれの店舗に対して毎回(N 回)次のようなクエリが発行されていることになる。

SELECT * FROM prefectures WHERE prefectures.id = {shop.prefecture.id};

生の SQL を書くとすれば、次のように予め都道府県テーブルと店舗テーブルを結合しておけば良い。

SELECT * FROM shop
INNER JOIN prefecture
    ON shop.prefecture = prefecture.id;

この例において Django の ORM で N + 1 問題を回避するためには、 select_related() の記述を追加する。

from models import Prefecture, Shop

queryset = Shop.Objects.all().select_related('prefecture')

for shop in queryset:
    print(f'店舗名:{shop.name} (所在地:{shop.prefecture.name})')

このようにすることで、queryset からクエリが発行される際に、 select_related() で指定した外部キーのテーブルが JOIN されるようになって、クエリの発行回数が 1 回で済むようになる。