DB::enableQueryLog() records each statement with its SQL, bindings and time in milliseconds (including the round trip), and count(DB::getQueryLog()) is the number to shrink. This helper, reused later, times an order-history loop over all 2,000 orders:
use App\Models\Order;
function measure(string $label, callable $work): void
{
DB::flushQueryLog();
DB::enableQueryLog();
$start = hrtime(true);
$work();
$ms = (hrtime(true) - $start) / 1e6;
$log = DB::getQueryLog();
printf("%-11s %5d queries %7.1f ms in MySQL %7.1f ms in all\n", $label, count($log),
array_sum(array_column($log, 'time')), $ms);
}
measure('lazy', function () {
foreach (Order::all() as $order) {
$line = "{$order->customer->name} bought {$order->products->count()} titles";
}
});lazy 4001 queries 2581.3 ms in MySQL 3748.5 ms in all
That is 1 + 2,000 + 2,000 statements, one per order for each relationship. Each takes under a millisecond, so the slow-query log (Slow Query Log) never flags them: the count is the cost.
In a browser, Laravel Debugbar 19,286 (composer require fruitcake/laravel-debugbar --dev) counts for you whenever APP_DEBUG is true outside production and testing. On a Blade page of 50 orders it recorded 51 statements in 28.28 ms and traced all 50 customer queries to line 4 of the view. It leaks request data by design, so keep it off public sites.
| Tool | Package and version | What it shows |
|---|---|---|
| Laravel Debugbar | fruitcake/laravel-debugbar 4.4.3 | Queries per request, timings, source line |
| Laravel Telescope 5,228 | laravel/telescope 5.25.0 | Stored queries per request, slow over 100 ms |
| N+1 Query Detector 2,056 | beyondcode/laravel-query-detector 2.3.0 | Alerts when a relation loads per row |
Repositories: Debugbar (https://github.com/fruitcake/laravel-debugbar 19,286 ) (the old barryvdh/laravel-debugbar name installs the same code), Telescope (https://github.com/laravel/telescope 5,228 ) (Laravel Telescope) and Query Detector (https://github.com/beyondcode/laravel-query-detector 2,056 ). In production, DB::whenQueryingForLongerThan(500, $callback) reports requests whose total query time crosses a threshold, catching N+1 even when each query is fast.