All articles
LaravelMySQLPerformanceEloquentDatabase

The N+1 Problem in Laravel: How I Find and Fix It

PKParth Kanasagra
8 min read

I once inherited a Laravel codebase making 847 database queries per page load. Not an exaggeration — I had Debugbar open and counted them. The page took 12 seconds to load. The developer who built it had no idea it was a problem because it felt "fine" on their local machine with 50 rows of test data.

The N+1 query problem is the most common performance issue I see in Laravel code reviews. Here's how I find it and fix it.

What N+1 Actually Means

You load a list of 20 posts (1 query). Then for each post, you access $post->author in your template. That's 20 more queries — one per post. So instead of 2 queries (posts + all their authors), you make 21. With 100 posts it's 101 queries. That's the N+1 pattern.

// This looks innocent but isn't
$posts = Post::all();

foreach ($posts as $post) {
    echo $post->author->name; // One query PER iteration
}

// This is 2 queries total, regardless of how many posts
$posts = Post::with('author')->get();

Step 1: Make N+1 Throw an Exception

The best thing you can do right now is add this to your AppServiceProvider:

public function boot(): void
{
    Model::preventLazyLoading(! app()->isProduction());
}

In development, any lazy loaded relationship now throws a LazyLoadingViolationException. You can't miss it. This alone has stopped me from shipping N+1 bugs more times than I can count.

Nested Relationships

Eager loading works at multiple levels. Load everything you need upfront:

// Load posts with authors, and each author's company
Post::with(['author.company', 'tags', 'comments' => function ($q) {
    $q->latest()->limit(3);
}])->paginate(15);

withCount() Instead of Counting Relations

This catches a lot of people:

// Bad — loads all comments just to count them
$posts = Post::with('comments')->get();
$posts->first()->comments->count(); // Already loaded all comment data

// Good — single aggregated query, no comment data transferred
$posts = Post::withCount('comments')->get();
$posts->first()->comments_count; // Just the integer

Subquery Selects for Single Values

Need just the latest comment date per post? Loading the whole comments relationship wastes memory. Use a subquery:

Post::addSelect([
    'latest_comment_at' => Comment::select('created_at')
        ->whereColumn('post_id', 'posts.id')
        ->latest()
        ->limit(1),
])->get();

This is one SQL query and is far more memory-efficient than eager loading the relationship.

Select Only What You Need

By default, Eloquent does SELECT *. On tables with text columns or many fields, this transfers a lot of unnecessary data. Be explicit:

Post::with(['author:id,name,avatar_url'])
    ->select(['id', 'title', 'slug', 'published_at', 'author_id'])
    ->published()
    ->paginate(20);

The id in the relationship select is required — Eloquent needs it to match relationships.

Caching for Repeated Expensive Queries

$categories = Cache::remember('post-categories', 3600, function () {
    return Category::withCount('publishedPosts')
                   ->orderBy('name')
                   ->get();
});

// When a category changes, invalidate specifically
Cache::forget('post-categories');

Database Indexes: The Cheap Win

Indexes don't fix N+1 queries but they do fix slow queries at scale. If you're filtering or ordering by a column frequently, it needs an index. I always add these in the migration:

$table->index(['user_id', 'is_published', 'published_at']);
$table->unique('slug');
$table->index('status');

Run EXPLAIN on your slowest queries. type: ALL means a full table scan — that's an index missing.

Finding N+1 in Production

Log slow queries in production using Laravel's built-in hook:

DB::whenQueryingForLongerThan(500, function (Connection $connection) {
    Log::channel('slack')->warning('Slow query detected', [
        'url' => request()->fullUrl(),
        'sql' => $connection->getQueryLog(),
    ]);
});

This gives you real production data about which pages are slow and why. Much better than guessing.

Related Articles

PK

Parth Kanasagra

Certified Senior Laravel & PHP Developer · 9+ years experience · Available for hire