N+1クエリ — ループの中でDBを叩く
一覧ページが件数に比例して遅くなる正体が分かり、eager loadingやJOINで1〜2クエリに畳めるようになる。ORMが親切に隠すからこそ気づきにくい定番の性能事故。
- 親を1回引いた後、子を得るために各行でクエリを発行すると、合計 1 + N 回のDB往復になる。100件の一覧が100回の追加クエリを呼ぶ。
- 1回あたりは速くても、往復のレイテンシ×件数が積み上がる。ORMの遅延ロードが自動でやってしまうため、コードを見ても気づきにくい。
- 直し方は「まとめて引く」。eager loading(プリロード)・JOIN・IN句での一括取得・DataLoaderで、Nを1〜2クエリに畳む。
一覧ページが、表示件数が増えるほど遅くなる。10件なら快適なのに、100件で明らかにもたつく。その多くはこのN+1クエリです。
横にスクロール
症状:どう気づくか
- 一覧の表示件数に比例して応答が遅くなる。
- DBのスロークエリログに、ほぼ同じSELECTが何十回も並ぶ(
WHERE user_id = ?の?だけ違う)。 - APM(監視ツール)で1リクエストのクエリ本数が数十〜数百になっている。
たとえば「投稿一覧と、各投稿の著者名」を出すコード。
posts = Post.all # 1回:投稿を全部引く
posts.each do |post|
puts post.author.name # N回:投稿ごとに著者を引く
end
post.author に触れるたび、ORMが裏で SELECT * FROM users WHERE id = ? を発行します。投稿が100件なら、1(投稿)+ 100(著者)= 101回のクエリになります。
なぜ悪いのか
問題は1回のクエリの重さではなく、回数です。DBへの1往復には、クエリ自体の実行時間とは別に、ネットワークの往復遅延(RTT)が乗ります。1回0.5msでも、100回で50msがまるまる待ち時間として積み上がる。件数が増えるほど線形に悪化し、しかもORMが遅延ロードで自動的にやってしまうため、ソースを読んでも「ループの中でDBを叩いている」ようには見えません。ここが厄介さの本質です。
直し方
原則は「必要なものを、まとめて引く」。
eager loading(プリロード):関連を先読みするようORMに指示する。多くのORMが includes / with / prefetch_related といった仕組みを持ちます。
posts = Post.includes(:author).all # 2回にまとまる
posts.each { |post| puts post.author.name } # 追加クエリは発生しない
これで発行されるのは「投稿を引く1回」と「登場する著者IDをまとめて引く1回(WHERE id IN (...))」の2回だけになります。
| 手法 | クエリ数 | 向く場面 |
|---|---|---|
| eager loading | 2回(親+子をまとめて) | ORMの標準。まずこれ |
| JOIN で1回 | 1回 | 1クエリに収めたい/集計と一緒に取りたい |
| IN句で手動一括 | 2回 | ORMを使わない/プリロードが効かない経路 |
| DataLoader(バッチ+キャッシュ) | 2回 | GraphQLなど、取得が分散する構成 |
「とりあえず全部 includes」も別の問題を生みます。使わない関連まで先読みすると、無駄なデータ転送とメモリを食う。その画面で実際に使う関連だけを先読みするのが正解です。N+1を潰すことと、引きすぎないことの両立を狙います。
まとめ
- N+1は、親1回のあとに子をN回引いてしまう定番の性能事故。件数に比例して遅くなるのが特徴。
- 原因は往復回数。1回が速くてもレイテンシ×Nが積み上がる。ORMの遅延ロードが自動でやるため気づきにくい。
- 直し方は「まとめて引く」。eager loading をまず試し、必要なら JOIN や IN句一括、分散取得なら DataLoader。
- ただし引きすぎも別の害。その画面で使う関連だけを先読みする。
アンチパターン図鑑の記事ガイド
N+1クエリ — ループの中でDBを叩くを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
N+1
比較で見る軸
難易度: intermediate / カテゴリ: アンチパターン図鑑 / タグ数: 5
導入後に効く点
1回あたりは速くても、往復のレイテンシ×件数が積み上がる。ORMの遅延ロードが自動でやってしまうため、コードを見ても気づきにくい。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- intermediate
- カテゴリ
- アンチパターン図鑑
- タグ数
- 5
判断チェックリスト
- 自社の用途が「N+1 / ORM」に近いか確認する。
- 強みである「親を1回引いた後、子を得るために各行でクエリを発行すると、合計 1 + N 回のDB往復になる。100件の一覧が100回の追加クエリを呼ぶ。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。