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.