Eager Loading vs Lazy Loading
In this article, we'll explain the difference between lazy loading and eager loading, how the N+1 problem happens, and how to fix it using Eloquent's with() method.
Introduction
In the previous article, we learned how to define relationships between Eloquent models and access related data using clean, intuitive syntax like $post->user or $post->tags. What we didn't cover is a subtle but important detail: every time you access a relationship this way, Eloquent runs a separate database query behind the scenes to fetch that related data.
This usually isn't a problem when working with a single record. But when working with a list of records — say, displaying 50 blog posts along with each post's author — this behavior can quietly generate dozens of extra, unnecessary database queries, silently slowing your application down. This well-known issue even has a name: the N+1 query problem.
In this article, we'll explain the difference between lazy loading and eager loading, how the N+1 problem happens, and how to fix it using Eloquent's with() method.
Lazy Loading (The Default Behavior)
By default, Eloquent relationships are lazy loaded — meaning the related data isn't actually queried from the database until you explicitly access it.
php
$post = Post::find(1); // One query: fetch the post
echo $post->user->name; // Another query: fetch the related user, only nowThis is convenient and often perfectly fine for single records. The problem arises when this pattern repeats inside a loop.
The N+1 Query Problem
Consider this seemingly innocent code, displaying a list of posts along with each author's name:
php
$posts = Post::all(); // 1 query: fetch all posts
foreach ($posts as $post) {
echo $post->user->name; // 1 additional query PER post
}If there are 50 posts, this code runs 1 query to get the posts, plus 50 additional queries — one for each post's author — for a total of 51 queries. This is the "N+1" problem: 1 initial query, plus N additional queries (one per record in a loop).
For a small number of records, this might not be noticeable. But as your data grows — hundreds or thousands of posts — this pattern can severely degrade your application's performance, since each additional query adds real overhead.
Eager Loading: The Fix
Eager loading solves this by fetching all the related data upfront, in a small, fixed number of additional queries — regardless of how many records you're working with. You do this using the with() method.
php
$posts = Post::with('user')->get(); // 2 queries total: one for posts, one for all related users
foreach ($posts as $post) {
echo $post->user->name; // No additional queries — data is already loaded
}Instead of 51 queries, this approach runs just 2, no matter how many posts exist — a dramatic improvement.
Eager Loading Multiple Relationships
You can eager load several relationships at once by passing an array:
php
$posts = Post::with(['user', 'tags', 'comments'])->get();Eager Loading Nested Relationships
If you need to load a relationship's relationship (like a post's comments, and each comment's author), use dot notation:
php
$posts = Post::with('comments.user')->get();This fetches posts, their comments, and each comment's associated user, all in a controlled, efficient number of queries.
Lazy Eager Loading
Sometimes you've already retrieved a collection of models without eager loading, and realize afterward that you need related data. Rather than re-querying from scratch, you can use load() to eager load relationships on an existing collection:
php
$posts = Post::all();
// Later in your code, you realize you need the related users:
$posts->load('user');This still avoids the N+1 problem, even though the initial query didn't originally include eager loading.
Detecting the N+1 Problem in Your Own Code
The N+1 problem can be easy to miss, especially in views where relationship access is buried inside a Blade @foreach loop. A few practical ways to catch it:
Laravel Debugbar (a popular package) displays the exact number of queries run per page, making N+1 issues immediately visible during development.
Laravel Telescope provides similar query monitoring, useful for both local development and staging environments.
Manually counting queries using
DB::enableQueryLog()andDB::getQueryLog()during debugging, if you don't have a dedicated tool installed.
php
DB::enableQueryLog();
$posts = Post::all();
foreach ($posts as $post) {
$post->user;
}
dd(DB::getQueryLog());Tips for Managing Eager and Lazy Loading
Always eager load relationships you know you'll access in a loop, especially in views displaying a list of records with related data.
Use
with()in your controller, not inside the Blade view, so the query strategy stays close to where the data is actually retrieved.Watch for relationship access inside
@foreachloops in Blade files — this is the most common place N+1 problems hide, since it's easy to forget a query is being triggered.Use a query monitoring tool during development, like Laravel Debugbar, to catch N+1 issues before they reach production.
Don't eager load relationships you don't actually need — loading unnecessary data adds its own overhead, so be intentional about what you include in
with().
FAQ About Eager and Lazy Loading
1. Is lazy loading always bad? No. Lazy loading is perfectly fine when working with a single record, or when you're not certain you'll need a relationship every time. It only becomes a problem inside loops, where it multiplies into many extra queries.
2. Does eager loading always mean better performance? Not necessarily. Eager loading unnecessary relationships (data you don't actually use) adds overhead of its own. The goal is to eager load exactly what you need, not everything available.
3. How can I tell if my application has an N+1 problem? The most reliable way is using a query monitoring tool like Laravel Debugbar or Telescope, which show you the exact number of queries run on each page load, making unusually high query counts easy to spot.
4. Can I eager load a relationship conditionally? Yes, using with() combined with a closure lets you eager load a relationship while also applying additional constraints, such as only loading published comments: Post::with(['comments' => fn ($query) => $query->where('is_approved', true)])->get();
5. What's the difference between with() and load()? with() is used when building a new query, eager loading relationships as part of the initial retrieval. load() is used on a collection or model you already have, eager loading relationships afterward, without re-running the original query.
6. Does eager loading work with nested relationships across many levels? Yes, using dot notation like 'comments.user.profile', you can eager load relationships several levels deep, though it's worth being mindful of how much data this pulls in at once for very deep chains.
Conclusion
Understanding the difference between eager and lazy loading — and recognizing the N+1 query problem — is one of the most important performance lessons in working with Eloquent relationships. A small change, adding with() to a single query, can turn dozens of unnecessary database calls into just a couple, with no other changes required.
In the next article, we'll explore accessors, mutators, and attribute casting, showing you how to customize how model attributes are formatted and stored, directly within your Eloquent models.
Found this helpful? Share it!