$ lexprog.com

// notes from an old coder -- php, databases, and the occasional rant

[February 13, 2024] Eloquent ORM

Eloquent Performance: N+1 Problem

Eloquent N+1 Problem: Tips & Tricks

────────────────────────────────────────────────────────

Eloquent N+1 Problem: Tips & Tricks

Tip: Enable Lazy Loading Prevention

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

Throws an exception when you access an unloaded relationship.

Gotcha: with() Loads Everything

Post::with('comments')->get();
// Loads ALL comments, even if you only need 5

Use constraints:

Post::with(['comments' => fn($q) => $q->latest()->limit(5)])->get();

Tip: Use withCount() Instead of Counting

// Bad: N+1
foreach ($posts as $post) {
    echo $post->comments->count();
}

// Good: 1 query
$posts = Post::withCount('comments')->get();
foreach ($posts as $post) {
    echo $post->comments_count;
}

Tip: Selective Column Loading

Post::with('author:id,name,email')->get();

Only loads specific columns from the related model.

Gotcha: load() Runs After the Fact

$posts = Post::all(); // Query 1
$posts->load('author'); // Query 2

Use with() instead if you know you need the relation.

Tip: Use exists() for Presence Checks

// Bad
if ($post->comments->isNotEmpty()) { }

// Good
if ($post->comments()->exists()) { }

Tip: Debug with DB::listen()

DB::listen(function ($query) {
    Log::info($query->sql);
});

See every query being executed.

Gotcha: pluck() Can Cause N+1 Too

$posts->pluck('author.name'); // N+1 if author not loaded

Tip: Use cursor() for Memory-Neutral Iteration

When exporting 100K rows, get() loads everything into memory. cursor() uses yield and keeps memory flat regardless of row count. Perfect for artisan commands.

Tip: whereHas() vs load() — Two Different Things

whereHas() filters the parent query by relationship existence. load() eager-loads relationships AFTER the query. Mixing them up is a common source of logic bugs.

Gotcha: touch() Fires Multiple Queries

Calling touch() on a model updates the updated_at of ALL related models in the relationship chain. This can cascade into dozens of unnecessary writes.

Senior Insight

The N+1 problem is the most common performance issue in Laravel applications, and it's so easy to prevent. Beyond using with(), I rely on load() for conditional eager loading — only load relationships when they're actually needed. I also use setEagerLoads() in API responses to let the client specify which relations to include. This approach eliminates the 'load everything just in case' pattern that bloats responses and slows applications.

Source: Laravel Docs (https://laravel.com/docs/eloquent), Laravel News (https://laravel-news.com/), Freek.dev (https://freek.dev/tags/eloquent)

────────────────────────────────────────────────────────
<-- back to posts