N+1クエリ — ループの中でDBを叩く

一覧ページが件数に比例して遅くなる正体が分かり、eager loadingやJOINで1〜2クエリに畳めるようになる。ORMが親切に隠すからこそ気づきにくい定番の性能事故。

中級N+1ORMデータベースパフォーマンスアンチパターン最終更新: 2026-07-28
3つの要点
TL;DR
  1. 親を1回引いた後、子を得るために各行でクエリを発行すると、合計 1 + N 回のDB往復になる。100件の一覧が100回の追加クエリを呼ぶ。
  2. 1回あたりは速くても、往復のレイテンシ×件数が積み上がる。ORMの遅延ロードが自動でやってしまうため、コードを見ても気づきにくい。
  3. 直し方は「まとめて引く」。eager loading(プリロード)・JOIN・IN句での一括取得・DataLoaderで、Nを1〜2クエリに畳む。

一覧ページが、表示件数が増えるほど遅くなる。10件なら快適なのに、100件で明らかにもたつく。その多くはこのN+1クエリです。

横にスクロール

1+N回のDB往復を関連データの集合取得へ改善する比較
ループ内の個別SELECTを、必要な関連だけを取る定数回の問い合わせへ変える。

症状:どう気づくか

  • 一覧の表示件数に比例して応答が遅くなる。
  • 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 loading2回(親+子をまとめて)ORMの標準。まずこれ
JOIN で1回1回1クエリに収めたい/集計と一緒に取りたい
IN句で手動一括2回ORMを使わない/プリロードが効かない経路
DataLoader(バッチ+キャッシュ)2回GraphQLなど、取得が分散する構成
過剰なeager loadingは逆効果

「とりあえず全部 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、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

N+1ORMデータベースパフォーマンスアンチパターン