← Back to all articles laravel

Fixing N+1 Queries in Laravel Eloquent

A page loads fine in development with twelve records and crawls in production with twelve hundred. Nothing changed in the code. What changed is how many times one query inside a loop ran.

What an N+1 actually is

You fetch a list, then touch a relation on each item:

$projects = Project::all();          // 1 query

foreach ($projects as $project) {
    echo $project->client->name;      // 1 query — each time round
}

Twelve projects is 13 queries. Twelve hundred is 1,201. The code reads perfectly well, which is why this survives review.

Seeing it

Count queries rather than guessing. Log them in a local service provider:

// AppServiceProvider::boot(), local only
if (app()->environment('local')) {
    DB::listen(fn ($q) => logger($q->sql));
}

Load the page and count the lines. If one statement repeats with only the id changing, that is your N+1. Laravel Debugbar and Telescope show the same thing with less setup.

Make Laravel refuse to do it

Better than finding them by hand — have the framework throw when a relation loads lazily:

// AppServiceProvider::boot()
Model::preventLazyLoading(! app()->isProduction());

Now an N+1 fails loudly in development instead of quietly in production. Turn it on in an existing project and expect to find several immediately.

The fix

$projects = Project::with('client')->get();   // 2 queries, whatever the count

Laravel fetches every client in one whereIn and matches them up. Two queries for twelve rows, and two for twelve hundred.

Nested and multiple relations

Project::with(['client', 'tasks.assignee'])->get();

Load only the columns you use

Project::with('client:id,name')->get();

Include the foreign key — leave id out and the relation cannot be matched, and the relation comes back null with no error.

Counting without loading

To show how many tasks a project has, do not load the tasks:

$projects = Project::withCount('tasks')->get();
// $project->tasks_count

One aggregate query instead of pulling every row into memory to call count() on it.

When eager loading is the wrong fix

Eager loading trades queries for memory. Loading a relation for 50,000 rows to display ten of them just moves the problem. In that case paginate first, or select the few columns you need with a join.

Chunk long-running jobs rather than eager loading everything:

Project::with('client')->chunk(200, function ($projects) {
    // 200 at a time, memory stays flat
});

Keeping them out

  • Turn on preventLazyLoading outside production
  • Check the query count when a page feels slow, before optimising anything else
  • Eager load in the controller, not the view — a view that triggers queries hides them
  • Use withCount for totals

Most "Laravel is slow" reports are one of these. Fix the query count first; if it is still slow after that, then look at indexes.

Found this useful?

I write these from real client projects. Have one that needs building?

Start a project

Keep reading