All posts

Avoid N+1 Queries with Eager Loading

The N+1 problem is invisible with ten rows and painful with ten thousand. Load relationships up front and let Laravel catch lazy loading for you.

Marijan Kopčić••3 min read
Avoid N+1 Queries with Eager Loading

The N+1 query problem is the most common performance bug in Laravel applications, and it is almost always invisible in development. With ten rows in your local database, nobody notices eleven queries. With ten thousand rows in production, everybody notices ten thousand and one.

How it happens

@foreach (User::all() as $user)
    {{ $user->profile->name }}
@endforeach

One query loads the users. Then every iteration touches $user->profile, and Eloquent lazily runs a new query for that user's profile. A hundred users means 101 queries. The template also runs a query itself, which hides data access in a file nobody reviews for performance.

Load what you need, up front

Fetch the data before it reaches the view, and tell Eloquent which relationships you will use:

public function index(): View
{
    return view('users.index', [
        'users' => User::with('profile')->get(),
    ]);
}
@foreach ($users as $user)
    {{ $user->profile->name }}
@endforeach

Now there are exactly two queries, no matter how many users there are: one for the users and one for all of their profiles. The view only renders data, it never asks the database for anything.

The same idea covers nested relationships and counts:

Post::with(['author', 'comments.author'])->withCount('likes')->get();

Why this pays off

Predictable query counts. The number of queries depends on the code, not on the size of the data.

Faster pages under real load. Each query has network and parsing overhead. Removing hundreds of them per request is often the biggest single speed-up a page can get.

Views stay dumb. When all data is loaded before rendering, the template cannot surprise you with hidden database access.

Make Laravel catch it for you

You do not have to find N+1 problems by reading code. Laravel can refuse lazy loading outright in development:

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

Any accidental lazy load now throws an exception locally, long before it reaches production.

Where to be careful

Eager loading is not free either. Loading relationships you never display wastes memory and queries. Load what the page uses, nothing more. And for counts, use withCount() instead of loading a whole collection only to call count() on it.

A useful rule of thumb:

  • Relationship used inside a loop → with() it before the loop.
  • Only need how many → withCount(), not the full relationship.
  • Model already loaded → load() the missing relationship once.

The practical test

Open a list page with the debug bar or query log enabled and count the queries. If the number grows when you add rows to the table, you have an N+1. Find the relationship being touched in the loop and add it to with().

Keep reading