Eloquent Performance — Optimizing Laravel Database Queries
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
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