Laravel

Eloquent Performance: 7 Tricks That Actually Make a Difference

Svet Halachev Dec 20, 2025 4 min read
Eloquent Performance: 7 Tricks That Actually Make a Difference

Eloquent makes database work feel effortless. Maybe too effortless—it's easy to write queries that work fine in development but crawl in production once you have real data.

Here are seven techniques I reach for on every project. Nothing fancy, just solid practices that prevent the most common performance headaches.

1. Eager Load Relationships (Every Time)

You've heard this before, but it's worth repeating because it's the single biggest performance win in most Laravel apps.

// Bad: N+1 queries (1 query + 1 per user)
$users = User::all();
foreach ($users as $user) {
    echo $user->posts->count();
}

// Good: 2 queries total
$users = User::with('posts')->get();
foreach ($users as $user) {
    echo $user->posts->count();
}

Use Laravel Debugbar or Telescope to spot N+1 queries. Once you see a page making 200 queries instead of 3, you'll never forget to eager load again.

For nested relationships:

$posts = Post::with(['author', 'comments.user', 'tags'])->get();

2. Select Only What You Need

By default, Eloquent grabs every column. If your users table has 30 columns and you only need names for a dropdown, that's wasted memory and transfer time.

// Fetches everything
$users = User::all();

// Fetches only what you need
$users = User::select('id', 'name')->get();

// With eager loading
$posts = Post::with(['author' => function ($query) {
    $query->select('id', 'name');
}])->get();

Watch out: when eager loading, always include the foreign key columns or the relationship won't work.

3. Use Chunking for Large Datasets

Processing thousands of records? Don't load them all into memory at once.

// Bad: Loads 50,000 users into memory
User::all()->each(function ($user) {
    // Process user
});

// Good: Processes 1,000 at a time
User::chunk(1000, function ($users) {
    foreach ($users as $user) {
        // Process user
    }
});

// Even better for simple iterations
User::lazy()->each(function ($user) {
    // Process user
});

lazy() uses generators and is great for read-only operations. chunk() is better when you're updating records since it handles pagination properly.

4. Use Database Aggregates Instead of Collection Methods

Let the database do the math—it's optimized for it.

// Bad: Fetches all orders, then counts in PHP
$count = Order::where('status', 'completed')->get()->count();
$total = Order::where('status', 'completed')->get()->sum('amount');

// Good: Database does the work
$count = Order::where('status', 'completed')->count();
$total = Order::where('status', 'completed')->sum('amount');

Same goes for avg(), min(), max(), and exists():

// Check existence without fetching
if (User::where('email', $email)->exists()) {
    // Email taken
}

5. Index Your Query Columns

This isn't Eloquent-specific, but I see it missed constantly. If you're querying or sorting by a column, it probably needs an index.

// In your migration
Schema::table('orders', function (Blueprint $table) {
    $table->index('status');
    $table->index('created_at');
    $table->index(['user_id', 'status']); // Composite index
});

Check your slow queries with EXPLAIN:

// See the query plan
DB::table('orders')->where('status', 'pending')->explain()->dd();

6. Cache Expensive Queries

Some queries just take time—complex reports, aggregations across millions of rows. Cache them.

$stats = Cache::remember('dashboard-stats', 3600, function () {
    return [
        'total_users' => User::count(),
        'revenue_this_month' => Order::whereMonth('created_at', now()->month)->sum('amount'),
        'active_subscriptions' => Subscription::where('status', 'active')->count(),
    ];
});

For data that changes frequently but is expensive to compute, consider cache tags so you can invalidate selectively:

Cache::tags(['reports'])->remember('monthly-revenue', 3600, fn () => /* ... */);

// Later, when orders change
Cache::tags(['reports'])->flush();

7. Use Query Scopes to Keep Things Consistent

This isn't directly about performance, but scopes help you apply optimizations consistently across your app.

// In your Model
public function scopeWithBasicInfo($query)
{
    return $query->select('id', 'name', 'email', 'avatar');
}

public function scopeActive($query)
{
    return $query->where('status', 'active')
                 ->whereNotNull('email_verified_at');
}

// Usage
$users = User::active()->withBasicInfo()->get();

When you need to optimize a query, you update the scope once and every place using it benefits.

Bonus: Monitor in Production

All the optimization in the world doesn't help if you don't know where the problems are. Set up:

  • Laravel Telescope for development

  • Laravel Debugbar for local profiling

  • Query logging or APM tools for production

// Log slow queries in production
DB::listen(function ($query) {
    if ($query->time > 1000) { // Over 1 second
        Log::warning('Slow query', [
            'sql' => $query->sql,
            'time' => $query->time,
        ]);
    }
});

Wrapping Up

Most Eloquent performance issues come down to: loading too much data, making too many queries, or missing indexes. Fix those three things and you've handled 90% of database performance problems.

The remaining 10%? That's when you break out raw queries, read replicas, and serious database optimization. But you'd be surprised how far these basics take you.

Related Articles