Laravel Eloquent Eager Loading: Nested, Clean
If you're building applications with Laravel, you've probably run into the N+1 query problem. Fortunately, Laravel Eloquent makes it super easy to solve this using eager loading.
But did you know you can also eager load nested relationships using clean array syntax?
Let’s make it super simple. 👇
✅ The Problem
When you fetch a model and its relationships without eager loading, Laravel hits the database again and again for every related model. This slows down your app.
✅ The Clean Solution
Use the with() method and pass an array that includes any nested relationships. Here's a clean and readable way to load nested data:
Book::with([
'author' => [
'contacts',
'publisher',
],
])->get();
This code will:
- Get all books
- Load each book’s
author - For each
author, also load theircontactsandpublisher
All in one clean query set 💡
📌 Why This Matters
- Faster performance with fewer queries
- Cleaner code that's easy to read and maintain
- Perfect for API responses or when dealing with complex relationships
💬 Final Tip
Whenever you're loading nested data, always prefer this clean array syntax. It keeps your codebase elegant—and your app blazing fast. 🔥
Why Eager Loading Is the Whole Ballgame
Every lazy-loaded relationship inside a loop is a separate database query, and templates hide loops everywhere — a page listing fifty posts with authors and comment counts can quietly run a hundred and fifty queries. Eager loading collapses that to three. The clean array syntax matters because deep relations are where the damage concentrates: posts.comments.author loaded up front is one predictable query per table, while the same chain loaded lazily scales with row count. If you internalize one rule from this post, make it this: relationship access in a loop means with() in the query, no exceptions.
Seeing the Problem With Your Own Eyes
Install Laravel Debugbar in development and open any page you consider "done" — the query count at the bottom is the honest reviewer most projects never hire. Anything above a couple dozen queries on a typical page almost always traces to a missing eager load. Fix it at the controller, refresh, and watch a 120-query page become an 8-query page; the before-and-after is the fastest way to teach the habit to a team, because nobody argues with a number that just dropped by an order of magnitude.
Constraining What You Load
Eager loading everything is the opposite mistake. Use constrained loads — with(['comments' => fn($q) => $q->latest()->limit(5)]) — so listing pages fetch only what they render, and use withCount('comments') when the view needs a number rather than the records themselves. Select only needed columns in the constraint on wide tables, and remember the foreign key must be in any select list or the relation silently comes back empty. Loaded-but-unused data is bandwidth and memory you pay for on every request; the goal is matching the query to the view, not maximizing the with() array.
When to Reach for Lazy Eager Loading
Sometimes you don't know upfront whether a relation is needed — a conditional view, an API with optional includes. That's what load() on an existing collection solves: fetch the parents, and if the branch that needs children runs, load them all in one shot with the same array syntax. Combine it with loadMissing() in reusable code paths to avoid double-loading, and your query counts stay flat whichever branch executes. It's the same principle as everywhere else in Eloquent performance work: decide in one place, load in one query, and keep the loop innocent.
A Team Convention Worth Writing Down
Codify the habit in your project's contributing notes: controllers own eager loading, views never trigger queries. One sentence of convention, enforced in review, keeps query counts flat as the team grows — and makes Debugbar's number a shared scoreboard instead of one developer's obsession. When a pull request adds a relationship access, the reviewer's first question is now automatic: where's the with()? That's the whole system, and it scales better than any tooling.
Measuring the Win in Production
The payoff shows up in two graphs: database CPU and page response time. After sweeping a codebase for missing eager loads, it's common to watch database load drop by a third with zero infrastructure changes — queries that ran thousands of times per minute simply stop existing. Take a screenshot of your monitoring before the sweep and after; that image justifies the next performance ticket better than any argument. And when a new feature ships, check the graphs the same day: a sudden stair-step in query volume is the earliest, cheapest moment to catch the eager load someone forgot.
Where This Fits in the Bigger Performance Story
Eager loading is the first chapter of Laravel performance, not the whole book — but it's first for a reason. Query count is the metric most correlated with slow pages in typical applications, and it's the one you can fix without touching infrastructure, caching layers, or architecture. Master this, add the slow-query listener from my logging guide, and index your foreign keys: those three habits carry most Laravel apps comfortably to their first serious scale. The exotic optimizations can wait until the boring ones are done — and in my experience auditing client codebases, the boring ones almost never are.
The Habit That Prevents N+1 Forever
Laravel Eloquent eager loading is a reflex worth building: any time a view loops over a relationship, the query behind it should have a with(). My the N+1 case study and logging slow queries go deeper.
If you want your Eloquent layer audited, that's work I take on through Ramlit.