🏠 Home 👤 About ⚡ Skills 💼 Portfolio 📦 Packages 📝 Blog ✉ Contact ✉ Contact Now
All Articles

The N+1 Query Problem: Finding It Before Your Users Do

A dashboard that loaded in 80ms with ten records took 14 seconds with two thousand. The cause is almost always the same, and Laravel gives you a one-line way to catch it in development.

Junaid Ali 2 min read 0 views

A client messaged me: the admin dashboard had become unusable. It had been fine at launch. Nothing had been deployed. The only thing that changed was that they now had real data.

What an N+1 Actually Is

You run one query to fetch a list. Then, for each item in that list, an accessor or a Blade template triggers another query. Ten orders becomes eleven queries. Two thousand orders becomes two thousand and one.

// One query for orders...
$orders = Order::latest()->get();

// ...then one MORE query per order, inside the loop
@foreach ($orders as $order)
    {{ $order->customer->name }}
@endforeach

The Fix Is One Word

$orders = Order::with('customer')->latest()->get();

Two queries total, regardless of how many orders exist. That is the entire fix, and it took under a minute once the cause was identified.

Catching It Automatically

Laravel can simply refuse to lazy-load. Put this in AppServiceProvider::boot():

Model::preventLazyLoading(! app()->isProduction());

Now any un-eager-loaded relationship throws an exception in local and staging — impossible to miss, impossible to ship. This single line has caught more performance bugs for me than any profiler.

Nested and Counted Relations

Eager loading nests, and counting does not need the rows at all:

Order::with(['customer.company', 'items.product'])
    ->withCount('items')
    ->get();

Using withCount() instead of $order->items->count() avoids loading every row just to count them — a huge saving on large relations.

Test With Realistic Data

The root cause of every N+1 that reaches production is a development database with twelve rows in it. Seed thousands. Better yet, write a test that asserts a query count:

DB::enableQueryLog();
$this->get('/admin/orders')->assertOk();
$this->assertLessThan(15, count(DB::getQueryLog()));

Performance regressions then fail CI instead of reaching a client.

Tagged Laravel Performance Eloquent Database
Share this article

Want this built properly?

I take on a small number of projects at a time so each one gets real attention.

WhatsApp Teams LinkedIn Facebook GitHub