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://www.msaied.com) [ Home ](https://www.msaied.com) [ Projects ](https://www.msaied.com/projects) [ Articles  ](https://www.msaied.com/articles) [ Certificates ](https://www.msaied.com/certificates) [ Contact ](https://www.msaied.com#contact-section) 

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

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

  [ home ](https://www.msaied.com)    [ articles ](https://www.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%2Fwww.msaied.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%2Fwww.msaied.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://www.msaied.com/articles) 

 [ ![Filament v5 Preview: Schema Unification, Performance Shifts, and How to Prepare](https://cdn.msaied.com/340/1a05ca68637b898b676efb66f22e627f.png) filament laravel php 

### Filament v5 Preview: Schema Unification, Performance Shifts, and How to Prepare

Filament v5 is reshaping how panels, forms, and tables are composed. This deep-dive covers the confirmed archi...

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

 1 Jul 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/filament-v5-preview-schema-unification-performance-shifts-and-how-to-prepare) [ ![Laravel 13: New Features, Helpers, and Practical Upgrade Notes](https://cdn.msaied.com/339/58c4fa6fe9b6d25a2dac17c621b6f4c6.png) laravel laravel-13 upgrade 

### Laravel 13: New Features, Helpers, and Practical Upgrade Notes

Laravel 13 ships with async-first defaults, a leaner bootstrapping layer, and several quality-of-life helpers....

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

 1 Jul 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-13-new-features-helpers-and-practical-upgrade-notes) [ ![Laravel 12: Structured Route Files, Slim Skeletons, and the New Application Bootstrapping](https://cdn.msaied.com/337/05b39d16d0f88a5fb94d0cf74049b88b.png) laravel laravel-12 upgrade 

### Laravel 12: Structured Route Files, Slim Skeletons, and the New Application Bootstrapping

Laravel 12 ships with a leaner skeleton, first-class route file organisation, and a revised application bootst...

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

 1 Jul 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-12-structured-route-files-slim-skeletons-and-the-new-application-bootstrapping) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://www.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://www.msaied.com)
- [Projects](https://www.msaied.com/projects)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.msaied.com/certificates)
- [Contact](https://www.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)
