N+1問題
N+1 Query Problem
一覧の取得に件数ぶんのSQLが余計に発行され、件数に比例して遅くなる典型的な性能問題。
概要
N+1問題は、「一覧を1回のクエリで取得したあと、その各行に紐づく関連データを1件ずつ別のクエリで取りに行ってしまう」ことで、合計 1+N 本の SQL が発行されてしまう性能問題です。たとえばブログ記事を100件表示する画面で、記事一覧の取得に1本、各記事の著者名の取得に100本、合わせて101本のクエリが飛ぶ — これが典型的なN+1です。
厄介なのは、1本1本のクエリはごく軽いため、開発中のデータ件数が少ない環境ではまったく問題に見えないことです。本番でデータが増えた途端、クエリ本数がデータ件数に比例して膨れ上がり、「一覧画面だけ異様に遅い」「DBの接続が枯渇する」といった形で表面化します。Webアプリケーションの性能問題の中でも、もっとも頻繁に踏まれる落とし穴のひとつです。
なぜ生まれたか
N+1問題は特定の技術の欠陥というより、ORM の便利さの裏側で生まれた副作用です。ORMはデータベースの行をオブジェクトとして扱わせてくれるため、article.author.name のようにプロパティを辿るだけで関連データにアクセスできます。このとき多くのORMは「遅延ロード(lazy loading)」、つまり実際にアクセスされた瞬間に初めてクエリを発行する戦略を採ります。使わないデータを読まずに済む合理的な既定値ですが、ループの中で関連を辿ると、1周ごとに1本のクエリが発行されてしまいます。
つまりN+1は、「SQLを書かなくてよい」という抽象化によってクエリの発行タイミングがコードの見た目から消えたことで生まれた問題です。生SQLを書いていた時代なら JOIN を1本書いて終わりだった処理が、オブジェクトを自然に辿るコードでは無数のクエリに化ける。だからこそ対策も「抽象化を捨てる」のではなく、「これから関連データを使う」とORMに事前に伝えるイーガーロード(eager loading)という形で整備されてきました。
詳細
1+N本のクエリが飛ぶ様子
記事100件と著者を表示する処理を時系列で追うと、次のようになります。アプリとDBの間の往復には、クエリ実行そのものに加えてネットワークのラウンドトリップ時間が毎回かかる点に注目してください。
仮に1往復が2ミリ秒でも、101往復なら200ミリ秒。記事が1万件なら20秒です。処理時間がデータ件数 N に比例して伸びるため、開発環境の数件のデータでは気づけず、本番で顕在化するという経過をたどります。また、大量の細かいクエリはコネクションプールの枠を長時間占有し、遅延だけでなくDB全体の詰まりの原因にもなります。
解決策1: JOIN で1本にまとめる
もっとも直接的な解決は、関連テーブルを JOIN して1本のクエリで取り切ることです。ORMでは「join fetch」「includes」などの名前で、関連を結合込みで取得する指定が用意されています。1回の往復で全データが揃うため往復コストは最小になりますが、1対多の関連を JOIN すると親側の行が子の件数ぶん重複して返るため、多段の関連を一度に結合すると結果セットが掛け算で膨らむ点には注意が必要です。
解決策2: IN句で2本に分ける
もうひとつの定番は、まず一覧を取得し、そこに含まれるIDを集めて WHERE author_id IN 句 で関連データをまとめて取る方法です。クエリは2本になりますが、N には依存しません。1対多の関連でも結果が重複せず、多くのORM(Rails の includes、Prisma、Hibernate の batch fetching など)が内部でこの戦略を使います。JOIN と IN のどちらが速いかは関連の形やデータ量次第で、迷ったら実行計画を確認して判断します。
見つけ方と実務の勘所
N+1はコードレビューで見抜くより、観測で見つけるほうが確実です。開発環境でクエリログを出して「同じ形のクエリがIDだけ変えて連発されていないか」を見る、Rails の bullet のようなN+1検出ツールを入れる、APM で1リクエストあたりのクエリ本数を監視する、といった方法が定番です。「1画面あたりのクエリ本数はデータ件数に依存しない」を目安にすると判断しやすくなります。
一方で、すべての関連を常にイーガーロードするのも誤りです。使わない関連まで毎回結合すれば、今度は1本のクエリが不必要に重くなります。遅延ロードは「使うかどうか分からないデータを読まない」ための最適化であり、「この画面では確実に使う」と分かっている関連だけを事前ロードに切り替えるのが正しい使い分けです。N+1問題は、ORMの抽象の裏で実際にどんなSQLが流れているかを常に意識する、というデータベースプログラミングの基本姿勢を教えてくれる語彙でもあります。
