How to find and fix N+1 queries in Laravel
An N+1 query is the most common reason a Laravel page gets slower as your data grows. One query loads a list, then one more query runs for every row in it. Here is what it looks like, three ways to catch it before your users do, and how to fix it.
What an N+1 query looks like
Eloquent loads relations lazily. The first time your code reads $order->customer, Eloquent
runs a query for that one customer. Inside a loop, that is one query for the list and one more for
every row:
PHP and Blade// OrderController@index
$orders = Order::latest()->take(50)->get(); // 1 query
// orders/index.blade.php
@foreach ($orders as $order)
{{ $order->customer->name }} {{-- 1 more query per order: 50 more --}}
@endforeach
Each of those queries is fast on its own, so nothing looks wrong in development with ten rows. With five hundred orders, the same page runs five hundred and one queries, and every one of them is a round trip to the database.
Three ways to catch an N+1
1. Make lazy loading throw in development
Laravel can refuse to lazy-load relations. Add this to the boot method of
AppServiceProvider, and a lazy load outside production throws a
LazyLoadingViolationException that names the model and the relation:
app/Providers/AppServiceProvider.phpuse Illuminate\Database\Eloquent\Model;
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
}
The request stops right at the lazy load, so you know exactly which relation to eager-load. Because
of the isProduction() check, it never breaks a live page.
2. Log every query
DB::listen calls you back for each query the app runs. Log them, then look in
storage/logs/laravel.log for the same SQL repeating with different bindings:
In a service provider's boot methoduse Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
DB::listen(function ($query) {
Log::debug($query->sql, ['bindings' => $query->bindings, 'ms' => $query->time]);
});
It works anywhere, jobs and commands included. The catch is reading it: fifty nearly identical lines are easy to miss between everything else in the log.
3. Watch the request in a debugger
Laravel Debugbar and Telescope list every query a request ran. Gaze goes one step further: it groups each request with its queries, and when the same query shape runs again and again with different values, it folds the runs into one N+1 row, lists every execution with its time, and points at the line that ran them.
How to fix it: eager loading
Tell Eloquent which relations the page will need, and it loads them for the whole list in one extra query, however many rows there are:
$orders = Order::with('customer')->latest()->take(50)->get(); // 2 queries, for any number of orders
with('customer.address')loads nested relations in the same call.$orders->load('items')eager-loads after the fact, for example on a collection you were handed;loadMissing()skips relations that are already loaded.withCount('items')adds anitems_countcolumn when you only need the number, instead of loading every item to count it.- In an API resource,
$this->whenLoaded('customer')includes the relation only if it was eager-loaded, so the resource never triggers a lazy load of its own.
Then reload the page and check that the query count dropped. In Gaze, the N+1 row disappears and the request header shows the new total.
Where N+1 queries hide
- Blade views and components that read a relation inside
@foreach. - API resources and accessors that touch a relation.
- Policies and gates that load related models, called once per row.
- Queued jobs and commands that loop over models.
- Counting with
$post->comments->count(), which loads every comment, instead ofwithCount('comments').
An N+1 is many fast queries. One query that is slow on its own is a different problem:
how to find slow and duplicate queries in Laravel. And if
you have been chasing this with dd(), see
how to debug Laravel without dd().
Questions
What is an N+1 query in Laravel?
It is one query that loads a list of models, followed by one more query for each model when your code reads a relation that was not loaded. Fifty orders that each read their customer run fifty-one queries instead of two.
How do I detect N+1 queries in Laravel?
Call Model::preventLazyLoading() in development so a lazy load throws, log queries with DB::listen, or use a debugger that groups queries by request. Gaze folds repeated queries into one N+1 row and points at the line that ran them.
Does eager loading always make a page faster?
It replaces many small queries with one query per relation, which is almost always faster. Load only the relations the page uses, and use withCount when you only need a number.
Is it safe to turn on preventLazyLoading?
The usual setup is Model::preventLazyLoading(! $this->app->isProduction()), so lazy loading throws in development and tests but never breaks a live page.
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: Eager loading · Laravel: Preventing lazy loading · Laravel: Counting related models · Laravel: Listening for query events
See every N+1 the moment it runs
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