Home › Gaze › Find slow and duplicate queries in Laravel

How to find slow and duplicate queries in Laravel

When a Laravel page is slow because of the database, it is usually one of three things: a query that is slow on its own, the same query running more than once, or an N+1. Here is how to find the first two with Laravel's own tools, and how to see all three for every request at once.

Start with how to find and fix N+1 queries in Laravel, the most common reason a Laravel page slows down as your data grows.

Log every query that takes too long

DB::listen hands you each query with its bindings and its time in milliseconds. Log the ones above a threshold:

In a service provider's boot methoduse Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

DB::listen(function (QueryExecuted $query) {
    if ($query->time > 100) {   // milliseconds
        Log::warning('Slow query', ['sql' => $query->sql, 'bindings' => $query->bindings, 'ms' => $query->time]);
    }
});

Some requests are slow without a single slow query: they just run a lot of them. Laravel can call you when one request spends too long in the database in total:

use Illuminate\Database\Connection;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\DB;

DB::whenQueryingForLongerThan(500, function (Connection $connection, QueryExecuted $event) {
    // This request has spent more than 500 ms in the database in total.
});

Find out why a query is slow

Ask the database for its plan. The query builder runs EXPLAIN for you:

Order::where('status', 'shipped')->orderBy('created_at')->explain()->dd();

Look for a full scan of a large table, and for columns you filter, join or sort by that have no index. Often the fix is a single index in a migration:

Schema::table('orders', function (Blueprint $table) {
    $table->index(['status', 'created_at']);
});

Catch duplicate queries

The same query, with the same bindings, running twice in one request usually means a value is loaded in two places: a helper, a view composer, a policy, a middleware. Load it once and pass it down, or let once() remember it for the rest of the request:

public function settings(): array
{
    return once(fn () => Setting::pluck('value', 'key')->all());
}

Many runs of one query with different bindings is a different pattern, the N+1: how to find and fix N+1 queries in Laravel.

See all of it, per request

Gaze groups every query a request runs under that request, with the time each one took and a summary on top. Queries above a threshold are marked slow (500 ms by default, changed with GAZE_SLOW_QUERY_MS), repeats are counted as duplicates, and repeated query shapes fold into an N+1 row. The timeline draws each query from when it started, so you see where the time went at a glance.

The Gaze timeline for GET /orders/42: every query, cache call and view drawn as a bar from when it started, with a 128 ms view and the twelve runs of an N+1 query standing out
The timeline of one request in Gaze: the slow view and the N+1 stand out at once.

->gaze() on a query builder shows the SQL with its bindings before it runs, and the query inspector copies runnable SQL for EXPLAIN. If you would rather not see every query, GAZE_SLOW_QUERIES and GAZE_DUPLICATE_QUERIES send only those. Still reaching for dd() to look at results? See how to debug Laravel without dd().

Questions

How do I log slow queries in Laravel?

Listen with DB::listen in a service provider and log any query whose time is above your threshold; $query->time is in milliseconds. For the total time a request spends in the database, use DB::whenQueryingForLongerThan.

What counts as a slow query?

It depends on the page, but a single query above 100 ms on a page that should answer in under a second is worth a look. Gaze marks queries above 500 ms by default, and GAZE_SLOW_QUERY_MS changes the threshold.

How do I see the SQL with bindings in Laravel?

Call toRawSql() on a query builder to get the SQL with its bindings filled in, or ddRawSql() to dump it and stop. In Gaze, ->gaze() on a query builder shows the same before the query runs.

When can I use Gaze?

Gaze is free and coming soon for Mac, Windows and Linux. Get launch news from the Gaze page and we will email you the day it is out.

Sources: Laravel: Listening for query events · Laravel: Monitoring cumulative query time · Laravel: Debugging queries · Laravel: once()

Every query, under the request that ran it

Gaze is a desktop debugger for PHP, Laravel and JavaScript. Every dump, query, log and exception lands in one window, grouped by the request that caused it, with N+1 and slow queries flagged for you. It is free and coming soon for Mac, Windows and Linux. Get launch news and we will email you the day it is out.

Get launch news See Gaze, the free desktop debugger for Laravel