Boost Your Laravel and Database Performance: Real-World Tips for Faster Queries
A developer-focused guide to squeezing performance out of Laravel and MySQL - from fixing N+1 queries to caching, indexing, and knowing when to write raw SQL.
Slow queries are one of the most common production problems I've debugged. They creep in slowly - a project works fine at 1,000 rows, then grinds at 100,000. Here are the strategies that have made the most difference.
1. Eliminate N+1 Queries with Eager Loading
This is the most common Laravel performance mistake. Consider:
// ❌ N+1: 1 query for posts + N queries for each author
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name; // Fires a new query every iteration
}// ✅ 2 queries total, regardless of post count
$posts = Post::with('author')->get();Always use with() when you know you'll be accessing a relationship in a loop. Laravel Debugbar will show you the exact query count - make it a habit to check.
2. Index the Right Columns
Indexes dramatically speed up WHERE, ORDER BY, and JOIN operations - but they have a cost: they slow down writes and take up storage.
// In your migration
$table->index('user_id'); // Single column
$table->index(['user_id', 'status']); // Composite index for multi-column filtersRule of thumb: Index columns you filter or sort by frequently. Don't index columns that change often (high write volume) unless the read benefit clearly outweighs the write cost.
3. Chunk Large Datasets
Loading 500,000 records into memory at once will kill your server. Use chunk() or cursor():
// chunk() - processes in batches, lower memory peak
User::where('active', true)->chunk(500, function ($users) {
foreach ($users as $user) {
// process
}
});
// cursor() - uses a PHP generator, even lower memory
foreach (User::where('active', true)->cursor() as $user) {
// process
}cursor() is ideal when you process one record at a time. chunk() is better when you need a batch (e.g., bulk insert/update).
4. Cache Repetitive Query Results
Some data rarely changes but gets queried constantly - config values, lookup tables, aggregate stats. Cache them:
$categories = Cache::remember('product_categories', now()->addHours(6), function () {
return Category::orderBy('name')->get();
});The key is knowing your invalidation strategy. Tag-based caching (Cache::tags()) with Redis makes this much easier to manage at scale.
5. Select Only What You Need
// ❌ Fetches all columns
$users = User::all();
// ✅ Fetches only what's needed
$users = User::select('id', 'name', 'email')->get();This reduces data transfer between your DB and app server. On wide tables with JSON blobs or large text fields, this can be significant.
6. Profile Before You Optimize
Don't guess - measure. Two tools I use on every Laravel project:
- Laravel Debugbar - shows all queries, timing, memory per request during development
- Laravel Telescope - persistent query logging for staging environments
Turn them on, click through your app, and look for queries taking over 100ms or appearing more than expected.
7. Paginate Large Result Sets
Never return unbounded result sets to the UI. Always paginate:
$users = User::paginate(20);
// or for API responses:
$users = User::simplePaginate(20);Pagination also helps the database - LIMIT + OFFSET is far cheaper than loading everything.
8. Know When to Write Raw SQL
Laravel's query builder is excellent, but sometimes a complex GROUP BY with multiple aggregates is cleaner as raw SQL - and the DB optimizer can plan it better too:
$stats = DB::select(
'SELECT department, COUNT(*) as total, AVG(salary) as avg_salary
FROM employees
WHERE active = 1
GROUP BY department
ORDER BY total DESC'
);Just remember: always use parameter binding (? or named bindings) - never interpolate user input directly.
Summary
| Issue | Fix |
|---|---|
| Too many queries | Eager loading with with() |
| Slow filters/sorts | Add indexes |
| Memory spikes | chunk() or cursor() |
| Repeated DB calls | Cache with Cache::remember() |
| Over-fetching data | Select specific columns |
| Unbounded result sets | paginate() |
| Complex aggregates | Raw SQL with bindings |
Performance work is iterative. Profile first, optimize the worst offender, measure again. With these tools in your belt, you'll catch most problems before they reach production.