Back to posts
Database Optimization

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.

April 18, 20254 min read

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:

php
// ❌ 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
}
php
// ✅ 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.

php
// In your migration
$table->index('user_id');          // Single column
$table->index(['user_id', 'status']); // Composite index for multi-column filters

Rule 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():

php
// 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:

php
$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

php
// ❌ 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:

php
$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:

php
$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

IssueFix
Too many queriesEager loading with with()
Slow filters/sortsAdd indexes
Memory spikeschunk() or cursor()
Repeated DB callsCache with Cache::remember()
Over-fetching dataSelect specific columns
Unbounded result setspaginate()
Complex aggregatesRaw 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.