Laravelで開発していると、「N+1問題」という言葉はよく聞く。
知識としては理解しているつもりでも、
「開発中にどうやって早く気づくか」
「あとから問題にならないように、どう予防するか」
という部分は、正直あまり整理できていなかった。
そこで今回は、
N+1問題を意図的に検知できる状態を作るために、
Laravel向けのライブラリ
「Detected N+1 Query」 を予防目的で導入してみた。
ちょうどそのタイミングで、YouTubeで Laravel Pail を紹介している動画を見かけ、
「これ、N+1検知と組み合わせたら便利そうだな」と思い、
一緒に試してみることにした。
実際に触ってみると、この2つの組み合わせが思った以上に相性が良かったので、
今回はその検証メモを記事として残しておく。
N+1クエリって、結局なにが困るのか
N+1問題は説明不要だと思うので、ここでは簡単に触れる。
親データを1回、子データをN回取得する構造になると、
意図せずSQLが大量に発行され、
データ量や同時アクセスが増えたときに、レスポンス遅延のボトルネックになりやすい。
実務では、気をつけていても後から混入しがちなのが、厄介なところだ。
Detected N+1 Query をどう検知するのか
今回使ったのは、N+1クエリを検出してくれるライブラリ「laravel-query-detector」。
GitHub:
Laravelで動かしていると、
Detected N+1 Query
という警告ログが出るようになる。
ログ自体はちゃんと出ているのだけど、
ログファイルを開いて、「Detected N+1 Query」を探し出す作業が、正直ちょっと面倒だった。
そこで、ログをリアルタイムに可視化できる Pail を組み合わせてみた。
Pailでログをリアルタイムに見る
Pailを起動しておくと、Laravelのログがリアルタイムで流れてくる。
APIを叩いた瞬間に、
- 実行されたSQL
- 警告ログ
- Detected N+1 Query のメッセージ
が、そのままターミナルに表示される。
これが想像以上に分かりやすく、ログを探す手間がほとんどなくなった。
「このAPI叩いた瞬間に、これだけクエリ飛んでるのか」
「今の処理でN+1出てるな」
というのが、その場ですぐ把握できる。
Postman中心でAPIを叩いて検証している自分の開発スタイルとも相性が良く、
APIを叩いた直後にログが流れてくるのが、かなり気持ちいい。
実際にN+1を発生させてみた
今回は、N+1が実際にどのように発生し、
Pail上でどんなログとして見えるのかを確認するために、
あえてN+1が起きるコードを書いて検証してみた。
モデルの前提
今回のサンプルでは、以下のようなモデル構成を想定する。
Shop(店舗)
└ hasMany → Menu(メニュー)
└ belongsTo → Category(カテゴリ)
例えば、
- 1つのショップには複数のメニューがある
- 各メニューは1つのカテゴリに属する
- ドリンク
- フード
- デザート など
という、よくある飲食店データの構成をイメージしている。
Shop モデル
class Shop extends Model
{
public function menus()
{
return $this->hasMany(Menu::class);
}
}
Menu モデル
class Menu extends Model
{
public function shop()
{
return $this->belongsTo(Shop::class);
}
public function category()
{
return $this->belongsTo(Category::class);
}
}
Category モデル
class Category extends Model
{
public function menus()
{
return $this->hasMany(Menu::class);
}
}
実際にN+1が発生するコード
use App\Models\Shop;
Route::get('/shops', function () {
$shops = Shop::all();
foreach ($shops as $shop) {
foreach ($shop->menus as $menu) {
$categoryName = $menu->category->name;
logger()->info($categoryName);
}
}
return response()->json([
'count' => $shops->count(),
]);
});
Pailに出力された内容
Pailを起動した状態でAPIを叩くと、ターミナルにリアルタイムでログが流れる。
- 同じようなSELECTクエリが何度も表示される
- Detected N+1 Query の警告が出る
- API実行タイミングとログ出力が一致して見える
(ここにPailログのスクリーンショットを掲載)
Postman中心のAPI開発でも、かなり使えそう
自分の開発スタイルは、
- PostmanでAPIを叩く
- レスポンスと挙動を確認する
- ログを見て問題がないか確認する
という流れが多い。
Pailを起動しておくだけで、
- APIを叩いた瞬間のログが即見える
- N+1の兆候にすぐ気づける
- SQLの発行回数も感覚的に分かる
ので、「デバッグ体験」がかなり快適になった。
正直、今までは
パフォーマンス問題って、起きてから対処するもの
という意識がどこかにあったけど、
Pailがあると「早めに気づける」感覚が強くなる。
実務でどう活かせそうか
実務目線で考えると、こんな使い方が現実的かなと思った。
- 新しいAPIを作ったときに、Pailを起動して一通り叩いてみる
- クエリが異常に多くないか、N+1が出ていないかをチェック
- レビュー前のセルフチェックとして使う
パフォーマンスチューニング専用ツール、というよりは、
「変な実装に早めに気づくための保険」
として置いておくイメージが近い。
常時使う必要はないけど、
「怪しいな」と思ったタイミングでサッと起動できるのは強い。
触ってみた正直な感想
今回触ってみて感じたのは、
- N+1の“見える化”は、思っていた以上に効果がある
- ログをリアルタイムで見る体験は、想像以上に楽しい
- Postman中心のAPI開発とも相性が良い
という点。
Laravelは便利な分、内部で何が起きているかが見えづらくなりがちだけど、
Pailを使うと「中身が透けて見える」感じがあって、ちょっと面白い。
パフォーマンス改善というより、
**「自分のコードの癖を知るツール」**として使うのもアリだな、と思った。
しばらくは、検証環境では常にPailを立ち上げて開発してみようと思う。


