Skip to content

Eloquent Performance — Optimizing Laravel Database Queries

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Eloquent Performance. We cover key concepts, practical examples, and best practices to help you master this topic.

Laravel Eloquent performance optimization focuses on reducing database queries, optimizing memory usage with chunking and cursors, and using database indexes and subqueries.

What You'll Learn

By the end of this tutorial, you'll profile Eloquent queries, use chunk() and cursor() for large datasets, optimize with select() and exists(), implement subqueries, and tune database indexes.

Why Performance Matters

Slow database queries are the most common performance bottleneck in web applications. Optimizing Eloquent queries reduces response times and server load significantly.

Real-World Use

A reporting application processes 500,000 records. Using chunk() instead of all() reduces memory from 500MB to 5MB. Adding indexes cuts query time from 3 seconds to 50ms.

Performance Path

flowchart LR
  A[Eloquent ORM] --> B[Performance]
  B --> C[Query Profiling]
  B --> D[Chunking]
  B --> E[Cursor]
  B --> F[Subqueries]
  B --> G{You Are Here}
  style G fill:#f90,color:#fff

Query Profiling

Use Laravel Debugbar or query logging to identify slow queries.

<?php
// Enable query logging
use Illuminate\Support\Facades\DB;
DB::enableQueryLog();
$users = User::with("posts")->get();
$queries = DB::getQueryLog();
foreach ($queries as $q) {
    echo sprintf("%s - %s %s\n",
        $q["time"],
        $q["query"],
        json_encode($q["bindings"])
    );
}
// Production monitoring
use Illuminate\Database\Events\QueryExecuted;
Event::listen(QueryExecuted::class, function (QueryExecuted $query) {
    if ($query->time > 1000) {
        Log::warning("Slow query", [
            "sql" => $query->sql,
            "bindings" => $query->bindings,
            "time" => $query->time,
        ]);
    }
});

Chunking

Process large datasets in memory-efficient batches.

<?php
// Process 500k users in chunks of 1000
User::chunk(1000, function (Collection $users) {
    foreach ($users as $user) {
        // Process each user
        $user->sendNewsletter();
    }
});
// Chunk by ID for consistent chunking
User::where("status", "active")
    ->chunkById(500, function (Collection $users) {
        foreach ($users as $user) {
            $user->update(["last_processed_at" => now()]);
        }
    });

Cursor for Large Datasets

Use cursor() for memory-efficient iteration without loading all results.

<?php
// all() loads ALL rows into memory
$users = User::all(); // Memory: O(N)
// cursor() yields each row one at a time
foreach (User::cursor() as $user) {
    // Memory: O(1) - only one loaded at a time
    $user->processReport();
}
// Chunk before cursor for large relations
$posts = Post::with(["comments" => fn($q) => $q->cursor()])->cursor();

Subquery Optimization

Use subqueries to avoid N+1 pattern with aggregate data.

<?php
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
// Instead of loading all comments for each post
$posts = Post::addSelect([
    "last_comment_date" => Comment::select("created_at")
        ->whereColumn("post_id", "posts.id")
        ->latest()
        ->limit(1),
    "comment_count" => Comment::select(DB::raw("COUNT(*)"))
        ->whereColumn("post_id", "posts.id"),
])->get();
// Or using withExists
$posts = Post::withExists(["comments" => fn($q) => $q->where("approved", true)])->get();

Select Optimization

Only fetch columns you need.

<?php
// Bad - fetches all columns
$users = User::all();
// Good - only fetch needed columns
$users = User::select("id", "name", "email")->get();
// Without eager loading N+1
$users = User::with(["profile:id,user_id,bio"])->get();
// Use relationships with subquery
$users = User::addSelect(["last_login" => Login::select("created_at")
    ->whereColumn("user_id", "users.id")
    ->latest()
    ->limit(1)
])->get();

Common Mistakes

1. Loading All Rows with all()

all() loads every row into memory. Use paginate() or chunk() for large datasets.

2. Not Adding Database Indexes

Without indexes, queries scan entire tables. Add indexes for WHERE, JOIN, and ORDER BY columns.

3. Using get() When first() Suffices

get() returns a collection. Use first() for a single row.

4. Forgetting to Disable Query Log in Production

Query logging in production fills memory. Disable it.

5. Overusing Eager Loading on Large Collections

Eager loading many relations creates large joins. Use Lazy Loading with load().

Practice Questions

1. What is the difference between chunk() and cursor()?

chunk() processes batches of models. cursor() yields models one at a time.

2. How do you log slow queries?

Listen for QueryExecuted event and check the time attribute.

3. Why is select() important for performance?

Fetching only needed columns reduces data transfer and memory usage.

4. What is a subquery in Eloquent?

A nested SELECT that returns aggregate data without additional queries.

5. Challenge: Optimize a heavy query using chunk and select.

<?php
User::select("id", "email", "name")
    ->where("active", true)
    ->chunkById(500, function ($users) {
        foreach ($users as $user) {
            Mail::to($user)->send(new ReportMail($user));
        }
    });

FAQ

Should I always use eager loading?

Not always. For simple belongsTo with few models, lazy loading is fine. Profile first.

What is the best chunk size?

500-1000. Small chunks mean more queries. Large chunks use more memory.

Do indexes help with Eloquent performance?

Yes. Indexes are the most impactful optimization for database queries.

How do I monitor query performance in production?

Use Laravel Telescope, Debugbar, or log slow queries with QueryExecuted listener.

What is the N+1 detection tool?

PreventLazyLoading in AppServiceProvider throws exceptions for accidental lazy loads.

Mini Project: Query Performance Audit

Create a performance audit for an Eloquent-based application.

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\DB;
class AuditQueries extends Command {
    public function handle(): void {
        DB::enableQueryLog();
        // Run your application code here
        $this->call("db:seed", ["--class" => "TestDataSeeder"]);
        $queries = DB::getQueryLog();
        $slowQueries = array_filter($queries, fn($q) => $q["time"] > 200);
        $this->info(sprintf("Total queries: %d, Slow: %d", count($queries), count($slowQueries)));
        foreach ($slowQueries as $q) {
            $this->warn(sprintf("%dms: %s", $q["time"], $q["query"]));
        }
    }
}

What's Next

Laravel Artisan Console Laravel Queues Deep Laravel Cache Deep

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro