仕組みから学ぶ Web
応用読了目安 13#N+1#ORM#JOIN#性能

N+1 問題 — 1回で済むはずの問い合わせが100回になる

ORM の便利さが隠してしまう問い合わせ回数。発見の仕方と、対処の選択肢を使い分ける。

この記事の進み方

What — なぜ回数が増えるのか

ORM は「関連を辿ると自動で取ってくる」という便利さを提供します。この便利さが、問い合わせ回数を隠します。

Figure一覧表示で何回問い合わせているか
アプリORMDBproducts.all(limit: 100)SELECT * FROM products LIMIT 100for 商品 in 商品一覧 …product.category を参照 → SELECT2件目のカテゴリ → SELECT…… 100件目まで繰り返すようやく描画へ
1/7
products.all(limit: 100)商品を100件取得する。ここまでは1回のクエリ。

コードの見た目は for ループ1つ。しかし1周ごとにネットワークの往復が発生している。ループの中に I/O があることが、コードから見えないのが N+1 の本質。

3つの対処法

CompareN+1 を解消する方法

関連するものを、まとめて1回で取ってくる。ORM の標準的な解決策。

// Rails
Product.includes(:category).limit(100)

// Prisma
prisma.product.findMany({ take: 100, include: { category: true } })

// 発行されるクエリ
SELECT * FROM products LIMIT 100;
SELECT * FROM categories WHERE id IN (3, 5, 8, ...);   -- 1回にまとまる

→ 101回 が 2回 になる
クエリ回数
関連の種類 + 1
転送量
重複がない
実装の手間
1行追加するだけ
向いている場面
ほとんどの場合
  • 1対多の関連でも、親の行が重複せずに済む。
  • まず最初に試すべき手段。これで足りないときに他を検討する。

どれが最適かは、関連の数と取得するデータ量で決まる。JOIN が常に最速ではない。

確認 — ここまで読めたか

N+1 がスロークエリログに出てこないのはなぜですか?

まず選ぶ(解答例は a〜d の記号で説明します)

Why — なぜ気づきにくいのか

N+1 が厄介なのは、あらゆる指標で「正常」に見えることです。

  • スロークエリログに出ない。 1回2msのクエリは、どんな閾値にも引っかからない
  • CPU 使用率は上がらない。 待っているだけなので、負荷としては見えない
  • プロファイラで特定の関数が重く見えない。 時間が広く薄く分散する
  • 開発環境では再現しにくい。 データが10件なら11回のクエリで、体感できない

そして最大の理由は、コードを読んでも分からないことです。

for (const product of products) {
  console.log(product.category.name);   // ← ここでクエリが飛ぶ
}

product.category はプロパティアクセスに見えます。しかし ORM の遅延ロードによって、この行でネットワーク往復が発生している。ループの中に I/O があることが、構文から読み取れません。

往復回数が効く理由

1回のクエリが 2ms でも、実際にかかる時間はこうなります。

アプリ → DB のネットワーク往復:  15ms(同一 AZ でも 0.5〜1ms、別 AZ なら数ms)
DB のクエリ実行:                  2ms
                                 ────
                                 17ms × 200回 = 3.4秒

支配的なのはクエリの実行時間ではなく、往復の回数です。 だから対処法も「クエリを速くする」ではなく「回数を減らす」になります。

これは HTTP/1.1 のリクエスト数問題と同じ構造です。1件あたりのコストが小さくても、回数が積み上がれば支配的になる。

確認 — ここまで読めたか

同じ 201 回のクエリでも、DB が同一マシンにある場合と別リージョンにある場合で何が変わりますか?

まず選ぶ(解答例は a〜d の記号で説明します)

演習 — まず自分で判断する

解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。

演習 — 設計判断を問う

この一覧画面のクエリ回数を、どこまで減らせますか?

与えられた条件
  • ブログの記事一覧ページ。1ページに記事20件を表示する
  • 各記事について、次を表示する
  • ① 著者名(記事 → 著者、多対1)
  • ② タグ名の一覧(記事 → タグ、多対多。1記事あたり平均3件)
  • ③ コメント件数(記事 → コメント、1対多。1記事あたり平均40件)
  • ④ 最新コメント1件の本文(記事 → コメント、1対多)
  • 現在は素朴な実装で、1ページあたり 100 回以上のクエリが発行されている
  • 記事テーブルは 50 万行、コメントテーブルは 2000 万行
この軸で考える
  • · 各関連は多対1か1対多か。JOIN してよいのはどれか
  • · 件数だけが必要なものと、実体が必要なものの違い
  • · 2000万行のコメントテーブルで、20件ぶんの集計はどれくらい重いか
まず選ぶ(解答例は a〜d の記号で説明します)

演習 — 説明できるか

「一覧が遅いがスロークエリは無い」と困っている同僚に、何を見ればよいかを説明してください

与えられた条件
  • 相手はスロークエリログと CPU 使用率を確認済み
  • 相手は ORM を使っている
  • 見つけ方と直し方の両方を渡したい

読み終わりましたか?

読了にすると、これを前提とする記事がロードマップで開放されます。

この記事を前提にしている記事