0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

君もDjango🐍のselect_related🐍とprefetch_related🐍でN+1問題を解決しないか 🤔

Posted at

はじめに

Djangoで関連モデルのデータを取得する際、クエリが増えすぎる「N+1問題」に悩んだことはないだろうか。select_relatedprefetch_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問題を解決
  • 必要な関連だけを指定して無駄を避ける

関連データを取得する前に「本当に必要か?」「どう取得するのが効率的か?」を考える習慣をつけよう。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?