# Laravel Eloquent Performance Cheat Sheet
_N+1 queries, chunking and the memory limits Eloquent hides_
Eloquent makes the expensive query look identical to the cheap one. These are the patterns that keep a convenient ORM from becoming the bottleneck.
> Difficulty: intermediate  
> Version: 1.0  
> Updated: 2025-10-09  
> Categories: Laravel  
> Tags: Git

Source: https://invitationbuddy.com/cheat-sheet/laravel-eloquent-performance-cheat-sheet

---

## Loading relations
| Call | Does |
| --- | --- |
| `Post::with("author")->get()` | Eager load — 2 queries instead of N+1 |
| `Post::with("comments.author")->get()` | Nested eager load |
| `Post::with(["author:id,name"])->get()` | Select only the columns you need |
| `Post::withCount("comments")->get()` | A count without loading the rows |
| `Post::withExists("comments")->get()` | Cheaper still when you only need a boolean |
| `$posts->load("tags")` | Eager load after the fact on an existing collection |
| `Model::preventLazyLoading()` | Throws on lazy loads in dev — the fastest way to find them all |

## Iterating large sets
| Method | Memory | Caveat |
| --- | --- | --- |
| `get()` | Everything at once  Fine until the table grows |
| `chunk(500, fn)` | One chunk at a time  MODIFYING rows inside shifts the offsets |
| `chunkById(500, fn)` | One chunk at a time  The safe choice when you write during the loop |
| `lazy()` | One model at a time  Chunks underneath; behaves like a generator |
| `cursor()` | One model at a time  A single held connection — not for slow per-row work |

## Finding an N+1 in one run
Call Model::preventLazyLoading(! app()->isProduction()) in a service provider. Every lazy load then throws in local and staging with the exact relation and model named, and stays silent in production. It finds in one page load what reading the query log finds in an afternoon.

## Code examples
### Find every N+1 in one page load
Throws on any lazy load outside production, naming the exact relation and model.
```php
use Illuminate\Database\Eloquent\Model;

public function boot(): void
{
    // Loud in local and staging, silent in production
    Model::preventLazyLoading(! app()->isProduction());
}

// Then the fix is mechanical:
$posts = Post::query()
    ->with(['author:id,name', 'tags:id,name'])   // only the columns used
    ->withCount('comments')                       // a count, not the rows
    ->latest()
    ->paginate(20);

// Writing while iterating? chunkById, never chunk — chunk() shifts its
// own offsets when the rows it already passed are modified.
Post::where('needs_reindex', true)
    ->chunkById(500, fn ($rows) => $rows->each->reindex());
```

## FAQs
**What is the fastest way to find N+1 queries?**
Model::preventLazyLoading(! app()->isProduction()) in a service provider. Every lazy load then throws in local and staging, naming the exact model and relation, and stays silent in production.

**chunk or chunkById?**
chunkById whenever the loop modifies the rows it is reading. Plain chunk() uses offsets, and rows changing under it shift those offsets so records get skipped.

---
_Generated from https://invitationbuddy.com/cheat-sheet/laravel-eloquent-performance-cheat-sheet_
