Lazy loading protection does not catch all N+1 queries

I discovered this one the hard way

Joel Clermont
Joel Clermont
2023-11-30

I'm a huge fan of the new "strict mode" for Laravel models. One of the things it helps catch is the dreaded "N+1" query, which can cause performance issues in production.

It's nice to have a test fail locally to alert you to one of these issues before it makes its way to production. But recently, I ran into an issue where Sentry (our error / performance monitoring tool) reported an N+1 that Eloquent strict mode did not catch.

The actual code in our app is a little bit too complicated to show in a short example, but consider this code snippet:

// a purposely bad code example - DO NOT COPY
$users = User::all();
foreach ($users as $user) {
    $user->load('payments');
}

This horrendous code example exists only to demonstrate the issue. Notice that we are definitely causing an N+1 query here, even though we're using Laravel's eager loading feature.

If we have 100 users in our database, this code will result in 101 queries. If we simply move the load() call outside the loop, we can reduce this to 2 queries.

What I found surprising is that Laravel will not report this, even with Eloquent strictness turned on. The reason why is that Laravel is only tracking calls via relationship methods, not analyzing the actual queries being generated and looking for duplication.

So this code will fail Eloquent strictness:

// another purposely bad code example - DO NOT COPY
$users = User::all();
foreach ($users as $user) {
    $userPaymentCount = $user->payments->count();
}

Because we're using the payments relationship, Laravel will report this as an N+1 query. Thankfully, Sentry is analyzing the actual queries being generated, and it alerted us to the issue. This nuance is worth being aware of, especially if you don't have a tool like Sentry looking over your shoulder.

Here to help,

Joel

P.S. Do you work alone and want someone to pair with on your Laravel app?

Toss a coin in the jar if you found this helpful.
Want a tip like this in your inbox every weekday? Sign up below 👇🏼

Level up your Laravel skills!

Each 2-minute email has real-world advice you can use.