N+1問題とは?原因と解決方法をわかりやすく解説
はじめに
データベースを使った開発でよく聞く「N+1問題」。パフォーマンスを大きく悪化させる原因の一つです。
この記事では、N+1問題とは何か、なぜ発生するのか、そしてどう解決するのかを具体例を交えて解説します。
N+1問題とは
N+1問題とは、最初のクエリで一覧を取得し、その結果のIDをもとに関連データを1件ずつ取りに行ってしまう問題です。
N+1問題の流れ
- まずユーザー一覧を取得する(1回)
- 取得した各ユーザーのIDを使って、そのユーザーの関連情報(投稿など)を取得する(N回)
例えばユーザー10人の投稿一覧を取得する場合を考えます。
1回目: SELECT * FROM users;
→ user_id: 1, 2, 3, ... 10 が返ってくる
2回目: SELECT * FROM posts WHERE user_id = 1; ← id=1の関連情報
3回目: SELECT * FROM posts WHERE user_id = 2; ← id=2の関連情報
4回目: SELECT * FROM posts WHERE user_id = 3; ← id=3の関連情報
...
11回目: SELECT * FROM posts WHERE user_id = 10; ← id=10の関連情報
最初に一覧を取得するクエリが1回、そこで取得したIDを使って関連データを取得するクエリがN回、合計N+1回のクエリが発生します。
ユーザーが100人なら101回、1000人なら1001回です。データが増えるほどクエリ数が増え、パフォーマンスがどんどん悪化していきます。
なぜ発生するのか
N+1問題は、最初のクエリで取得したIDをループで回して、1件ずつ関連データを取りに行ってしまうことで発生します。
コードで見るとわかりやすいです。
// 1回目のクエリ: ユーザー一覧を取得
const users = await db.query('SELECT * FROM users');
// 取得したIDをもとに、1件ずつ関連データを取りに行く(N回)
for (const user of users) {
const posts = await db.query(
'SELECT * FROM posts WHERE user_id = ?', [user.id]
);
user.posts = posts;
}
一見普通のコードに見えますが、ユーザーの数だけSELECT文が実行されてしまいます。これがN+1問題の典型的なパターンです。
解決方法
考え方はどの方法も共通しています。IDをもとに1件ずつ取りに行くのをやめて、まとめて取得するということです。
方法:JOINで一発で取得する
最もシンプルな方法です。テーブルを結合して1回のクエリで全データを取得します。
-- N+1回のクエリが1回になる
SELECT users.name, posts.title
FROM users
JOIN posts ON users.id = posts.user_id;
ユーザーが何人いても、クエリは常に1回です。
なぜJOINは速いのか
「JOINも内部ではデータを1件ずつ照合しているのでは?」と思うかもしれません。たしかにデータを照合している点は同じですが、違うのはどこで処理するかです。
N+1問題の場合、アプリケーション(Node.jsなど)とデータベースの間で何度も通信が発生します。
アプリ → DB: ユーザー一覧ください
DB → アプリ: はいどうぞ
アプリ → DB: user_id=1の投稿ください
DB → アプリ: はいどうぞ
アプリ → DB: user_id=2の投稿ください
DB → アプリ: はいどうぞ
...(10回繰り返し)
1回の通信には接続の確立、クエリの解析、結果の送信といったオーバーヘッドがあります。これが10回、100回と繰り返されるとその分だけ遅くなります。
JOINの場合は通信が1回だけです。
アプリ → DB: ユーザーと投稿をJOINして一括でください
DB → アプリ: はいどうぞ(まとめて返す)
データベース内部ではたしかに照合処理をしていますが、それはデータベースがインデックスなどの最適化されたアルゴリズムを使って高速に処理します。
つまり、N+1問題の本質は通信の往復回数が無駄に多いことです。JOINはその往復を1回にまとめるから速いということです。
まとめ
N+1問題は、最初のクエリで取得したIDをもとに、関連データを1件ずつ取りに行ってしまうことで発生する問題です。データが増えるほどパフォーマンスが悪化するため、早い段階で気づいて対処することが重要です。
解決方法はJOINで一発取得するのが基本です(他にもIN句やORMのEager Loadingなどがありますが割愛します)。
開発中にクエリのログを確認し、同じようなSELECT文が大量に発行されていたら、N+1問題を疑ってみてください。