こう 聞かれます
- 「N+1 問題とは
何で、 どう 防ぎますか」 - 「一覧の
画面が、 件数が 増える ほど 遅くなります。 何を 疑いますか」 - 「ORM を
使っていると、 なぜ N+1 に 気づきにくいのですか」
面接官が 見ている点
- 何が N で
何が 1 か、を 具体的な例で 説明できるか - JOIN、
IN、 ORM の 先読み (eager loading)、 DataLoader のような、 まとめて 取る 手を 挙げられるか - ORM は
コードからは 問い合わせの 数が 見えに くい、と 分かっていて、 記録で 確かめる 習慣があるか
短い答え
- N+1 問題は、
一覧を 1 回の 問い 合わせで 取った あと、 その 件数 N の 分だけ 1 件ずつ関連の データを 取りに いき、 問い 合わせが N+1 回に なる 問題です。 - JOIN や IN で
まとめて 取る、 ORM の 先読み (eager loading) を 使う、 GraphQL なら DataLoader のように 同じ 種類の 取り出しを まとめる、 で防ぎます。 - ORM は
コードの 見た 目から 問い 合わせの 数が 分かりにくいので、 開発中に 発行された SQL を 記録に 出し、 件数を 変えても 回数が 増えないかを 確かめます。
少し 詳しい答え
何が 起きているか
「記事の
SQL クエリ
-- 1 回目 記事を 100 件取る
SELECT id, title, author_id FROM posts LIMIT 100;
-- 2 回目から 記事ごとに書き手を取る (これが 100 回)
SELECT name FROM authors WHERE id = 1;
SELECT name FROM authors WHERE id = 2;
-- 以下続く1 件ずつは
まとめて取る
JOIN で 1 回に
SQL クエリ
-- JOIN で 1 回
SELECT p.id, p.title, a.name
FROM posts p
JOIN authors a ON a.id = p.author_id
LIMIT 100;
-- IN で 2 回 (記事を取ったあと、書き手をまとめて取る)
SELECT id, name FROM authors WHERE id IN (1, 2, 3);ORM (表のinclude や JOIN FETCH のような
GraphQL では、
まとめて
トレードオフ
まとめて
- JOIN 1 対多の
関連を JOIN すると、 親の 行が 子の 数だけ重なって 返り、 受け取る 量が 増えます。 子が 多い ときは IN で 2 回に 分ける ほうが 軽いこともあります - 先読みを
何でも付ける 使わない 関連まで 毎回取ると、 かえって 重くなります。 その 画面で 使う 関連だけ先読みします - IN に入れる数何万もの
値を IN に 並べると、 DB に よっては 遅くなったり上限に 当たったりします。 数が 多い ときは 分けて取ります
話すときは、
よく ある間違い
- 少ない
データで 試して 「速い」と言う N+1 は 件数が 増えてから 効いてきます。 件数を 増やして 回数を見ます - ORM の
関連を ループの中で触る 見た目は 普通の 取り出しでも、 裏で 1 件ずつ問い 合わせています - 何でも JOIN でまとめる 1 対多では
行が 重なって 量が 増えます。 IN で 分ける ほうが 軽いこともあります
深掘りの問い
- 「N+1 を
見つけるには、 どうしますか」 開発中に ORM が 出す SQL を 記録に 出す、 1 回の 画面の 表示で 何回問い 合わせたかを 数える、 遅い 問い 合わせの 記録を 見る、と答えます - 「API の
作りで N+1 に なる ことは ありますか」 画面が 一覧の API を 呼んだ あと、 1 件ずつ詳しい API を 呼ぶのも 同じ形なので、 一覧の 応答に 要る 項目を 入れる、 まとめて 取る API を 用意する、と答えます - 「キャッシュで
解決して よいですか」 回数は 減りますが、 古い データが 出る 問題が 増えます。 まず取り方を 直し、 それでも 重い所に キャッシュを 足す、と答えます