開発・技術選定

Django が遅いと言われたら最初に見る N+1 — select_related と prefetch_related の使い分け

「一覧ページが遅い」と相談を受けたとき、最初に疑うのは N+1 クエリです。
サーバーのスペックを上げる前に、まずここを確認します。

N+1 とは

# 記事を10件取得(クエリ1回)
posts = Post.objects.all()[:10]

for post in posts:
    print(post.category.name)   # ← ここで1件ずつ追加のクエリ(10回)

合計 1 + 10 = 11回。100件なら101回になります。
1クエリが 5ms でも、100件で 0.5 秒がここだけで消えます。

まず計測する

推測せず、実際のクエリ数を見ます。

from django.db import connection, reset_queries

reset_queries()
list(Post.objects.all()[:10])
print(len(connection.queries))

django-debug-toolbar を入れれば画面上で確認できます。
本番で使う場合は DEBUG=False で無効になる点に注意してください。

select_related — 正方向の1対1・多対1

JOIN で一度に取ってくる方式です。ForeignKey と OneToOne に使います。

posts = Post.objects.select_related("category", "author")

発行されるのは1クエリだけ。

SELECT ... FROM post
INNER JOIN category ON ...
INNER JOIN user ON ...

prefetch_related — 逆方向・多対多

別クエリで取ってきて Python 側で結合する方式です。
ManyToMany と逆参照(related_name)に使います。

posts = Post.objects.prefetch_related("tags")

クエリは2回になります(記事の取得 + タグの一括取得)。
JOIN すると行が増えてしまうケースでは、こちらのほうが速くなります。

使い分けの判断

関係 使うもの
ForeignKey(自分 → 相手) select_related
OneToOne select_related
ManyToMany prefetch_related
逆参照(相手 → 自分の集合) prefetch_related

「1件に決まるなら JOIN、複数になるなら別クエリ」と覚えると迷いません。

入れ子の絞り込みは Prefetch で

タグの中でも公開中のものだけ、というときは Prefetch を使います。

from django.db.models import Prefetch

posts = Post.objects.prefetch_related(
    Prefetch("tags", queryset=Tag.objects.filter(is_visible=True))
)

件数だけ欲しいなら annotate

コメント数を出すために全件取得するのは無駄です。

from django.db.models import Count

posts = Post.objects.annotate(comment_count=Count("comments"))

DB 側で数えるので、Python にデータを持ってこずに済みます。

まとめ

  • 遅いと感じたら、まずクエリ数を数える(推測しない)
  • 1件に決まる関係は select_related、複数になる関係は prefetch_related
  • 絞り込みたいときは Prefetch、数えたいだけなら annotate

インフラを増強する前に、ここを直すだけで体感が変わることがほとんどです。