Eloquent N+1 Query Optimization in Laravel | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com/public) - [Home](https://www.msaied.com/public)
- [Projects](https://www.msaied.com/public/projects)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [About](https://www.msaied.com/public#about)

           [  Contact](https://www.msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com/public)
- [ProjectsCase studies](https://www.msaied.com/public/projects)
- [ArticlesEngineering notes](https://www.msaied.com/public/articles)
- [CertificatesCredentials](https://www.msaied.com/public/certificates)
- [AboutHow I work](https://www.msaied.com/public#about)
- [ContactGet in touch](https://www.msaied.com/public#contact)

  [Start a conversation](https://www.msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/articles)
4. /
5. Eloquent Query Optimization: Killing N+1 Problems at the Source

 Eloquent Query Optimization: Killing N+1 Problems at the Source
================================================================

 N+1 queries silently destroy Laravel app performance. This guide shows senior engineers how to detect, eliminate, and prevent them using eager loading strategies, query analysis, and Eloquent best practices.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 27 Jul 2026 · Updated 27 Jul 2026 · 3 min read

ShareCopy linkCopied

 ![Eloquent Query Optimization: Killing N+1 Problems at the Source](https://cdn.msaied.com/475/39c5bf23764003ebc9ac32d840752d49.png) 

  On this page +1. [The N+1 Problem Is Still Killing Your App](#the-n1-problem-is-still-killing-your-app)
2. [Detecting N+1 Queries in Development](#detecting-n1-queries-in-development)
3. [The Obvious Fix: Eager Loading](#the-obvious-fix-eager-loading)
4. [Nested Eager Loading](#nested-eager-loading)
5. [Subtle N+1 Patterns That Survive Review](#subtle-n1-patterns-that-survive-review)
6. [Lazy Loading Inside Blade](#lazy-loading-inside-blade)
7. [Counting Relations](#counting-relations)
8. [Polymorphic Relations](#polymorphic-relations)
9. [Architectural Prevention](#architectural-prevention)
10. [Key Takeaways](#key-takeaways)

 The N+1 Problem Is Still Killing Your App
-----------------------------------------

Every Laravel developer knows what an N+1 query is. Far fewer have a systematic strategy for preventing them at the architectural level. This article goes beyond the basics — we'll look at detection tooling, subtle N+1 patterns that survive code review, and structural approaches that make the problem harder to introduce in the first place.

Detecting N+1 Queries in Development
------------------------------------

The fastest feedback loop is `DB::listen` or a dedicated package. In a service provider or test bootstrap:

```php
DB::listen(function (QueryExecuted $event) {
    if (str_contains($event->sql, 'select')) {
        logger()->debug($event->sql, ['time' => $event->time]);
    }
});

```

For a more structured approach, [Laravel Debugbar](https://github.com/barryvdh/laravel-debugbar) groups duplicate queries visually. In CI, use `spatie/laravel-query-detector` to fail tests when N+1 queries are detected automatically.

```php
// In a Pest test
use Spatie\QueryDetector\QueryDetector;

it('loads posts without N+1', function () {
    app(QueryDetector::class)->enable();

    $posts = Post::with('author', 'tags')->get();
    $posts->each(fn ($p) => $p->author->name);

    expect(app(QueryDetector::class)->getDetectedQueries())->toBeEmpty();
});

```

The Obvious Fix: Eager Loading
------------------------------

You already know `with()`. The subtlety is *when* to apply it.

```php
// Fragile: caller must remember to eager-load
$posts = Post::all();

// Better: scope it at the model level for common access patterns
class Post extends Model
{
    protected $with = ['author']; // always eager-load author
}

```

Use `$with` sparingly — it runs on every query including ones that don't need the relation. Prefer explicit `with()` calls in repository or query builder methods.

### Nested Eager Loading

```php
$posts = Post::with([
    'comments.author',
    'tags',
    'media' => fn ($q) => $q->where('type', 'cover'),
])->get();

```

Constrained eager loads let you filter the relation without triggering extra queries.

Subtle N+1 Patterns That Survive Review
---------------------------------------

### Lazy Loading Inside Blade

```blade
{{-- This fires a query per post --}}
@foreach ($posts as $post)
    {{ $post->author->name }}
@endforeach

```

Enable `Model::preventLazyLoading()` in `AppServiceProvider` for non-production environments:

```php
Model::preventLazyLoading(! app()->isProduction());

```

This throws a `LazyLoadingViolationException` immediately, surfacing the problem during development.

### Counting Relations

```php
// N+1: fires a COUNT query per post
$posts->each(fn ($p) => $p->comments->count());

// Correct: one extra query total
$posts = Post::withCount('comments')->get();
// Access via $post->comments_count

```

### Polymorphic Relations

Polymorphic `morphTo` relations are notorious. Use `morphWith` to eager-load the correct related types:

```php
$activities = Activity::with('subject')->get();
// subject could be Post, Comment, or Video — each type fires its own query

// Better: constrain per type
$activities = Activity::with([
    'subject' => fn ($morph) => $morph
        ->morphWith([
            Post::class => ['author'],
            Comment::class => ['post'],
        ]),
])->get();

```

Architectural Prevention
------------------------

The most durable fix is structural. If your query logic lives in a dedicated class, you control the eager loads in one place:

```php
final class PostListQuery
{
    public function handle(int $perPage = 20): LengthAwarePaginator
    {
        return Post::query()
            ->with(['author', 'tags', 'media'])
            ->withCount('comments')
            ->latest()
            ->paginate($perPage);
    }
}

```

Controllers and Filament resources call `PostListQuery` — they never build raw queries. Eager loading is a concern of the query object, not the caller.

Key Takeaways
-------------

- Enable `Model::preventLazyLoading()` in development; it catches violations immediately.
- Use `withCount()` instead of loading full relation collections just to count them.
- Constrain eager loads with closures to avoid over-fetching.
- Centralise query construction in dedicated query classes or repositories to enforce consistent eager loading.
- Add a Pest assertion using `spatie/laravel-query-detector` to your critical read paths in CI.

- [laravel](https://www.msaied.com/public/articles?search=laravel)
- [eloquent](https://www.msaied.com/public/articles?search=eloquent)
- [performance](https://www.msaied.com/public/articles?search=performance)
- [database](https://www.msaied.com/public/articles?search=database)
- [optimization](https://www.msaied.com/public/articles?search=optimization)

 Frequently asked questions 
---------------------------

  When should I use `$with` on a model versus calling `with()` explicitly?Use `$with` only for relations that are genuinely required on every access path for that model. For most cases, prefer explicit `with()` calls in query objects or repository methods so you don't over-fetch on queries that don't need the relation.

   Does `Model::preventLazyLoading()` affect production performance?No. The recommended pattern is `Model::preventLazyLoading(! app()-&gt;isProduction())`, which only activates the guard in non-production environments. In production it is a no-op with zero overhead.

   How do I handle N+1 issues in Filament table resources?Override the `getEloquentQuery()` method on your Filament resource's ListRecords page or the Resource class itself to add `with()` and `withCount()` calls. Filament does not automatically eager-load relations referenced in columns.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://www.msaied.com/public#person)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

[About](https://www.msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleLaravel API Rate-Limiting: Custom Limiters, Per-Route Strategies, and Header Contracts](https://www.msaied.com/public/articles/laravel-api-rate-limiting-custom-limiters-per-route-strategies-and-header-contracts-1) [Next articleLaravel API Resources: Sparse Fieldsets, Conditional Relationships, and Stable Contracts](https://www.msaied.com/public/articles/laravel-api-resources-sparse-fieldsets-conditional-relationships-and-stable-contracts)  

   On this page
-------------

1. [The N+1 Problem Is Still Killing Your App](#the-n1-problem-is-still-killing-your-app)
2. [Detecting N+1 Queries in Development](#detecting-n1-queries-in-development)
3. [The Obvious Fix: Eager Loading](#the-obvious-fix-eager-loading)
4. [Nested Eager Loading](#nested-eager-loading)
5. [Subtle N+1 Patterns That Survive Review](#subtle-n1-patterns-that-survive-review)
6. [Lazy Loading Inside Blade](#lazy-loading-inside-blade)
7. [Counting Relations](#counting-relations)
8. [Polymorphic Relations](#polymorphic-relations)
9. [Architectural Prevention](#architectural-prevention)
10. [Key Takeaways](#key-takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/public/sitemap.xml)
