paulund

Avoiding N+1

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 of with(). 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.