đ How One Laravel Mistake Can Trigger 300 Queries (and How to Fix It)
The Hidden Laravel Performance Bug: Understanding the N+1 Problem
If you work with databases and applications, you may have heard of the N+1 problem. Itâs a common performance issue that happens when an application makes too many database queries instead of fetching data efficiently. This can slow down your app and make it harder to scale.
đWhat Is the N+1 Problem?
Imagine you have a list of blog posts, and each post has multiple comments. This is a classic one-to-many relationship because one post can have many comments.
Now, letâs say you need to fetch all posts along with their comments.
If your application isnât optimized, it might do something like this:
1. First, fetch all posts:
$posts = Post::all();2. Then, for each post, fetch its comments one by one:
foreach ($posts as $post) {
// Query for each post: SELECT * FROM comments WHERE post_id = ?
$comments = Comment::where(âpost_idâ, $post->id)->get();
}If you have 100 posts, this results in 101 queries
(1 query for posts + 100 queries for comments).
Thatâs why itâs called the N+1 problem â
you run N queries (one for each post) + 1 main query.
â ď¸Why Is This a Problem?
The N+1 problem can seriously slow down your application for several reasons:
Too many database queries overload your database server.
Fetching each postâs comments separately forces the database to run the same query again and again.Increased network traffic between your Laravel app and the database.
Instead of sending 2 queries (posts + all comments), your app may send 101 queries or more.Slower response times because the application waits for multiple comment queries to finish.
Each post triggers its own query, adding delay for every request.Performance gets worse as the dataset grows.
For example, if you have 1,000 posts, Laravel may execute 1,001 queries
(1 for posts + 1 for each postâs comments).
In short, the more posts you have, the slower your page becomes.
đ§How to Fix the N+1 Problem
1. Use Joins to Fetch Data in One Query
Instead of making a separate query for each postâs comments, you can fetch everything in a single database query using a JOIN.
SELECT posts.*, comments.*
FROM posts
LEFT JOIN comments ON posts.id = comments.post_id;This way, you avoid running 101 queries (1 for posts + 100 for comments).
Everything is retrieved in one efficient request.
Laravel Query Builder Equivalent:
$posts = DB::table(âpostsâ)
->leftJoin(âcommentsâ, âposts.idâ, â=â, âcomments.post_idâ)
->select(âposts.*â, âcomments.*â)
->get();2. Batch Queries Instead of Fetching One by One
If you prefer not to use a JOIN, another solution is batch queryingâfetching all comments for multiple posts in one call.
SQL Example:
SELECT * FROM comments WHERE post_id IN (1, 2, 3, 4, 5);This allows you to:
First fetch all posts
Then fetch all comments for those posts in a single batch query
Instead of 100 separate queries, you now perform:
1 query â posts
1 query â all their comments
Total = 2 queries instead of 101.
Laravel Eloquent Equivalent:
$posts = Post::all(); // Query 1
$comments = Comment::whereIn(âpost_idâ, $posts->pluck(âidâ))->get(); // Query 2â
The Laravel fix: Eager loading (with())
Laravelâs built-in solution is to eager-load relations before looping:
// Controller (fixed)
$posts = Post::with(âcommentsâ)->get(); // Query #1 (posts) + Query #2 (comments for all posts)
// Now in the view, no extra queries:
@foreach($posts as $post)
@foreach($post->comments as $comment)
{{ $comment->body }}
@endforeach
@endforeach
Result: 2 queries total no matter how many posts.
đ Detect N+1 in Laravel
Laravel Debugbar (dev): shows each query and count.
Laravel Telescope: records queries for requests.
DB::listen(): log queries in tests or a temporary debug route.
Add assertions in tests to monitor query count (use sparingly).
Quick example using DB::listen:
DB::listen(function ($query) {
logger($query->sql, $query->bindings);
});đ Handling large datasets
Eager-loading everything for massive datasets can blow memory. Use chunking or cursors:
Post::chunkById(100, function ($posts) {
$posts->load(âcommentsâ); // eager-load per chunk
foreach ($posts as $post) {
// process
}
});cursor() streams models but you can only eager-load per batch (collect ids, then load in batches).
đ Common gotchas & pitfalls
If you select() custom columns on a relation, include the foreign key so Eloquent can map relations:
$posts = Post::with([âcommentsâ => function ($q) {
$q->select(âidâ, âpost_idâ, âbodyâ, âcreated_atâ);
}])->select(âidâ, âtitleâ)->get();limit()inside eager load is not the same as âlimit per parentâ in every DB. UselatestOfMany()or subquery patterns for latest-N-per-parent.Polymorphic relations require careful eager-loading; Laravel groups by type and runs efficient queries, but always test.
Over-eager-loading deep relations (
with(âa.b.c.dâ)) can increase memory and do too much work â load only whatâs required.
â
Best practices checklist (senior devs use this)
Before merging a PR, ask:
Am I accessing a relation inside a loop? If yes, ensure that relation is eager-loaded.
Do I only need aggregates (count, sum)? Use
withCount(),withSum().Am I selecting only the columns needed for the view/API? (Use
select()).Is this endpoint high-traffic? Consider caching or denormalized read models.
For large datasets, did I use chunking or streaming?
Did I profile queries with Debugbar or Telescope?
Are there joins/subqueries that could replace heavy eager-loading?
đ Final thoughts
The N+1 problem is simple to understand and easy to fix once you know where to look. In Laravel, with(), withCount(), constrained eager loading, and smart use of joins/subqueries give you a powerful toolbox to fetch data efficiently.
If you:
identify relations used in views,
eager-load whatâs required,
prefer aggregates when possible,
and profile query counts regularly,
youâll keep your app fast and scalable as data grows.


