The N+1 Query Problem is the most common and damaging performance flaw in applications that use an Object-Relational Mapper (ORM) like Laravel’s Eloquent. It can turn a simple API call into a slow, resource-hogging operation that hits your database dozens or even hundreds of times.
The goal of this post is to show you exactly how the N+1 problem occurs and provide practical, code-driven solutions using Eager Loading for common Laravel scenarios.
What is the N+1 Problem?
The N+1 problem occurs when an application executes:
- 1 query to retrieve a list of parent records (the “N” part).
- N queries (one separate query for each parent) to retrieve the related child records.
If you have 50 posts, you execute 1 + 50 = 51 total database queries. If you have 500 posts, you execute 501 queries. This overhead quickly overwhelms the database server.
The Problematic Code Structure
Assume we have two models: Post (belongs to) and User (has many posts).
| Model | Relationship |
Post | public function user() { return \$this->belongsTo(User::class); } |
❌ Wrong: The N+1 Query Trap
// 1. Fetch 50 posts (1 Query)
$posts = Post::limit(50)->get();
// 2. Loop through each post (50 Queries: one for each user)
foreach ($posts as $post) {
// A query is executed here: SELECT * FROM users WHERE id = ?
echo "Post: {$post->title}, Author: {$post->user->name}\n";
}PHPResult: 51 queries executed, 50x slow.
Solution 1: Eager Loading with with()
Eager Loading tells Eloquent to “pre-load” all the necessary relationship data in advance using a minimal number of queries. For a one-to-many relationship, Eloquent loads all 50 posts, extracts all 50 unique user_ids, and then runs a single WHERE IN query to fetch all required users.
The Optimized Code
// 1. Fetch 50 posts AND all related users (2 Queries total)
$posts = Post::with('user')->limit(50)->get();
// 2. Loop through each post (0 extra Queries)
foreach ($posts as $post) {
// The user model is already loaded and "attached" to the post object
echo "Post: {$post->title}, Author: {$post->user->name}\n";
}PHPResult: 2 queries executed, lightning-fast.
Mastering Practical Scenarios
Eager Loading goes beyond simple one-to-one relationships. Here are the essential techniques for complex, real-world data structures.
Scenario A: Loading Nested Relationships
When you need to access a relationship’s relationship (e.g., a Post‘s User‘s Profile).
| Model Structure | Relationships |
Post | ->User |
User | ->Profile |
❌ Wrong (Deeply Nested N+1)
// Fetch all posts (1 Query)
$posts = Post::all();
foreach ($posts as $post) {
// This executes a query for the User (N queries)
// AND another query for the Profile (M queries, where M <= N)
echo "Author Bio: {$post->user->profile->bio}\n";
}PHP✅ Correct (Dot Notation Eager Loading)
Use dot notation (.) inside the with() method to load nested relationships in just three queries.
// 3 Queries total: Posts, Users, and Profiles
$posts = Post::with('user.profile')->get();
foreach ($posts as $post) {
// All data is pre-loaded
echo "Author Bio: {$post->user->profile->bio}\n";
}PHPScenario B: Loading Multiple Relationships
When a single parent model has several relationships needed on the view.
| Model Structure | Relationships |
Post | -> User |
Post | -> Tags |
Post | -> Comments |
❌ Wrong (Manual Eager Loading)
// This is not standard Eager Loading; it's just slow code.
$posts = Post::all();
// This will cause three N+1 problems inside the loop!
foreach ($posts as $post) {
echo "Author: {$post->user->name}\n"; // N+1
echo "Tags: {$post->tags->count()}\n"; // N+1
echo "Comments: {$post->comments->count()}\n"; // N+1
}PHP✅ Correct (Array Eager Loading)
Pass an array to with() to load multiple relationships in parallel.
// 4 Queries total: Posts, Users, Tags, and Comments
$posts = Post::with(['user', 'tags', 'comments'])->get();
foreach ($posts as $post) {
// All relationships are loaded
echo "Author: {$post->user->name}\n";
echo "Tags: {$post->tags->count()}\n";
echo "Comments: {$post->comments->count()}\n";
}PHPScenario C: Filtering the Eager Loaded Data (Constrained Eager Loading)
Often, you don’t need all of a relationship’s data—only a subset (e.g., only published comments).
❌ Wrong (Filtering after loading)
// 2 Queries: Posts and ALL Comments (slow and wasteful)
$posts = Post::with('comments')->get();
foreach ($posts as $post) {
// Filtering happens in PHP memory AFTER the database loaded all comments
$published_comments = $post->comments->where('is_published', true);
echo "Published Comment Count: {$published_comments->count()}\n";
}PHP✅ Correct (Constrained Eager Loading)
Use a closure in the with() method to modify the relationship query.
// 2 Queries: Posts and ONLY published comments (optimized)
$posts = Post::with(['comments' => function ($query) {
// This constraint is applied to the single WHERE IN query
$query->where('is_published', true)->orderBy('created_at', 'desc');
}])->get();
foreach ($posts as $post) {
// Only published comments are available
echo "Published Comment Count: {$post->comments->count()}\n";
}PHPScenario D: Counting Related Records Efficiently
When you only need the count of related items, but not the items themselves (e.g., displaying the number of upvotes).
❌ Wrong (Loading and then Counting)
// N+1 problem just to get a count!
$posts = Post::all();
foreach ($posts as $post) {
// Loads all votes into memory, then counts them (N queries)
echo "Vote Count: {$post->votes->count()}\n";
}PHP✅ Correct (withCount)
Use withCount() to add a [relationship]_count attribute to the parent model. This uses a single, optimized aggregate query.
// 2 Queries: Posts and an aggregate count query for votes
$posts = Post::withCount('votes')->get();
foreach ($posts as $post) {
// Access the pre-calculated count attribute
echo "Vote Count: {$post->votes_count}\n"; // FAST and memory-efficient
}PHPPro Tips for Eager Loading
Always Check the Debugbar: If you use Laravel Debugbar, always check the “Queries” tab. If you see dozens or hundreds of duplicate queries, you have an N+1 problem.
Use Global Eager Loading: For relationships that are always needed (like a Post‘s User), define them in the parent model’s $with property.
class Post extends Model
{
// These relationships will always be eager loaded automatically
protected $with = ['user', 'category'];
// ...
}PHPThe Load() Method: Use the load() method to eagerly load relationships on an existing collection of models (e.g., after an update or when relationships are only needed conditionally).
// Fetch posts without relationships
$posts = Post::where('status', 'draft')->get();
// Now load the users for the existing collection
$posts->load('user');PHPMastering Eager Loading is the single biggest performance gain you can achieve in any Eloquent-based Laravel application. By switching from O(N) query complexity to O(1) query complexity, you ensure your application remains fast and scalable as your user base and data grow.
Thanks for reading. Happy Coding!