Eloquent N+1 Fixes: Eager Loading &amp; Subquery Selects | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    Eloquent N+1 at Scale: Eager Loading Strategies, Subquery Selects, and Lazy Eager Loading        On this page       1. [  Eloquent N+1 at Scale: Beyond with() ](#eloquent-n1-at-scale-beyond-codewithcode)
2. [  The Classic Problem, Briefly ](#the-classic-problem-briefly)
3. [  Subquery Selects: Collapse the Relation Into a Column ](#subquery-selects-collapse-the-relation-into-a-column)
4. [  withCount vs. loadCount vs. Subquery — When to Use Each ](#codewithcountcode-vs-codeloadcountcode-vs-subquery-when-to-use-each)
5. [  Lazy Eager Loading Without the N+1 ](#lazy-eager-loading-without-the-n1)
6. [  Chunked Processing and Eager Loading Together ](#chunked-processing-and-eager-loading-together)
7. [  Detecting N+1 in CI with Clockwork or a Custom Listener ](#detecting-n1-in-ci-with-clockwork-or-a-custom-listener)
8. [  Key Takeaways ](#key-takeaways)

  ![Eloquent N+1 at Scale: Eager Loading Strategies, Subquery Selects, and Lazy Eager Loading](https://cdn.msaied.com/302/40500f25f8a29e6cd6eac4938f7211d0.png)

  #laravel   #eloquent   #performance   #database  

 Eloquent N+1 at Scale: Eager Loading Strategies, Subquery Selects, and Lazy Eager Loading 
===========================================================================================

     27 Jun 2026      4 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   Eloquent N+1 at Scale: Beyond with()  ](#eloquent-n1-at-scale-beyond-codewithcode)
2. [  02   The Classic Problem, Briefly  ](#the-classic-problem-briefly)
3. [  03   Subquery Selects: Collapse the Relation Into a Column  ](#subquery-selects-collapse-the-relation-into-a-column)
4. [  04   withCount vs. loadCount vs. Subquery — When to Use Each  ](#codewithcountcode-vs-codeloadcountcode-vs-subquery-when-to-use-each)
5. [  05   Lazy Eager Loading Without the N+1  ](#lazy-eager-loading-without-the-n1)
6. [  06   Chunked Processing and Eager Loading Together  ](#chunked-processing-and-eager-loading-together)
7. [  07   Detecting N+1 in CI with Clockwork or a Custom Listener  ](#detecting-n1-in-ci-with-clockwork-or-a-custom-listener)
8. [  08   Key Takeaways  ](#key-takeaways)

 Eloquent N+1 at Scale: Beyond `with()`
--------------------------------------

Every Laravel developer knows `with()`. Fewer know when it becomes the wrong tool. At scale — thousands of rows, deeply nested relations, or paginated admin panels — naive eager loading trades N+1 for a single monstrous `IN (...)` clause that can be just as slow. Here is how to think more precisely.

---

### The Classic Problem, Briefly

```php
// Fires 1 + N queries — one per post
$posts = Post::all();
foreach ($posts as $post) {
    echo $post->author->name;
}

```

The fix everyone knows:

```php
$posts = Post::with('author')->get();

```

This works until your `IN` clause contains 50,000 IDs and MySQL's optimizer gives up.

---

### Subquery Selects: Collapse the Relation Into a Column

When you only need a single scalar from a relation — a count, a latest date, a status flag — a subquery select is almost always faster than eager loading the full relation.

```php
use Illuminate\Database\Query\Builder;

$posts = Post::query()
    ->addSelect([
        'latest_comment_at' => Comment::select('created_at')
            ->whereColumn('post_id', 'posts.id')
            ->latest()
            ->limit(1),
    ])
    ->withCasts(['latest_comment_at' => 'datetime'])
    ->paginate(50);

```

One query. No relation hydration. The subquery runs correlated per row, but the database can use an index on `(post_id, created_at)` and the planner often chooses a nested-loop index scan that outperforms a large `IN`.

The same pattern replaces `withCount` for filtered counts:

```php
$posts = Post::query()
    ->addSelect([
        'approved_comment_count' => Comment::selectRaw('COUNT(*)')
            ->whereColumn('post_id', 'posts.id')
            ->where('approved', true),
    ])
    ->get();

```

---

### `withCount` vs. `loadCount` vs. Subquery — When to Use Each

| Scenario | Recommended approach | |---|---| | Always need the count | `withCount` on the base query | | Conditionally need it after load | `loadCount` on the collection | | Filtered count or latest scalar | Subquery select | | Multiple aggregates, same table | Single raw subquery with `CASE` |

---

### Lazy Eager Loading Without the N+1

`load()` fires a second query after the collection is already in memory. It is not lazy in the PHP sense — it is deferred eager loading. Use it when the decision to load a relation depends on runtime logic:

```php
$posts = Post::paginate(100);

if ($request->boolean('include_authors')) {
    $posts->load('author');
}

```

For truly lazy loading with N+1 prevention, enable `Model::preventLazyLoading()` in `AppServiceProvider`:

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

```

This throws a `LazyLoadingViolationException` in local and CI environments, surfacing every unguarded relation access before it reaches production.

---

### Chunked Processing and Eager Loading Together

`chunk()` and `cursor()` break large datasets into manageable pieces, but they do not automatically eager-load relations. Use `chunkById` with a manual `load()` call:

```php
Post::chunkById(500, function ($posts) {
    $posts->load('author', 'tags');

    foreach ($posts as $post) {
        // author and tags already hydrated
        dispatch(new SyncPostToSearch($post));
    }
});

```

Avoid `cursor()` when you need relations — it yields one model at a time and cannot batch-load anything.

---

### Detecting N+1 in CI with Clockwork or a Custom Listener

```php
DB::listen(function ($query) {
    if (app()->runningUnitTests()) {
        // Fail fast if a single test fires more than 10 queries
        static $count = 0;
        if (++$count > 10) {
            throw new \RuntimeException('Possible N+1: query count exceeded threshold.');
        }
    }
});

```

This is crude but effective as a canary. For production, Telescope's query panel grouped by endpoint is the fastest way to spot regressions.

---

### Key Takeaways

- `with()` is correct for full relation hydration; subquery selects are better for single scalars or filtered aggregates.
- `Model::preventLazyLoading()` in non-production environments eliminates entire classes of N+1 bugs before deployment.
- `chunkById` + `load()` is the correct pattern for large batch jobs that need relations.
- A correlated subquery with a good covering index often outperforms a large `IN (...)` eager load.
- Measure with `EXPLAIN ANALYZE` before assuming eager loading is always the right fix.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Feloquent-n1-at-scale-eager-loading-strategies-subquery-selects-and-lazy-eager-loading&text=Eloquent+N%2B1+at+Scale%3A+Eager+Loading+Strategies%2C+Subquery+Selects%2C+and+Lazy+Eager+Loading) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Feloquent-n1-at-scale-eager-loading-strategies-subquery-selects-and-lazy-eager-loading) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  When should I use a subquery select instead of withCount?        Use a subquery select when you need a filtered aggregate, a latest scalar value, or when the IN clause from withCount would contain thousands of IDs. Subquery selects run correlated per row but leverage covering indexes efficiently and avoid large IN clauses that can degrade the query planner. 

      Q02  Does Model::preventLazyLoading() affect production performance?        No. The recommended pattern is to pass !app()-&gt;isProduction() so the guard only throws exceptions in local and CI environments. In production the check is skipped entirely, adding zero overhead. 

      Q03  Can I use cursor() with eager loading for large exports?        No. cursor() yields one Eloquent model at a time using a PHP generator and cannot batch-load relations. Use chunkById() instead, then call load() on each chunk to eager-load relations in batches of a controlled size. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://msaied.com/articles) 

 [ ![Multi-Tenant SaaS with Filament: Per-Tenant Panel Themes and Dynamic Config at Runtime](https://cdn.msaied.com/301/88c73195f8f12fd047dc2683b2105ead.png) laravel filament multi-tenant 

### Multi-Tenant SaaS with Filament: Per-Tenant Panel Themes and Dynamic Config at Runtime

Go beyond basic tenant scoping. Learn how to swap Filament panel colours, navigation, and config values per te...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 27 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/multi-tenant-saas-with-filament-per-tenant-panel-themes-and-dynamic-config-at-runtime) [ ![Help Make Filament Faster: Beta Versions of v4 and v5 Now Available for Testing](https://cdn.msaied.com/299/b7163ad1d319ebdd0e7128cc976053bf.png) Filament Laravel Performance 

### Help Make Filament Faster: Beta Versions of v4 and v5 Now Available for Testing

The Filament team has released performance-focused beta versions of both v4 and v5. Install them today, test i...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 26 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/help-make-filament-faster-beta-versions-of-v4-and-v5-now-available-for-testing) [ ![Filament v4 Schema-Based Forms, Infolists, and the Unified Schema API](https://cdn.msaied.com/297/6eb3a7aaf7148fd21116eea870bd004e.png) filament laravel filament-v4 

### Filament v4 Schema-Based Forms, Infolists, and the Unified Schema API

Filament v4 replaces scattered form and infolist definitions with a single Schema API. Learn how unified schem...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 26 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/filament-v4-schema-based-forms-infolists-and-the-unified-schema-api-1) 

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

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
