Skip to content
← Back to Blog

How to Fix the N+1 Query Problem in Laravel (Eager Loading Explained)

Learn what the N+1 query problem is in Laravel, how to detect it with Telescope and query logs, and how to fix it with eager loading using with(), load(), and withCount().

Edited 2 hours ago
How to Fix the N+1 Query Problem in Laravel (Eager Loading Explained)

How to Fix the N+1 Query Problem in Laravel (Eager Loading Explained)

If your Laravel application feels slow and you cannot figure out why, the N+1 query problem is one of the most common culprits. It silently multiplies your database queries, turning a page that should run 2 queries into one that runs 101. In this guide, you will learn exactly what the N+1 problem is, how to spot it, and how to fix it with eager loading.

What Is the N+1 Query Problem?

The N+1 query problem happens when your code runs one query to fetch a list of records, then runs one additional query for each record to fetch a related record. If you have N records, you end up running N+1 queries total.

Here is the classic example. Imagine you want to display a list of users along with their posts.

This looks innocent, but here is what actually hits the database:

1. One query to fetch all users: select * from users

2. One query per user to fetch their posts: select * from posts where user_id = ?

If you have 100 users, Laravel runs 101 queries. That is the N+1 problem — 1 query plus N additional queries.

Why the N+1 Problem Is So Dangerous

The N+1 problem is dangerous because it works perfectly in development and only hurts you in production. With 5 test users, you will never notice 6 queries. With 10,000 real users, your page runs 10,001 queries and times out.

Three reasons it catches developers off guard:

  • Eloquent makes relationships feel free. Accessing $user->posts looks like reading a property, not running a database query. There is no visual signal that a query just happened.
  • It scales with your data. The more successful your application becomes, the worse the problem gets. Growth literally makes it slower.
  • It hides inside loops. The extra queries are scattered across loop iterations, so they do not show up as one obvious slow query in your logs.How to Detect the N+1 Problem

Before fixing anything, confirm you actually have the problem. Laravel gives you several ways to see your queries.

Method 1: Laravel Telescope

Laravel Telescope is the fastest way to spot N+1 queries. Install it in your development environment:

BASH
composer require laravel/telescope --dev

php artisan telescope:install

php artisan migrate

Open Telescope in your browser and click the Queries tab. If you see dozens of nearly identical queries like select * from posts where user_id = ? with different IDs, you have an N+1 problem.

Method 2: Query Log

You can log all queries for a single request without installing anything:

PHP
DB::enableQueryLog();$users = User::all();foreach ($users as $user) {echo $user->posts->count();}dd(DB::getQueryLog());

Count the queries in the output. If the number grows with the number of users, you have confirmed the problem.

Method 3: Laravel Debugbar

The Laravel Debugbar package shows a query counter right in your browser toolbar:

BASH
composer require barryvdh/laravel-debugbar --dev

Load the slow page and check the query count. Anything above 20-30 queries for a simple listing page deserves investigation.The Fix: Eager Loading with with()

The solution is eager loading — telling Laravel to fetch the related records up front in a single query, instead of lazily loading them one by one inside the loop.

Add the with() method to your query:

PHP
$users = User::with('posts')->get();
foreach ($users as $user) {
echo $user->name;
echo $user->posts->count();
}

Now Laravel runs exactly 2 queries:

  1. select * from users
  2. select * from posts where user_id in (1, 2, 3, ...)

The second query uses a WHERE IN clause to fetch all related posts at once. Eloquent then matches them to the correct users in PHP memory. No matter how many users you have, it stays at 2 queries.

Eager Loading Nested Relationships

Real applications have deeper relationship chains. Suppose each post has comments, and you want to display users, their posts, and each post's comments.

Without eager loading, this is an N+1 inside an N+1 — potentially thousands of queries. Fix it with nested eager loading using dot notation:

PHP
$users = User::with('posts.comments')->get();
foreach ($users as $user) {
foreach ($user->posts as $post) {
echo $post->comments->count();
}
}

This runs just 3 queries: one for users, one for posts, one for comments. The dot notation tells Laravel to eager load the entire chain.

Constraining Eager Loads

Sometimes you do not want every related record — only a filtered subset. You can pass a closure to with() to constrain the eager load:

PHP
$users = User::with(['posts' => function ($query) {
$query->where('published', true)->latest()->limit(5);
}])->get();

This fetches only the 5 latest published posts per user. The constraint applies at the database level, so you are not pulling unnecessary data into memory.

You can also select specific columns to reduce memory usage:

PHP
$users = User::with('posts:id,user_id,title')->get();

Important: when selecting specific columns, always include the foreign key (user_id in this case). Without it, Eloquent cannot match the posts back to their users, and the relationship will come back empty.Eager Loading After the Fact with load()

Sometimes you already have a model instance and realize you need its relationships. Use load() to eager load on an existing model or collection:

PHP
$user = User::find(1);
// Later, when you need the posts:
$user->load('posts');

For collections, load() works the same way:

PHP
$users = User::all();
$users->load('posts');

This is useful when the decision to load a relationship happens conditionally, after the initial query has already run.

Counting Related Models Without Loading Them

A common variation: you only need the count of related records, not the records themselves. Loading full models just to count them wastes memory. Use withCount() instead:

PHP
$users = User::withCount('posts')->get();
foreach ($users as $user) {
echo $user->posts_count;
}

This adds a posts_count attribute to each user with a single efficient COUNT query. No post models are loaded into memory at all.

You can combine withCount() with constraints:

PHP
$users = User::withCount(['posts' => function ($query) {
$query->where('published', true);
}])->get();

Common Pitfalls to Avoid

Even developers who know about eager loading make these mistakes:

  • Eager loading in the wrong place. Adding with() to a query that is never used with the relationship wastes a query. Only eager load relationships you actually access.
  • Forgetting nested relationships. User::with('posts') does not eager load posts.comments. If your view accesses comments, add the nested load explicitly.
  • Using with() on already-loaded models. with() only works on the query builder. If you already called get(), use load() instead.
  • Selecting columns without the foreign key. As mentioned above, User::with('posts:title') without user_id silently returns empty relationships. Always include the key Eloquent needs to match records.
  • Over-eager loading. Loading 10 relationships "just in case" can be slower than a few lazy queries. Profile with Telescope and eager load only what the page actually renders.How to Prevent N+1 Queries in the Future
  • Fixing N+1 queries one by one is reactive. Here is how to catch them before they reach production:
  • Add query count assertions to your feature tests. Laravel lets you assert the number of queries a request runs. If a code change introduces an N+1, the test fails immediately.
  • Review Telescope during development. Make it a habit to glance at the query tab when building listing pages. Spikes in query count are easy to spot early.
  • Use the preventLazyLoading() method in development. Laravel can throw an exception whenever code triggers a lazy load, forcing you to add explicit eager loads:
PHP
// In AppServiceProvider::boot(), development only:
Model::preventLazyLoading(! app()->isProduction());

With this enabled, any accidental lazy load crashes loudly in development instead of silently slowing down production.

Conclusion

The N+1 query problem is one of the highest-impact performance fixes you can make in a Laravel application. A single with() call can take a page from 1,001 queries down to 2. The key takeaways:

  • Accessing relationships inside loops without eager loading causes the N+1 problem
  • Eager loading with with() fetches related records in bulk using WHERE IN
  • Use dot notation for nested relationships (posts.comments)
  • Use withCount() when you only need counts, not full models
  • Detect problems early with Telescope, Debugbar, or preventLazyLoading()

If your Laravel application is slower than it should be, check your query count first. The fix is often a single method call away.

More Articles

Laravel How to Hire a Laravel Developer Without Hiring the Wrong One Laravel How to Build an AI Agent in Laravel 13 with Laravel AI SDK Python Python Web Scraping with BeautifulSoup: A Complete Beginner’s Guide