【PHP・Laravel】Detected N+1 QueryをPailで可視化してみた

この記事は約6分で読めます。
スポンサーリンク

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:

GitHub - beyondcode/laravel-query-detector: Laravel N+1 Query Detector
Laravel N+1 Query Detector. Contribute to beyondcode/laravel-query-detector development by creating an account on 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);
    }
}
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を立ち上げて開発してみようと思う。

タイトルとURLをコピーしました