The invoice page is slow and the code looks like one query. It is not. Each line's tax code is loaded as the Blade view loops, and none of those queries appear in review. DB::listen() runs a callback for every query on the connection after you register it, so one request leaves a trail.
Put it at the top of the route or the controller method you are investigating. Take it out before you commit. It is a torch, not a feature.
01 What it does
The callback receives the SQL and the bindings, in order. An N+1 shows up as the same select from invoice_lines repeated once per invoice.
// app/Http/Controllers/InvoiceController.php
use Illuminate\Support\Facades\DB;
DB::listen(fn ($query) => logger($query->sql, $query->bindings));logger() shows the text. A counter shows whether a "fixed" page still fires a query per row.
DB::flushQueryLog();
DB::enableQueryLog();
$invoices = Invoice::query()->with('lines')->latest()->limit(20)->get();
logger(count(DB::getQueryLog()));with('lines') should keep that count at two: one for invoices, one for lines. A higher number means a relation is still lazy.
02 Gotchas
- The listener sees queries after it was registered. A query in a provider that booted earlier is invisible.
DB::listen()is the default connection. A report connection stays silent until you callDB::connection('reports')->listen(...).- Leave it in production and the log volume becomes the outage. It does not belong behind a feature flag you will forget.
enableQueryLog()stores every query in memory for the request. Use it to count, then turn it off. It is not the production metric.
03 Related
Catch N+1 queries before your users do is the guard that throws instead of logging. Eager loading on purpose is the with() this log is checking.