「一覧ページが遅い」と相談を受けたとき、最初に疑うのは 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
インフラを増強する前に、ここを直すだけで体感が変わることがほとんどです。