応用読了目安 13 分#N+1#ORM#JOIN#性能
N+1 問題 — 1回で済むはずの問い合わせが100回になる
ORM の便利さが隠してしまう問い合わせ回数。発見の仕方と、対処の選択肢を使い分ける。
先に読んでおくとよい記事
この記事の進み方
What — なぜ回数が増えるのか
ORM は「関連を辿ると自動で取ってくる」という便利さを提供します。この便利さが、問い合わせ回数を隠します。
1/7
products.all(limit: 100) — 商品を100件取得する。ここまでは1回のクエリ。
コードの見た目は for ループ1つ。しかし1周ごとにネットワークの往復が発生している。ループの中に I/O があることが、コードから見えないのが N+1 の本質。
3つの対処法
関連するものを、まとめて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 がスロークエリログに出てこないのはなぜですか?
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 が同一マシンにある場合と別リージョンにある場合で何が変わりますか?
演習 — まず自分で判断する
解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。
演習 — 設計判断を問う
この一覧画面のクエリ回数を、どこまで減らせますか?
与えられた条件
- ブログの記事一覧ページ。1ページに記事20件を表示する
- 各記事について、次を表示する
- ① 著者名(記事 → 著者、多対1)
- ② タグ名の一覧(記事 → タグ、多対多。1記事あたり平均3件)
- ③ コメント件数(記事 → コメント、1対多。1記事あたり平均40件)
- ④ 最新コメント1件の本文(記事 → コメント、1対多)
- 現在は素朴な実装で、1ページあたり 100 回以上のクエリが発行されている
- 記事テーブルは 50 万行、コメントテーブルは 2000 万行
この軸で考える
- · 各関連は多対1か1対多か。JOIN してよいのはどれか
- · 件数だけが必要なものと、実体が必要なものの違い
- · 2000万行のコメントテーブルで、20件ぶんの集計はどれくらい重いか
演習 — 説明できるか
「一覧が遅いがスロークエリは無い」と困っている同僚に、何を見ればよいかを説明してください
与えられた条件
- 相手はスロークエリログと CPU 使用率を確認済み
- 相手は ORM を使っている
- 見つけ方と直し方の両方を渡したい
読み終わりましたか?
読了にすると、これを前提とする記事がロードマップで開放されます。