$ lexprog.com

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

[June 15, 2025] Eloquent ORM

Eloquent Chunk Processing: Large Datasets

Eloquent Chunk Processing: Large Datasets

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

Eloquent Chunk Processing: Large Datasets

Tip: chunk() for Batch Processing

User::chunk(100, function ($users) {
    foreach ($users as $user) {
        process($user);
    }
});

Loads 100 records at a time.

Gotcha: chunk() and Updates

If you update records inside chunk(), the next chunk may skip or duplicate rows. Use chunkById() instead.

Tip: chunkById() for Safe Updates

User::chunkById(100, function ($users) {
    foreach ($users as $user) {
        $user->update(['processed' => true]);
    }
});

Uses ID-based pagination, safe for updates.

Gotcha: cursor() for Memory Efficiency

foreach (User::cursor() as $user) {
    process($user);
}

Yields one model at a time. Lowest memory usage.

Tip: lazy() for Chunked Cursor

User::lazy(500)->each(fn($user) => process($user));

Combines chunk efficiency with cursor memory savings.

Gotcha: Relations with Cursor

Eager loading with cursor() can still cause memory issues. Load relations manually within the loop.

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: withCount() Adds a Subquery

withCount('comments') runs a correlated subquery on every row. On large tables, this can be slower than a separate query. Profile before relying on it.

Senior Insight

Processing large datasets in Laravel is a common requirement, and the choice between chunk(), cursor(), and lazy() has real performance implications. chunk() loads models into memory in batches — fine for most cases, but it can skip or duplicate records if the data changes during iteration. cursor() uses yield and keeps memory flat, making it ideal for exports. lazy() is a middle ground. I default to lazy() for read-only operations and chunk() when I need to update records.

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

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