【えぬぷらすわんもんだい】

N+1問題 とは?

💡 「1回で済むはずのクエリがN回余計に走る」パフォーマンス問題
📌 このページのポイント
N+1問題(悪い例) JOINで解決(良い例) アプリ DB 1. 一覧取得 2. 詳細取得 id=1 3. 詳細取得 id=2 4. 詳細取得 id=3 ... N+1. 詳細取得 id=N N回繰り返し 合計 N+1 回のクエリ(遅い) アプリ DB 1回のJOINクエリ 全データ一括取得 SELECT * FROM orders JOIN details ON orders.id = details.order_id 合計 1 回のクエリ(高速)
N+1問題の仕組み
ひよこ ひよこ

N+1問題って具体的にどういう状態?

ペンギン先生 ペンギン先生

例えば「記事一覧と各記事の著者名を表示する」場面。まず「SELECT * FROM articles」で記事を10件取得(1回目)。次にループで各記事の「SELECT * FROM users WHERE id = ?」を10回実行(N回)。合計11回のクエリが走る。JOINすれば1回で済むのにね。

ひよこ ひよこ

なんでこんなことが起きるの?

ペンギン先生 ペンギン先生

ORMの遅延ロード(Lazy Loading)が主な原因。「article.author」にアクセスしたときに初めてクエリが走る仕組みだと、ループ内で毎回クエリが発行される。コード上は「article.author.name」と書いただけなのに、裏でSQLが走っていることに気づかないんだ。

ひよこ ひよこ

どうやって解決するの?

ペンギン先生 ペンギン先生

Eager Loading(事前にまとめて読み込む)が王道。RailsならArticle.includes(:author)、LaravelならArticle::with('author')と書くと、記事取得と著者取得が2回のクエリで済む。もしくはJOINで1回のクエリにまとめることもできるよ。

ひよこ ひよこ

Eager Loadingにしておけば万事解決?

ペンギン先生 ペンギン先生

単純にはそうなんだけど、Eager Loadingも「使わないリレーションまでEagerにする」と無駄なデータを読み込んでしまう。さらにネストしたリレーション(記事→著者→所属組織→所在地...)をどこまでEagerにするかの判断が厄介。深くEagerにしすぎると1クエリが巨大なJOINになって、それはそれで遅い。画面ごとに必要なリレーションだけをEagerにする設計が大事なんだけど、画面要件が変わるたびに修正が必要になるからメンテナンスコストとの戦いでもあるんだ。

ペンギン
まとめ:ざっくりこれだけ覚えればOK!
N+1問題って出てきたら「1+N回もクエリが走る無駄な状態で、JOINやEager Loadingで1〜2回に減らせる」と思えばだいたいOK!
📖 おまけ:英語の意味
「N+1 Problem」 = N+1回の問題
💬 1回の一覧取得 + N件分の追加クエリで「N+1」。本来1〜2回で済むはずなのにN+1回走るという意味
← 用語集にもどる