Hello! In our last lesson, we delved into powerful querying techniques like whereHas, withCount, and subqueries. We saw how they help you retrieve complex data efficiently, often as a way to avoid performance bottlenecks.
Today, we're going to tackle the single most common performance issue in Eloquent applications: the N+1 query problem. You've heard me mention it several times, and now it's time for a deep dive. Ignoring this problem can lead to dramatically slow applications, especially as your database grows.
Your goal is to write optimized database queries, and mastering this topic is a non-negotiable step on that path. By the end of this lesson, you will be able to identify the N+1 query problem and resolve it using eager loading (with).
1. What is the N+1 Query Problem?
The N+1 query problem occurs when you fetch a list of parent models and then, within a loop, access a related model for each parent.
Imagine you have Post models, and each Post belongs to a User. If you want to display a list of 50 posts along with their author's name, you might write code like this:
// In a Controller
$posts = Post::all(); // 1 query to get all 50 posts
// In a Blade view
foreach ($posts as $post) {
// This line triggers a new query for *each* post!
echo $post->user->name;
}
This code executes:
- 1 query to retrieve all 50 posts.
- N additional queries (where N is 50) to fetch the user for each post inside the loop.
This results in a total of 1 + 50 = 51 queries. This is the N+1 problem. While it might be unnoticeable with 10 posts, it will cripple your application with 1,000 posts.
The image below shows the dramatic difference in performance. Notice how the unoptimized code results in 101 queries and takes over 5 seconds, while the optimized version takes only 2 queries and a fraction of a second.
This inefficient approach is called lazy loading. Eloquent is "lazy" by default—it only loads relationship data when you explicitly access it. While convenient, it's the primary cause of the N+1 problem in loops.
2. Identifying the Problem with Laravel Debugbar
The first step to fixing the N+1 problem is spotting it. Your best tool for this is Laravel Debugbar, a package that adds a development toolbar to your application, showing database queries, memory usage, and more.
30 Days to Learn Laravel, Ep 13 - Eager Loading and the N+1 Problem
This video from Laracasts is an excellent, practical guide to identifying and fixing the N+1 problem. First, let's see how to install Laravel Debugbar and use it to spot the issue.
Watch from 02:38 to 05:48. Pay close attention to how accessing the relationship inside a loop leads to a large number of queries. The key takeaway is recognizing the pattern of many identical queries that differ only by the ID in the WHERE clause—this is the classic signature of an N+1 problem.
As the video demonstrates, once Debugbar is installed (composer require barryvdh/laravel-debugbar --dev), you can simply load your page and check the "Queries" tab. A long list of repeated SELECT * FROM users WHERE id = ? queries is a clear warning sign.
3. The Solution: Eager Loading with with()
The solution to the N+1 problem is eager loading. Instead of waiting to load relationships until they're accessed, you tell Eloquent to fetch them upfront, right after the initial query for the parent models.
This is done using the with() method.
// The FIX: Use with() to eager load the 'user' relationship
$posts = Post::with('user')->get(); // This will only run 2 queries!
// In a Blade view
foreach ($posts as $post) {
// No new query is run here! The user data is already loaded.
echo $post->user->name;
}
How does this work? Eloquent performs just two queries:
select * from postsselect * from users where id in (1, 2, 5, 12, ...)
It gets all the post IDs from the first query and uses them in a single IN clause to fetch all the necessary users in the second query. It then intelligently stitches this data together in memory, so when you call $post->user, the data is already available.
Eloquent: Relationships - Eager Loading
The official Laravel documentation provides the definitive explanation of this concept. Please read the section on Eager Loading.
Read the section titled "Eager Loading". It explains the problem with a Book and Author example and shows how with('author') reduces 26 queries down to just 2.
Now, let's see the fix applied in the Laracasts video.
30 Days to Learn Laravel, Ep 13 - Eager Loading and the N+1 Problem
Continuing with the Laracasts video, let's see how to apply the with() method to solve the problem we just identified.
Watch from 05:55 to 07:13. Observe how changing Job::all() to Job::with('employer')->get() immediately reduces the query count in the Debugbar from 12 to 3 (one of which is for the session).
You can also eager load multiple relationships at once:$posts = Post::with(['user', 'comments'])->get();
4. Proactively Preventing Lazy Loading
It's better to prevent a problem than to fix it. Laravel provides a powerful mechanism to stop N+1 issues during development. You can instruct Eloquent to throw an exception whenever it detects that a relationship is being lazy-loaded.
This is done by adding a single line to the boot method of your AppServiceProvider.
// app/Providers/AppServiceProvider.php
use Illuminate\Database\Eloquent\Model;
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
}
With this in place, if you forget to eager load a relationship that you access in a loop, Laravel won't silently run N+1 queries. Instead, it will stop execution and show you a LazyLoadingViolationException, telling you exactly which relationship on which model you tried to lazy-load. This forces you to fix the problem immediately.
30 Days to Learn Laravel, Ep 13 - Eager Loading and the N+1 Problem
Let's see this prevention feature in action. It's an invaluable safety net for any Laravel developer.
Watch from 07:52 to 10:15. See how adding Model::preventLazyLoading() to the AppServiceProvider causes an exception, and how fixing the code by adding .with('employer') makes the exception go away.
5. Common N+1 Pitfalls
Even when you know about eager loading, N+1 queries can sneak in. Here are two common mistakes to watch out for.
Mistake 1: Using relationship() Instead of relationship
When you define a relationship, like books() on an Author model, Eloquent gives you two ways to access it:
$author->books: Accesses the data (the collection of loadedBookmodels). This uses eager-loaded data if available.$author->books(): Accesses the query builder instance for the relationship. This always starts a new query.
Consider this code where you want to show an author and their book count:
// Controller - Eager loading looks correct!
$authors = Author::with('books')->get();
// Blade View - THE MISTAKE
@foreach ($authors as $author)
{{ $author->name }} - {{ $author->books()->count() }} books
@endforeach
Even though you eager-loaded books, calling $author->books()->count() ignores that data and runs a new SELECT COUNT(*) query for every single author, reintroducing the N+1 problem.
The Fix: Use the loaded collection property and its count() method: {{ $author->books->count() }}.
Or even better, as we saw in the last lesson, use withCount() if you only need the count: $authors = Author::withCount('books')->get();.
Eloquent Performance: 4 Examples of N+1 Query Problems
This article from Laravel News has an excellent section that clearly illustrates this very common mistake.
Read "Case 2. Two Important Symbols." This section perfectly explains the difference between $author->books() (the method) and $author->books (the data) and how it can lead to an unexpected N+1 problem.
Mistake 2: Relationships Hidden in Packages or Accessors
An N+1 query can also be hidden inside a model accessor or a third-party package. For example, the popular spatie/laravel-medialibrary package provides a handy getFirstMediaUrl() method. If you use this in a loop without eager loading the media relationship, you'll trigger an N+1 query.
The lesson here is to always be vigilant. No matter where the code comes from—your model, a trait, or a package—if it accesses a relationship within a loop, you must ensure that relationship is eager loaded. Always keep an eye on your Debugbar.
Conclusion
You have now tackled one of the most critical performance concepts in Laravel development. Understanding and resolving the N+1 query problem is fundamental to building scalable applications.
Here are the key takeaways:
- The Problem: The N+1 query problem is caused by lazy loading relationships inside a loop, resulting in one query for the parent models plus N queries for their children.
- Identification: Use Laravel Debugbar to monitor your query count. A series of repeated, similar queries is the main symptom.
- The Solution: Use eager loading with the
with('relationship')method to load related models efficiently in just two queries. - Prevention: Enable
Model::preventLazyLoading()in yourAppServiceProviderduring development to catch these issues automatically.
In our previous lesson, we saw how withCount and subqueries could fetch aggregate data. Today, you learned how with() can fetch the full related models. You now have a comprehensive toolkit for handling relationships efficiently.
Next Up:
What if you only want to eager load posts that are published? Or eager load an author's posts, and for each of those posts, also load their comments? The with() method is just the beginning. In our next lesson, we will explore constrained and nested eager loading, giving you even more fine-grained control to optimize complex relationship queries.