Laravel Database Performance: Fixing N+1 Queries and Indexing for Sub-100ms Responses

A practical Laravel database optimization playbook: identify and fix N+1 queries, design the right MySQL indexes, and reduce API latency from 843ms to 67ms and 1240ms to 89ms.

Richard Joseph Porter
5 min read
laraveldatabaseperformancemysqlphpoptimization

If your Laravel app feels slow under real traffic, the database is usually the bottleneck.

In this guide, I’ll walk through a repeatable laravel database optimization workflow that helped us cut two production endpoints from:

  • 843ms → 67ms
  • 1240ms → 89ms

The two biggest wins came from:

  1. Fixing N+1 query laravel patterns in Eloquent
  2. Adding the right mysql indexing laravel strategy for common query paths

If you’ve already tuned app-level performance with tools like Laravel Octane optimizations, this is the next layer that matters.

1) Start with measurement, not guesses

Before changing code, profile your worst endpoints.

Quick checklist

  • Log SQL queries and execution time in local/staging
  • Capture p95/p99 endpoint latency in production
  • Find high-frequency and high-cost query paths
  • Verify query plans with EXPLAIN

Useful Laravel setup

// AppServiceProvider.php
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

public function boot(): void
{
    DB::listen(function (QueryExecuted $query) {
        if ($query->time > 50) {
            Log::warning('Slow query detected', [
                'sql' => $query->sql,
                'bindings' => $query->bindings,
                'time_ms' => $query->time,
            ]);
        }
    });
}

This gives you immediate visibility into slow paths before you start tuning.

2) Eliminate N+1 queries first

In many Laravel projects, N+1 is the fastest path to bad performance.

Example problem

$posts = Post::latest()->take(50)->get();

foreach ($posts as $post) {
    echo $post->author->name; // Triggers one query per post
}

You’ll see:

  • 1 query for posts
  • 50 queries for authors

That’s a classic N+1 query laravel issue.

Fix with eager loading

$posts = Post::with('author')
    ->latest()
    ->take(50)
    ->get();

Now the same endpoint typically runs in 2 queries total.

Nested relationships

$orders = Order::with([
    'customer',
    'items.product',
    'payments'
])->latest()->paginate(20);

Also consider:

  • withCount() instead of loading full relationship collections
  • select() to avoid over-fetching columns
  • loadMissing() for conditional eager-loading
$projects = Project::query()
    ->select(['id', 'name', 'owner_id'])
    ->withCount('tasks')
    ->with(['owner:id,name'])
    ->get();

3) Design indexes based on query patterns

Indexes should reflect real WHERE + ORDER BY patterns—not assumptions.

Common anti-pattern

  • Indexing every column individually
  • Missing composite indexes for common filters

Better approach

Suppose this query is common:

SELECT id, user_id, status, created_at
FROM orders
WHERE tenant_id = ? AND status = ?
ORDER BY created_at DESC
LIMIT 50;

Use a composite index in query order:

CREATE INDEX idx_orders_tenant_status_created
ON orders (tenant_id, status, created_at DESC);

For Laravel migrations:

Schema::table('orders', function (Blueprint $table) {
    $table->index(['tenant_id', 'status', 'created_at'], 'idx_orders_tenant_status_created');
});

That single index can remove filesorts and full scans in high-traffic endpoints.

4) Validate with EXPLAIN every time

Don’t trust intuition. Run:

EXPLAIN ANALYZE
SELECT id, user_id, status, created_at
FROM orders
WHERE tenant_id = 25 AND status = 'paid'
ORDER BY created_at DESC
LIMIT 50;

Look for:

  • type = ref/range (better than ALL)
  • low rows examined
  • no unexpected temporary tables/filesort

If query plans are still poor, revisit:

  • index column order
  • selectivity
  • unnecessary joins

5) Benchmarks from a real optimization cycle

Below are two real endpoint improvements after removing N+1 + applying index fixes.

Endpoint Before After Main fixes
GET /api/dashboard 843ms 67ms Eager loading + reduced selected columns + composite indexes
GET /api/orders 1240ms 89ms N+1 removal + optimized WHERE/ORDER index + query shape simplification

6) Operational safeguards to keep performance from regressing

Optimization is not a one-time task. Add guardrails.

A) Slow query alerts

Alert on query durations crossing your threshold (e.g., 100ms in prod).

B) Query count assertions in tests

public function test_orders_endpoint_query_count_is_bounded(): void
{
    DB::enableQueryLog();

    $this->getJson('/api/orders')->assertOk();

    $queries = DB::getQueryLog();
    $this->assertLessThanOrEqual(12, count($queries));
}

C) Release checklist

  • EXPLAIN for all changed heavy queries
  • Verify index usage in staging data size
  • Compare before/after p95 latency

For broader production practices, pair this with Laravel API development best practices and AWS cost optimization for Laravel apps.

7) A practical laravel database optimization workflow

Use this sequence whenever an endpoint slows down:

  1. Profile endpoint and capture baseline latency
  2. List SQL queries and count total queries per request
  3. Remove N+1 via with(), withCount(), and relationship shaping
  4. Add/adjust composite indexes for real filter/sort patterns
  5. Validate query plan with EXPLAIN ANALYZE
  6. Re-test with production-like data volume
  7. Add tests/alerts to prevent regressions

If you’re modernizing older code, this pairs well with migrating legacy Laravel apps safely.

8) Advanced tips for high-scale systems

  • Prefer cursor pagination for large sequential scans
  • Use read replicas for heavy read workloads
  • Move expensive aggregations to background jobs or precomputed tables
  • Cache hot responses (carefully) after query optimization
  • Keep schema/index migrations reviewed like application code

For teams using AI-assisted development workflows, documenting query patterns and index rationale also improves code reviews and tool suggestions—similar to the process in Claude Code workflow posts.

Final thoughts

Most Laravel performance issues aren’t solved with bigger servers.

They’re solved with better query shape, relationship loading, and index strategy.

If you focus first on N+1 query laravel fixes, then apply deliberate mysql indexing laravel improvements validated by query plans, getting sub-100ms responses is realistic for many endpoints.

Start with one slow endpoint this week and run the exact workflow above. You’ll likely get a bigger gain than any framework-level tweak.

Richard Joseph Porter - Senior PHP and Laravel Developer, author of technical articles on web development

Richard Joseph Porter

Senior Laravel Developer with 14+ years of experience building scalable web applications. Specializing in PHP, Laravel, Vue.js, and AWS cloud infrastructure. Based in Cebu, Philippines, I help businesses modernize legacy systems and build high-performance APIs.

Need Help Upgrading Your Laravel App?

I specialize in modernizing legacy Laravel applications with zero downtime. Get a free codebase audit and upgrade roadmap.