Eloquent Query Optimization: 7 Common Mistakes That Kill Your Laravel App Performance
Meta Description: Discover the 7 most common Eloquent mistakes that slow down Laravel applications, with real benchmarks and concrete solutions from production experience.
Category: Laravel & Performance
Tags: Eloquent, Performance, Laravel, Optimization, Database, N+1
Suggested Publication Date: 1.5 months ago
Introduction
I recently audited a Laravel application that took 8 seconds to load a simple dashboard. The problem? Over 400 SQL queries to display 20 rows of data. The team was using Eloquent, but without understanding what was happening under the hood.
After optimization: same page, 12 queries, 180ms. That’s a 97% improvement.
Here are the 7 mistakes I encounter systematically in Laravel projects, and how to fix them. No theoretical BS—just real code from production apps I’ve worked on.
Mistake #1: The N+1 Problem (The Classic Killer)
This is the number one performance killer I see. It happens so subtly that most developers don’t even realize they’re doing it.
The Problem
// In your controller
$posts = Post::all();
// In your view
@foreach($posts as $post)
<p>{{ $post->user->name }}</p>
@endforeach
What actually happens in the database:
SELECT * FROM posts; -- 1 query
SELECT * FROM users WHERE id = 1; -- Query for each post
SELECT * FROM users WHERE id = 2;
SELECT * FROM users WHERE id = 3;
-- ... 100 queries if you have 100 posts
That’s 101 queries for something that should be 2 queries.
The Solution: Eager Loading
$posts = Post::with('user')->get();
Result:
SELECT * FROM posts;
SELECT * FROM users WHERE id IN (1,2,3,...); -- Just 2 queries total
Real Benchmark from One of My Projects
- Before: 847ms for 50 posts
- After: 43ms for 50 posts
- Improvement: 95% reduction
Pro Tip: Install Laravel Telescope
This will show you N+1 queries automatically. I install it on every project during development:
composer require laravel/telescope --dev
php artisan telescope:install
Now open /telescope in your browser and watch the “Queries” tab. Any duplicate queries? You’ve got an N+1 problem.
Mistake #2: SELECT * Instead of Selecting Only What You Need
Loading all columns when you only use 2-3 is pure waste. I see this everywhere.
Bad Practice
$users = User::all(); // Fetches ALL columns
If your users table has 15 columns including TEXT fields with long descriptions, you’re loading megabytes of unnecessary data.
Good Practice
$users = User::select(['id', 'name', 'email'])->get();
// Even better with relationships
$posts = Post::with(['user:id,name,avatar'])
->select(['id', 'title', 'user_id', 'created_at'])
->get();
Measured Impact on a Real Project
On a Users table with 15 columns including heavy TEXT fields:
- SELECT * : 2.3MB data transferred
- SELECT id, name, email : 180KB
- Savings: 92% less bandwidth
This matters especially on mobile connections.
Mistake #3: Using count() on a Collection Instead of the Database
This one drives me crazy because it’s so common.
Bad (Loads Everything Into Memory)
$count = Post::all()->count(); // DON'T DO THIS
This loads ALL posts into memory, creates a collection, then counts. If you have 10,000 posts, you just loaded 10,000 rows for nothing.
Good (Count at Database Level)
$count = Post::count(); // Let MySQL do the counting
Even Worse Pattern I’ve Seen
// Someone actually wrote this in a project I inherited
if (Post::all()->count() > 0) {
// ...
}
The Right Way
if (Post::exists()) {
// ...
}
Performance Difference
Post::all()->count()on 10K posts: 840msPost::count(): 8ms- 100x faster
Mistake #4: Forgetting Database Indexes
You do where() queries on columns without indexes? Your database is doing a full table scan. On small tables, you won’t notice. On 100K+ rows, it’s a disaster.
How to Add Indexes
// In your migration
Schema::table('posts', function (Blueprint $table) {
$table->index('status');
$table->index('user_id');
$table->index(['category_id', 'status']); // Composite index
});
When to Add Indexes
- Columns used in WHERE clauses
- Foreign keys (though Laravel migrations add these automatically)
- Columns in ORDER BY
- Columns in JOIN conditions
Real Example
I worked on an e-commerce app where the products search was taking 4 seconds. The query:
Product::where('status', 'active')
->where('category_id', $categoryId)
->orderBy('created_at', 'desc')
->paginate(20);
After adding a composite index on (status, category_id, created_at):
- Before: 4200ms
- After: 45ms
- 93% faster
Warning: Don’t Over-Index
Every index slows down INSERT/UPDATE operations. Index smartly:
- Don’t index columns that are rarely queried
- Don’t index columns with low cardinality (like boolean fields with only 2 values)
- Monitor your query patterns and index accordingly
Mistake #5: Loading Unused Relationships
The Problem
// Loading 5 relationships when you only use 2
$posts = Post::with(['user', 'comments', 'tags', 'category', 'media'])->get();
// But in your view, you only display user and category
You’re loading 3 unnecessary relationships. More queries, more memory, slower response.
The Solution
// Load only what you use
$posts = Post::with(['user', 'category'])->get();
Even Better: Conditional Loading
$posts = Post::query()
->when($request->has('include_comments'), function($query) {
$query->with('comments');
})
->get();
Now you only load comments when the request specifically asks for them.
Mistake #6: Not Using chunk() or lazy() for Large Datasets
Loading 50,000 rows with all() will crash your application. I’ve seen it happen multiple times.
Bad (Memory Overflow Guaranteed)
$users = User::all(); // Loading 50K users into memory
foreach ($users as $user) {
// Process
}
On a project with 80K users, this crashed the server every time someone ran the export script.
Good (Process in Batches)
User::chunk(200, function($users) {
foreach ($users as $user) {
// Process
}
});
Even Better (Laravel 8+)
User::lazy()->each(function($user) {
// Process one by one without memory overload
});
lazy() uses PHP generators under the hood. It’s magic for large datasets.
Real Use Case
Export of 120K users with PDF generation:
all(): crashed after 8K users (memory exhausted)chunk(500): success in 4min 23slazy(): success in 3min 47s
Mistake #7: Queries Inside Loops
This is N+1 version 2.0, often more subtle and harder to spot.
Horrible Example
foreach ($posts as $post) {
$post->update([
'views' => $post->views + 1
]); // 100 UPDATE queries
}
If you have 100 posts, that’s 100 separate UPDATE queries.
The Right Way
Post::whereIn('id', $posts->pluck('id'))
->increment('views');
One query instead of 100.
Another Common Pattern I See
// Building an array of IDs inside a loop
$userIds = [];
foreach ($posts as $post) {
$userIds[] = $post->user_id;
}
$users = User::whereIn('id', $userIds)->get();
Cleaner Version
$users = User::whereIn('id', $posts->pluck('user_id'))->get();
My Eloquent Optimization Checklist
Before every production deployment, I verify:
- ✅ Laravel Telescope installed in dev to spot queries
- ✅ No N+1 queries (Telescope detects them automatically)
- ✅ Database indexes on all search columns
- ✅ SELECT only necessary columns
- ✅
chunk()orlazy()for large volumes - ✅ Cache on recurring heavy queries
- ✅ Load tests with at least 1000 records
Bonus: My Favorite Debugging Tool
// Add this to any query to see the SQL
$posts = Post::with('user')
->where('status', 'published')
->toSql();
dd($posts); // Shows the actual SQL query
Or use ->dd() in Laravel 8+:
Post::with('user')
->where('status', 'published')
->dd(); // Dumps query and dies
Conclusion
Eloquent optimization isn’t optional. On all my production projects, fixing these 7 mistakes systematically divided response times by 5 to 10.
The good news? Most of these are automatically detectable with Laravel Telescope and fixable in minutes.
My rule of thumb: If a page takes more than 200ms to load, there’s an optimization opportunity. If it takes more than 500ms, there’s definitely a problem.
Start with the N+1 queries. That alone will give you 80% of the performance gains.
Is Your Laravel Application Slow?
I audit and optimize Laravel applications to improve their performance. From query analysis to intelligent caching implementation, contact me for a free diagnosis.
What I offer:
- Performance audit with detailed report
- Optimization of critical queries
- Caching strategy implementation
- Database indexing recommendations
- Load testing and scalability analysis
Let’s make your app fast!
