はじめに
Djangoで関連モデルのデータを取得する際、クエリが増えすぎる「N+1問題」に悩んだことはないだろうか。select_relatedとprefetch_relatedを使えば、この問題を効率的に解決できる。
QuerySetとは
DjangoのQuerySetは、遅延評価されるSQLクエリの設計図だ。データが必要になるまでSQLは実行されない。
# まだSQL未実行
queryset = Article.objects.all()
# ここでSQL実行
for article in queryset:
print(article.title)
メソッドチェーンで条件を追加できる
queryset = (Article.objects
.filter(status='published')
.select_related('author')
.order_by('-created_at'))
N+1問題とは
記事一覧で著者名も表示したい場合を考える。
# 悪い例:N+1問題が発生
articles = Article.objects.all() # 1回目のクエリ
for article in articles:
print(article.author.name) # 各ループでクエリ実行!
記事が10件なら合計11回のクエリが実行される。
select_related:JOINで解決
select_relatedは、ForeignKeyやOneToOneFieldの関連を1つのSQLクエリ(JOIN)で取得する。
# 1回のクエリで記事と著者を取得
articles = Article.objects.select_related('author').all()
for article in articles:
print(article.author.name) # 追加クエリなし
複数の関連を取得
# 複数指定可能
articles = Article.objects.select_related('author', 'category').all()
# ネストした関連も可能
articles = Article.objects.select_related('author__company').all()
制限事項
ForeignKeyとOneToOneFieldのみ対応。ManyToManyや逆参照には使えない。
prefetch_related:別クエリで効率化
prefetch_relatedは、複数の関連を別SQLで取得しPython側で結合する。ManyToManyや逆参照に対応。
# 記事とタグを効率的に取得(2クエリ)
articles = Article.objects.prefetch_related('tags').all()
for article in articles:
for tag in article.tags.all(): # 追加クエリなし
print(tag.name)
ネストした関連
# 記事 → コメント → 投稿者
articles = Article.objects.prefetch_related(
'comments__author'
).all()
逆参照のForeignKey
# 著者とその記事一覧
authors = Author.objects.prefetch_related('article_set').all()
for author in authors:
for article in author.article_set.all():
print(article.title)
使い分け
| 関連の種類 | 使用メソッド |
|---|---|
| ForeignKey / OneToOneField | select_related |
| ManyToManyField | prefetch_related |
| 逆参照のForeignKey | prefetch_related |
| ネストした関連 | 両方を組み合わせ |
実践例
ブログ記事一覧
articles = Article.objects.select_related(
'author', 'category'
).prefetch_related(
'tags', 'comments'
).all()
for article in articles:
print(f"{article.title} by {article.author.name}")
print(f"Tags: {', '.join([t.name for t in article.tags.all()])}")
よくある間違い
ManyToManyにselect_relatedを使う
# エラーになる
articles = Article.objects.select_related('tags').all() # ❌
取得済みなのに再クエリ
articles = Article.objects.select_related('author').all()
for article in articles:
# 不要なクエリ
author = Author.objects.get(id=article.author_id) # ❌
不要な関連を取得
# authorを使わないのに取得
articles = Article.objects.select_related('author').all()
for article in articles:
print(article.title) # authorは未使用
まとめ
-
select_related:ForeignKey/OneToOneをJOINで1クエリ化 -
prefetch_related:ManyToMany/逆参照を別クエリで効率化 - 両方を組み合わせてN+1問題を解決
- 必要な関連だけを指定して無駄を避ける
関連データを取得する前に「本当に必要か?」「どう取得するのが効率的か?」を考える習慣をつけよう。