Eloquent beyond the basics: eager loading done right
If you've ever watched Laravel's debug bar show 500 queries on a single page load, you've met the N+1 problem. Here's how to fix it — and when you should leave it alone.
Eloquent makes relationships feel effortless — which is exactly why the N+1 problem sneaks up on you. You write one line of code, and Eloquent quietly fires a query for every row you're looping over. On a list page with 50 users, that's 51 queries. On a busy dashboard, it's a firehose.
The N+1 problem, explained
Consider this seemingly innocent snippet. For each user in the collection, Eloquent runs a fresh
posts
query the first time you touch it:
// 1 user query + 50 post queries = 51 queries total $users = User::all(); foreach ($users as $user) { echo $user->posts->count(); }
Rule of thumb: if you see the debug bar showing 1 + n queries on a collection loop, that's your signal to eager load.
Fix it with with()
Eager loading tells Eloquent to fetch the related models upfront, in a single query, using a
WHERE IN (...).
Two queries instead of fifty-one:
$users = User::with('posts')->get(); // 2 queries: users + posts WHERE user_id IN (...)
Nested and conditional eager loading
Relationships are chainable, and you can constrain them with closures when you only need a subset:
// nested two levels deep User::with(['posts.author', 'posts.tags'])->get(); // constrained: only published posts User::with([ 'posts' => fn ($q) => $q->where('published', true), ])->get();
When not to eager load
- Tiny result sets. For under ~20 rows the extra query joins aren't worth the complexity.
-
Counts only. Use
withCount('posts')instead of loading whole models. - Already cached. If the relationship is served from cache, eager loading only adds latency.
Pro tips
Use load()
for lazy, on-demand eager loading when the relationship is only needed conditionally. Reach for
withCount()
to avoid hydrating models you never render. And keep the debug bar open — it's the fastest way to
catch an N+1 before it reaches production.
TL;DR
Every loop iteration that queries a relationship is a sign. Reach for
with(),
constrain with closures, count with withCount(),
and keep an eye on your query count.
That's it — two queries instead of fifty-one. Your debug bar (and your database) will thank you.
Fazale Rabbi
Laravel Full-Stack Developer writing about what I build and debug.
Let's work together →