← Back to list

Mastering Laravel Query Optimization

If you have ever shipped a Laravel app that worked perfectly in local but started gasping for air in production, you are not alone. Slow…

Coder Manjeet in Towards Dev · 2026-04-14 13:01 · 3 claps · 8.7 min read paywalled
#laravel #php #coding #programming #database
Open on Medium ↗
Wiki topics: AI · AI · General 💻 · Programming

Mastering Laravel query optimization

Mastering Laravel query optimization

Mastering Laravel Query Optimization

If you have ever shipped a Laravel app that worked perfectly in local but started gasping for air in production, you are not alone. Slow pages, 1000+ queries on a single request, exports that time out, dashboards that take forever to load — most of the time, the real culprit is not Laravel itself, but how we write our database queries.

In 2026, the framework gives us more tools than ever: strict Eloquent modes, smarter chunking and lazy loading, better indexing practices, even full‑text search and vector features in the ecosystem. Still, I keep reviewing code where a few small changes turn multi‑second queries into millisecond ones.

In this post, I want to walk you through the key trends and practical techniques around Laravel query optimization that I see in real projects right now. We will focus on real‑world problems, slow dashboards, heavy exports, and abusive LIKE queries — and fix them step by step with modern Laravel patterns.

It is a survival skill. By the end of this article, I want you to feel confident that you can spot slow queries early and fix them without premature micro‑optimizations.

1. Why Query Optimization Matters More Than Ever

Modern Laravel apps rarely stay small. E‑commerce, SaaS dashboards, survey tools, and internal systems all hit tens of thousands of rows quickly, and response time directly impacts user satisfaction and revenue. Research on PHP frameworks shows that database optimization, eager loading, indexing, and caching can cut response times by over 30 percent under load, especially for Laravel‑based systems.

Laravel’s own documentation and ecosystem guides now treat optimization as a first‑class concern: chunking large result sets, eager loading to avoid N+1, adding proper indexes, and caching configuration, routes, and expensive queries are all recommended as baseline best practices. Community articles and talks from Laravel developers regularly highlight that slow APIs and dashboards are almost always traceable to a handful of badly written queries, not the framework.

Query optimization has moved from “advanced topic” to “daily habit.” The teams that win are the ones who design queries deliberately, measure them, and treat performance regressions as real bugs.

2. Treat N+1 Queries as a Bug, Not an Accident

If you have already read my previous post, you probably know exactly what I mean by N+1 queries, since I covered them in detail there.

The most common performance issue I still see in Laravel code reviews is the N+1 query problem: loading a collection, then lazily touching a relationship inside a loop.

Naive example (orders dashboard):

$orders = Order::latest()->take(50)->get();

foreach ($orders as $order) {
    echo $order->customer->name;
    echo $order->items->count();
}

This looks harmless, but:

  • 1 query for the 50 orders.
  • Up to 50 queries for customer.
  • Up to 50 queries for items.

That is roughly 101 queries for one page.

Fix it with eager loading and counts

$orders = Order::with(['customer'])
    ->withCount('items')
    ->latest()
    ->take(50)
    ->get();

foreach ($orders as $order) {
    echo $order->customer->name;   // no extra query
    echo $order->items_count;      // comes from withCount
}

Now you are down to 2 queries: one for the orders, one for the relationships and counts, which is exactly how Laravel’s eager loading is intended to be used.

Use strict mode to catch lazy loading early

A big trend in modern Laravel is turning on Eloquent strictness in non‑production environments so any accidental lazy loading throws an exception. Using preventLazyLoading() in your AppServiceProvider forces you to fix N+1 issues during development instead of discovering them with angry production logs later:

use Illuminate\Database\Eloquent\Model;

public function boot(): void
{
    Model::preventLazyLoading(! app()->isProduction());
}

Laravel’s docs explicitly recommend this pattern as a way to keep query behavior predictable and avoid hidden N+1s. If you only apply one idea from this article, make it this one.

3. Index‑First Mindset for Complex Queries

Once you fix N+1 issues, the next bottleneck is usually heavy where/join/orderBy queries on large tables. Community guides and performance case studies increasingly emphasize that most slow queries come from missing or bad indexes, not from PHP.

Imagine a reporting page with this query:

$orders = Order::query()
    ->whereBetween('created_at', [$from, $to])
    ->where('status', 'paid')
    ->orderByDesc('created_at')
    ->paginate(50);

If status and created_at are not indexed, your database may have to scan the entire table for every request, which is catastrophic once you hit hundreds of thousands of rows.

Measure with EXPLAIN and slow query logs

Before adding indexes blindly, follow the widely recommended flow:

  • Turn on the database slow query log (or use tools like Laravel Telescope / Debugbar).
  • Run EXPLAIN on the query to see if it is doing a full table scan (“ALL”) and which indexes are used.

This “measure first, then index” approach is now a standard best practice in Laravel performance articles.

Add the right composite index

For the query above, a common pattern is a composite index:

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

Performance‑focused Laravel articles show that targeted indexes on WHERE, JOIN, and ORDER BY columns can improve query speed by several times, especially under real traffic. The key trend is designing indexes around actual query patterns, not guessing.

4. Stream Large Datasets Instead of Loading Them

Another optimization pattern that has become mainstream is never loading huge result sets into memory at once. Laravel 13’s Eloquent documentation highlights chunk, chunkById, lazy, and cursor as recommended strategies when dealing with tens of thousands of records. Performance guides also emphasize that chunking is essential when processing large datasets in APIs or background jobs.

Imagine an export job that does this:

$orders = Order::with('customer', 'items')->get();

// build CSV from $orders...

On a big table, this can easily exhaust memory or take so long that the job times out.

Use chunking for safer exports

Order::with('customer', 'items')
    ->chunkById(500, function ($orders) use ($csvWriter) {
        foreach ($orders as $order) {
            $csvWriter->addRow([
                $order->id,
                $order->customer->name,
                $order->items->count(),
                $order->total,
            ]);
        }
    });

According to Laravel’s docs, chunkById is preferred when you might be updating the records as you iterate, and it prevents inconsistent result sets by always moving forward on the ID.

When to use lazy collections and cursors

  • lazy() gives you a LazyCollection that behaves like a stream, still querying in chunks behind the scenes.
  • cursor() keeps only one model in memory at a time, but cannot eager load relationships, so it is best for simple scalar processing.

The trend here is clear: if you see Model::all() on a busy table, treat it as a code smell and replace it with chunking or lazy approaches.

5. Ask Less from the Database (Smarter Selects and Aggregates)

Another common mistake is asking the database for far more data than you actually need. Modern Laravel optimization guides repeatedly recommend selecting only necessary columns, using aggregates instead of full collections, and preferring EXISTS‑style checks over loading models you do not use.

Select only needed columns

If an API endpoint only needs a few fields, do not hydrate full models with every column:

$users = User::query()
    ->select(['id', 'name', 'email'])
    ->where('active', true)
    ->paginate(50);

This reduces network payload and improves both database and PHP performance, and is a widely recommended baseline practice.

Use pluck and aggregates instead of full collections

Need just IDs?

$userIds = User::where('active', true)->pluck('id');

Need counts or sums?

$activeCount = User::where('active', true)->count();
$revenue = Order::where('status', 'paid')->sum('total');

Laravel’s query builder and Eloquent builder offer explicit methods like exists, doesntExist, and relationship helpers that internally use efficient EXISTS queries instead of loading entire rows.

Use withCount and withExists for dashboards

For dashboard cards showing counts and badges, you can avoid extra queries by using withCount() and related helpers:

$users = User::withCount('posts')->paginate(20);

These are built specifically to generate performant subqueries and are safer than manual counting loops.

6. Cache and Queue Heavy Query Work

Not every query has to hit the database on every request. Modern Laravel performance guides consistently stress caching expensive queries and offloading heavy, non‑interactive work to queues to keep your main HTTP requests fast.

Cache expensive, read‑heavy queries

For example, a dashboard that shows aggregated metrics which change every few minutes:

$stats = Cache::remember('dashboard:stats', now()->addMinutes(5), function () {
    return [
        'orders_today' => Order::whereDate('created_at', today())->count(),
        'revenue_today' => Order::whereDate('created_at', today())
            ->where('status', 'paid')
            ->sum('total'),
    ];
});

Laravel’s official docs and community articles point out that caching configuration, routes, views, and frequently accessed query results is a key part of production hardening.

Queue slow, non‑critical operations

If your app sends emails or generates exports based on large query results, the modern pattern is:

  • Let HTTP endpoints perform only the minimal queries needed to respond quickly.
  • Dispatch a queued job that runs heavier queries and processing in the background.

Laravel’s queue system is built exactly for this, and performance studies show clear improvements when long‑running query work is moved off the main request cycle.

7. Use Dedicated Search Tools Instead of Abusive LIKE Queries

One of the most common “hidden” performance problems is trying to implement full‑text search with massive LIKE "%query%" filters over large tables. This does not scale, and you feel it the moment your project hits real traffic.

Laravel’s answer here is Scout, and more recently its database engine that uses MySQL / PostgreSQL full‑text indexes and LIKE clauses intelligently under the hood. Instead of writing manual search queries everywhere, you let Scout manage the index and provide a fluent API.

Example: using Scout’s database engine

// App\Models\Post
use Laravel\Scout\Searchable;

class Post extends Model
{
    use Searchable;
    public function toSearchableArray(): array
    {
        return [
            'id'    => $this->id,
            'title' => $this->title,
            'body'  => $this->body,
        ];
    }
}

Then in your controller:

$posts = Post::search($request->input('q'))->paginate(20);

The official docs explain how the database engine uses full‑text indexes where available, and how you can further tune search strategies with attributes like SearchUsingFullText and SearchUsingPrefix. This approach gives you much better performance and relevancy than hand‑rolled LIKE queries on large datasets.

For even more advanced use cases (typo tolerance, facets, geo search), Scout supports drivers such as Algolia, Meilisearch, and Typesense, which are widely used in the Laravel ecosystem for high‑traffic search workloads.

8. Measure Everything — Debugbar, Telescope, and Real Metrics

The final trend is cultural rather than purely technical: stop guessing, start measuring. Recent Laravel performance guides emphasize that tools like Laravel Debugbar, Telescope, slow query logs, and profiling should be part of your regular workflow.

Common recommended habits include:

  • Always checking number of queries per request when building new features.
  • Investigating any request that crosses a threshold (for example, more than 20 queries or more than 200 ms) before shipping.
  • Setting up slow query alerts and reviewing them periodically, adding or adjusting indexes as data grows.

Real‑world case studies show dramatic improvements simply by fixing a few problematic endpoints: for example, reducing a routine from 400 queries to 4 by adding two indexes and eager loading the right relationships. In my own client work, the biggest wins usually come from finding and fixing just 3–5 bad queries, not from rewriting whole modules.

Pulling It All Together in a Real Project

Let’s imagine a realistic scenario: you are building an admin dashboard for orders in a Laravel app. Out of the box, you might:

  • Load orders with Order::latest()->paginate(50);
  • In the Blade view, loop and access $order->customer, $order->items, $order->shippingAddress.
  • Add filters for status, date range, and customer email.
  • Offer a CSV export of all paid orders.

If you deploy this without thinking about queries, you will likely face:

  • N+1 queries for customer, items, and shippingAddress.
  • Slow filters due to missing indexes on status, created_at, and email.
  • Exports that try to load the entire result set into memory.

Using the trends we covered, a modern approach would be:

  1. Eager load relationships and counts, and enable strict lazy loading in local/staging.
  2. Add composite indexes for (status, created_at) and customer email based on actual filters.
  3. Stream exports with chunkById in a queued job instead of loading all results.
  4. Cache dashboard aggregates that do not change every second.
  5. Use Telescope/Debugbar in development to verify your query count and execution time.

Following practices recommended in Laravel’s docs and community performance guides, these changes alone can transform a struggling dashboard into one that feels instant even under real data and traffic.

My Final Thoughts (and Your Turn)

Query optimization is not about being clever with micro‑benchmarks. It is about respecting the database, using the tools Laravel gives you, and building habits that catch slow patterns early.

The big trends I see across the Laravel community in 2026 are clear:

  • Treat N+1 as a bug and use strict Eloquent modes.
  • Design indexes around real query patterns.
  • Stream data with chunking and lazy collections.
  • Ask less from the database with smart selects and aggregates.
  • Move heavy work to caches, queues, and dedicated search tools.
  • Measure everything with the right tools instead of guessing.

Usefull resources and Community Links:

If this article helped you, I would love to hear from you.

  • Drop a comment with your favorite Laravel query optimization trick or the worst query you have ever fixed.
  • Give this post a few claps so more artisans can see it.
  • Follow me on Medium at Coder Manjeet for more deep dives into real‑world Laravel problems, and feel free to ping me on X if you want me to review a tricky performance issue.

Let’s build faster, smarter Laravel apps together.


메타데이터
post_id
4dd8043f4d8e
slug
mastering-laravel-query-optimization-4dd8043f4d8e
url
https://towardsdev.com/mastering-laravel-query-optimization-4dd8043f4d8e
canonical_url
https://towardsdev.com/mastering-laravel-query-optimization-4dd8043f4d8e
author_url
https://medium.com/@codermanjeet
status
ok
fetched_at
2026-06-11 05:11:55