3 min read
Avoiding N+1
The N+1 query problem is one of the most common performance pitfalls in Laravel applications. It happens silently — your code runs correctly, but the database receives far more queries than necessary.
What Is N+1?
When you load a collection of models and then access a relationship on each one inside a loop, Laravel fires a separate query for each model. If you have 100 users, you end up with 101 queries: one to fetch the users, and one per user to fetch their related data.
// ❌ N+1 in action $users = User::all(); // 1 query foreach ($users as $user) { echo $user->profile->bio; // 1 query per user — 100 extra queries } // Total: 101 queries
Fixing It with Eager Loading
The with() method tells Laravel to load the relationship in a single additional query, regardless of how many parent models exist:
// ✅ Eager loading — 2 queries total $users = User::with('profile')->get(); foreach ($users as $user) { echo $user->profile->bio; // Already in memory } // Total: 2 queries
Loading Multiple Relationships
Pass an array to with() to eager load several relationships at once:
$users = User::with(['profile', 'posts', 'roles'])->get();
Nested Eager Loading
If your relationship chains go deeper, use dot notation:
// Load posts and, for each post, its comments and the comment authors $users = User::with([ 'posts.comments', 'posts.comments.author', ])->get();
Laravel translates this into a small, fixed number of queries no matter how many users, posts, or comments exist.
Constrained Eager Loading
You can apply conditions to an eager-loaded relationship without breaking out of the with() call:
$users = User::with([ 'posts' => function (Builder $query) { $query->where('published', true) ->orderBy('created_at', 'desc') ->limit(5); }, ])->get();
This loads only the five most recent published posts per user — still in a single query for the posts table.
Lazy Eager Loading
If you already have a collection and later discover you need a relationship, call load() on the collection rather than re-querying:
$users = User::all(); // Somewhere later in your code: $users->load('posts');
This adds one query for all the posts, not one per user.
Detecting N+1 Problems
Laravel Debugbar is the fastest way to spot N+1 queries during development. Install it and look at the Queries panel — repeated queries with the same structure but different IDs are a dead giveaway:
composer require barryvdh/laravel-debugbar --dev
Laravel also ships with a built-in query log you can enable in tests or local development:
use Illuminate\Support\Facades\DB; DB::enableQueryLog(); // ... run your code ... $queries = DB::getQueryLog(); // Inspect $queries to see every query that fired
Tips
- Eager load relationships at the point where you first fetch the data, not deeper in the call stack. This keeps the intent visible and the query count predictable.
- If you only need a count, use
withCount()instead ofwith(). It adds a subquery rather than loading every related model. - If you need a sum or other aggregate, use
withSum('orders', 'total')— same principle. - Review your eager loading regularly as your application grows. A relationship that did not exist last month may now cause N+1 problems in an existing query.
Related
- Performance Improvements in Laravel — eager loading in context alongside caching, query optimisation, and config caching
- Working with Large Datasets in Laravel — chunking and lazy collections for scenarios where eager loading alone is not enough