Laravel で N+1問題の原因と解決方法 を教えてくれないかな?
今回は、「最初の1回のクエリ + N回の追加クエリ」が実行されてしまう N+1問題 を Laravel での原因と解決方法 について、ご紹介します。
Laravel の 環境構築がまだの場合 は、下記より Laravel の 環境構築 を行なってください。
N+1問題 とは?
N+1問題 とは、「最初の1回のクエリ + N回の追加クエリ」が実行されてしまう問題 のことです。
例えば、投稿(Post)が1000件 あり、各投稿のユーザー(User)を表示する 場合、
まず 投稿(Post)を取得するクエリが1回実行 され、その後、各投稿に紐づくユーザー(User)を取得するクエリが投稿の件数分だけ実行 されます。
つまり、投稿が1000件ある場合、合計で1001回のクエリが発行されてしまいます。
これが N+1問題 です。
N+1問題 のイメージ図
投稿(Post)の件数分(例:1000件)だけユーザー(User)を取得するクエリが実行される ため、
投稿数が増えるほどクエリ数も増えてしまい、アプリケーションのパフォーマンス低下の原因 になります。
posts取得(1クエリ)
SELECT * FROM posts;
│
▼
+-------+-------+-------+-------+
| Post1 | Post2 | Post3 | ... |
+-------+-------+-------+-------+
│ │ │ │
▼ ▼ ▼ ▼
user取得 user取得 user取得 ... N回
結果
1 + N クエリ
N+1問題 の原因
Laravel の ORMであるEloquentのリレーション は、デフォルトで Lazy Loading(遅延ロード)になっています。
ORM(Object-Relational Mapping)とは、データベースのデータをPHPのオブジェクトとして扱う仕組みのこと です。
Laravel では Eloquent を使うことで、SQLを書かなくてもオブジェクトとして直感的にデータベース操作ができます。
Lazy Loading とは、関連データが必要になったタイミングで、その都度データベースから取得する仕組み のため、ループ内でリレーションを呼び出すと、データの件数分だけクエリが実行され、N+1問題が発生 してしまいます。
N+1問題 が発生する コード例 と実行される SQL
N+1問題 が発生する例 として、Postモデル(postsテーブル) と Userモデル(usersテーブル) としているため、モデルやテーブル、ダミーデータなどまだ準備していない場合 は、下記を参考にご準備ください。
コード例
※ SQLクエリログ を確認のため、簡易的に「routes/web.php」に記述しています
// routes/web.php
use App\Models\Post;
// N+1問題の原因と解決方法
Route::get('/n-1', function () {
\DB::enableQueryLog(); // クエリログの有効化
// Postモデル(postsテーブル)とUserモデル(usersテーブル)があり、PostはUserにbelongsTo(投稿は1人のユーザーに属する)
// 全ての投稿を取得
$posts = Post::all();
// 投稿ごとに紐づくユーザーの名前を表示
foreach ($posts as $post) {
echo $post->user->name; // ループ内でリレーションを呼ぶたびにクエリが実行される
}
dd(\DB::getQueryLog()); // クエリの内容を出力
});
実行されるSQL(投稿が1000件の場合)
投稿がN件 あれば、1 + N回 の SQLクエリが発行 されます
※ 投稿が増えるとSQLも増え、アプリケーションのパフォーマンス低下につながる
-- posts取得
select * from posts;
-- 投稿ごとにuser取得(N回繰り返される)
select * from users where id = 1;
select * from users where id = 2;
select * from users where id = 3;
...
select * from users where id = N;
実際のSQLクエリログ (1000 + 1 = 1001件)

N+1問題 の解決方法
コード例
Eager Loading(先読みロード)を使うと、with(‘user’) を付ける だけで、ループ内で追加クエリが発生しなくなります
※ SQLクエリログ を確認のため、簡易的に「routes/web.php」に記述しています
// routes/web.php
use App\Models\Post;
// N+1問題の原因と解決方法
Route::get('/n-1', function () {
\DB::enableQueryLog(); // クエリログの有効化
// Postモデル(postsテーブル)とUserモデル(usersテーブル)があり、PostはUserにbelongsTo(投稿は1人のユーザーに属する)
// 全ての投稿を取得
// $posts = Post::all(); コメントアウト
// with()を使うことで、関連するユーザーをまとめてEager Loading(先読みロード)で取得できる
$posts = Post::with('user')->get();
// 投稿ごとに紐づくユーザーの名前を表示
foreach ($posts as $post) {
echo $post->user->name; // ループ内でリレーションを呼ぶたびにクエリが実行される
}
dd(\DB::getQueryLog()); // クエリの内容を出力
});
実行されるSQL(投稿が1000件の場合)
投稿がN件 でも、SQLクエリはたった2回 だけになる
※ ループ内でユーザーを1件ずつ取得する必要がなく、SQLクエリ数は 1 + N → 2回 に削減されます
-- posts取得
select * from posts;
-- usersをまとめて取得(IN句でN件まとめて取得)
select * from users where id in (1,2,3,...,N);
実際のSQLクエリログ (1 + 1 = 2件)
※ users をまとめて IN句で100件全てまとめて取得

N+1問題解決後のイメージ図
- posts取得時に、usersをまとめて取得
- ループ内で参照しても、SQLクエリ数は変わらない
- 投稿件数がN回に増えても、SQLは2回のまま変わらない
Eager Loading(with使用)
Eager Loading(with使用)
① posts取得(1クエリ)
│
▼
+-------+-------+-------+-------+
| Post1 | Post2 | Post3 | ... |
+-------+-------+-------+-------+
│ │ │
└───── userまとめて取得 ─────┘
(1回)
合計クエリ数: 2回
まとめ
今回は、Laravel でよく発生する N+1問題 の原因と解決方法 について紹介しましたが、いかがでしたでしょうか?
N+1問題 とは、「最初の1回のクエリ + N回の追加クエリ」が実行されてしまう問題 のことで、
ループ内でリレーションを呼ぶたびに追加クエリが実行される ため、データ件数が増えるほどSQLが増え、アプリケーションのパフォーマンス低下につながります。
原因は、Laravel で使用されている Eloquentのリレーションがデフォルトで Lazy Loading(遅延ロード)になっていること です。
Lazy Loading では、関連データが必要になったタイミングで都度データベースにアクセスするため、N+1問題が発生してしまいます。
解決方法は、Eager Loading(先読みロード)を使い、with() を活用する ことで、関連データをまとめて取得でき、SQLクエリ数を大幅に削減すること ができます。
これにより、投稿件数が増えてもクエリ数は変わらず、N+1問題を防ぐことが可能 です。
まずは、この N+1問題 と発生原因、with() を使った解決方法を理解する ことで、Laravel アプリケーションのパフォーマンス改善に役立てる ことができます。
Laravel の知識をさらに深めて、より効率的な開発を目指しましょう。




コメント