N+1問題 — 職員室に10回も聞きに行かない
「ブログの記事一覧に、記事10件とそれぞれの著者名を表示したい」— ごく普通の要件です。ところが素直に書いたコードが、データベースへの問い合わせを11回も発行していることがあります。これがN+1問題。アプリが遅くなる原因の、定番中の定番です。
たとえるなら、職員室(データベース)と教室(アプリ)の間を、生徒の名前を1人ずつ聞きに10回往復するか、名簿を1回でもらってくるかの違いです。下の図1で、2つのやり方を実際に走らせて比べてみてください。
1回1回は速い。積もると遅い
N+1問題のたちが悪いところは、1つ1つのクエリはまったく遅くないことです。図1のとおり、効いてくるのはクエリの中身ではなく往復そのもの。アプリとデータベースは別のコンピュータにいることが多く、1往復ごとにネットワークの待ち時間(数ミリ秒)がまるごとかかります。記事が10件なら11往復、100件なら101往復 — 表示する件数(N)に比例して、勝手に往復が増えていくのです。
原因のほとんどは「一覧をループで回しながら、ループの中で関連データを1件ずつ取りに行く」コードです。ORM(RailsのActive RecordやPrismaなど)を使っていると、post.author.nameと書いただけで裏でクエリが走るため、SQLを1行も書いていないのに発生するのが見つけにくさの理由です。
対策はシンプルで、「あとで1人ずつ聞く」のをやめて最初にまとめて聞くこと。図1の②のようにJOINで1回で取るか、記事を取ったあとに「この著者ID全員分の名前をください」とIN句で2回目にまとめて取る(ORMのeager loading、includesやinclude)のが定石です。11回が1〜2回になります。
- N+1問題
- 一覧N件の取得1回+関連データの取得N回で、計N+1回のクエリが走ってしまうこと。
- JOIN
- 複数のテーブルをつなげて、1回のクエリでまとめて取る書き方。
- eager loading
- ORMに「関連データも最初にまとめて読んでおいて」と指示する機能。N+1の定番対策。
まとめ
N+1問題は「クエリが重い」のではなく「往復が多い」問題です。開発中はN=10で気づかず、本番でN=1000になって悲鳴を上げる — というのがお決まりのパターン。一覧ページが遅いと感じたら、まずクエリログを開いて同じ形のSQLがずらっと並んでいないかを見てください。それがN+1の足跡です。