N+1 Problem Visualized: Fix It With with()
Learn how Laravel's N+1 query problem happens, visualize the extra queries, and fix it with Eloquent eager loading using with().
The N+1 Problem Is Usually Hiding in a Loop
Your Laravel page loads 100 records. The query looks harmless. The response is slow anyway.
You open the database profiler and discover something ugly: 101 SQL queries for one page.
That is the classic Laravel N+1 problem. And in many Eloquent cases, the fix is remarkably small:
with()
Laravel's Eloquent relationships are lazy loaded by default. When you access a relationship that has not already been loaded, Eloquent queries the database at that point. Put that access inside a loop and you can accidentally turn one database query into dozens or hundreds. Laravel's documentation specifically recommends eager loading to avoid this N+1 pattern.
First, Visualize What N+1 Actually Means
Imagine a simple blog application with these relationships:
- A
Postbelongs to anAuthor. - An author can have many posts.
The Eloquent relationship might look like this:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Post extends Model
{
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id');
}
}
Now suppose you want to display a list of posts and the author's name.
The innocent-looking code
$posts = Post::latest()->get();
foreach ($posts as $post) {
echo $post->author->name;
}
At first glance, there is only one get(). So it is tempting to assume that this means one database query.
It does not.
Where the Extra Queries Come From
The first query retrieves the posts:
SELECT *
FROM posts
ORDER BY created_at DESC;
Then this line runs inside the loop:
$post->author->name
Because author has not been loaded yet, Eloquent lazily loads it.
For the first post:
SELECT *
FROM users
WHERE id = 17
LIMIT 1;
For the second:
SELECT *
FROM users
WHERE id = 42
LIMIT 1;
Then another query for the third post. And another for the fourth.
If the page contains 100 posts, the rough query count becomes:
1 query for posts
+
100 queries for authors
=======================
101 queries
That is 1 + N — one query for the original collection and one query for each related model accessed afterward.
The Fix: One with()
Instead of waiting for Eloquent to load each author separately, tell it that you already know the relationship will be needed:
$posts = Post::with('author')
->latest()
->get();
Your loop can stay exactly the same:
foreach ($posts as $post) {
echo $post->author->name;
}
The difference is how Eloquent gets the data.
Instead of running a separate author query for every post, Eloquent eager loads the relationship using a query against the relevant author IDs. For a basic relationship like this, the operation becomes approximately two queries rather than 101.
SELECT *
FROM posts
ORDER BY created_at DESC;
SELECT *
FROM users
WHERE id IN (17, 42, 8, 31, ...);
That is the key idea behind Eloquent eager loading: fetch the related data in batches instead of repeatedly asking the database for one record at a time.
Why with() Works
with() does not magically make the database faster. It changes when and how Eloquent retrieves relationship data.
Without eager loading:
Posts
↓
Loop
↓
Author query
↓
Author query
↓
Author query
↓
Author query
↓
...
With eager loading:
Posts
↓
Load all required authors
↓
Loop
↓
Use already-loaded authors
The relationship is already in memory when your loop accesses it.
Laravel Eloquent Eager Loading With Multiple Relationships
Real applications rarely need just one relationship. You might have a post, its author, comments, and the users who wrote those comments.
You can eager load multiple relationships in one query definition:
$posts = Post::with([
'author',
'comments',
])->latest()->get();
You can also load nested relationships using dot notation:
$posts = Post::with([
'author',
'comments.author',
])->latest()->get();
Laravel supports nested eager loading as well as loading multiple relationships through with().
Don't Eager Load Everything
Once developers discover with(), there is an easy trap:
Post::with([
'author',
'comments',
'comments.author',
'categories',
'tags',
'attachments',
'likes',
])->get();
That may remove one N+1 problem while creating another performance problem: loading data the request never uses.
Eager loading is not a command to "load everything." It is a way to tell Eloquent which relationships you know the current operation needs.
A better rule is:
Eager load relationships because you need them, not because they exist.
Load Only the Columns You Need
You can also constrain an eager-loaded relationship when you do not need every column.
$posts = Post::with([
'author:id,name',
])->latest()->get();
There is one important detail here: when selecting specific columns for a relationship, include the key required to match the relationship correctly. In a belongsTo relationship, that generally means including the related model's primary key.
For larger responses, limiting selected columns can reduce the amount of data transferred and hydrated into Eloquent models.
When the Relationship Is Optional, Use load()
Sometimes you do not know whether a relationship is needed until after retrieving the parent records.
$posts = Post::latest()->get();
if ($includeAuthors) {
$posts->load('author');
}
Eloquent supports lazy eager loading through load(). This lets you eager load a relationship after the models have already been retrieved.
If you only want to load the relationship when it has not already been loaded, loadMissing() is useful:
$posts->loadMissing('author');
This distinction is useful in service classes and reusable application code where different callers may load different relationships.
Prevent N+1 Bugs Before They Reach Production
Fixing an N+1 query after a production performance incident is not ideal. Laravel can help you catch accidental lazy loading during development.
You can configure Eloquent to prevent lazy loading outside production:
<?php
namespace App\Providers;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Model::preventLazyLoading(
! $this->app->isProduction()
);
}
}
With this enabled, accidentally accessing an unloaded relationship can surface during development instead of quietly generating extra queries.
This is particularly useful for teams because an N+1 problem can easily be introduced during an otherwise harmless-looking Blade, API Resource, or serialization change. Laravel provides preventLazyLoading() specifically for this kind of strictness check.
A Real-World Example: API Resources
N+1 problems are not limited to Blade templates.
They frequently appear when an API resource accesses relationships while transforming a collection.
public function toArray($request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'author' => $this->author->name,
];
}
If your controller retrieves 100 posts without loading author, serializing the collection can trigger the same N+1 behavior.
A cleaner approach is to make the controller or query layer explicitly decide which relationships are required:
$posts = Post::with('author')
->latest()
->paginate(20);
Then the resource can safely expose the already-loaded relationship. Laravel also provides whenLoaded() for conditionally including relationships in API resources without triggering an unnecessary lazy load.
How to Find an N+1 Problem
Don't guess. Count the queries.
If a page suddenly becomes slow after adding a relationship to a loop, inspect the SQL generated by that request.
The pattern is usually obvious:
select * from posts ...
select * from users where id = 1
select * from users where id = 2
select * from users where id = 3
select * from users where id = 4
select * from users where id = 5
...
One repeated query with only the ID changing is a strong N+1 signal.
Laravel's own tooling can make these problems easier to investigate. Laravel Nightwatch, for example, can show the database queries associated with an HTTP request and has been used by the Laravel team to identify N+1 queries in real applications.
Three Production Gotchas
1. N+1 can hide behind a tiny piece of code
This line looks harmless:
{{ $post->author->name }}
But if it sits inside a loop, it can become a database query per row. The expensive part is not always in the controller. It may be buried in a Blade component, API Resource, accessor, serializer, or presenter.
2. Eager loading can increase memory usage
with() reduces repetitive database queries, but the related models still have to be loaded into memory.
Loading thousands of parents and several large relationships in one request can create a different bottleneck.
For large datasets, consider pagination, chunking, or lazy processing instead of loading the entire dataset at once.
3. Avoid replacing N+1 with an oversized query graph
There is a difference between:
Post::with('author')->get();
and:
Post::with([
'author',
'comments.author',
'comments.attachments',
'tags',
'categories',
'reactions.user',
'media',
])->get();
The second query may be technically free of the original N+1 problem while still doing far more work than the endpoint requires.
Optimize for the data the request actually needs.
The One-Word Fix Is Really a Query Design Habit
with() is easy to remember, but the important lesson is not the method itself.
The real habit is to look at the code that consumes your Eloquent models and ask:
"Which relationships will this request access?"
If you already know the answer, load those relationships as part of the query.
$posts = Post::with('author')
->latest()
->get();
That one line can turn a request that executes 101 similar queries into one that performs roughly two relationship-aware queries.
And once your application grows, that difference stops being theoretical. It shows up in database CPU, request latency, connection usage, and eventually your infrastructure bill.
Final Takeaway
The Laravel N+1 problem is rarely caused by complicated SQL. More often, it comes from a perfectly ordinary Eloquent relationship being accessed at the wrong time.
If a relationship is going to be used repeatedly, eager load it.
with()
Then verify the query count instead of assuming the optimization worked.
Good Eloquent code is not just about writing fewer lines. It is about making the database work predictable.
Comments
No comments yet. Be the first to share your thoughts.