DEV Community

Timilehin Olusegun
Timilehin Olusegun

Posted on

Laravel Performance Tuning: Optimizing Database Queries for Scalability

Ever worked on a Laravel project where database queries slowed down as traffic increased? I faced this recently while optimizing a real estate platform's backend, and I'll share some lessons I learnt.

Database optimization is one of the key areas in developing scalable and performant applications. Database optimization improves data retrieval, which leads to faster response times and lesser page load time. It can also help to reduce server load, thereby maximizing cost savings.

The Real Estate Platform Challenge

Picture this: You've built a beautiful real estate platform that serves multiple cities with advanced search filters. The property listings load quickly, search filters respond instantly, and everything seems perfect. Then, as your application scales and the user base grows, those queries that performed perfectly in development start taking longer and longer to execute. Sound familiar?

That's exactly what happened with our platform. Our wake-up call came through Sentry alerts flagging slow database queries in our production environment. The monitoring revealed search result queries taking upwards of 5 seconds to complete—not exactly the snappy experience we'd promised!

Common Query Pitfalls (And How to Avoid Them)

1. The N+1 Query Problem: Your Database's Secret Enemy

Remember playing those video games where defeating one enemy spawns multiple smaller ones? That's essentially what the N+1 query problem is like in Laravel. You fetch a list of properties, and then for each property, you make additional queries to fetch related data. Before you know it, your database is handling hundreds of queries instead of just one.

Here's what it typically looks like:

// Before optimization
$properties = Property::all();
foreach ($properties as $property) {
    echo $property->agent->name;  // This triggers a new query for each property
}

// After optimization
// Eager Loading with `with()`
$properties = Property::with(['agent'])->get();
foreach ($properties as $property) {
    echo $property->agent->name;  // No additional queries needed!
}
Enter fullscreen mode Exit fullscreen mode

2. The Art of Database Indexing

Think of database indexes like the index of a book—they help you find what you're looking for without scanning every page. But there's much more to indexing than just throwing an index on every column. Let's dive deeper.

Understanding Different Types of Indexes

// Basic single-column index
Schema::table('properties', function (Blueprint $table) {
    $table->index('price');
});

// Composite index for multiple columns
Schema::table('properties', function (Blueprint $table) {
    $table->index(['city', 'price']); // Order matters!
});

// Unique index
Schema::table('properties', function (Blueprint $table) {
    $table->unique('property_code');
});
Enter fullscreen mode Exit fullscreen mode

Index Strategy Best Practices

  1. Column Order in Composite Indexes Matters
   // Good: Matches query pattern
   $properties = Property::where('city', 'New York')
                        ->whereBetween('price', [200000, 500000])
                        ->get();

   // Index should match this pattern
   $table->index(['city', 'price']); // City first, then price
Enter fullscreen mode Exit fullscreen mode
  1. Selective Indexing
    Not every column needs an index. Some of the guidelines to choosing what columns to index are:

    • Index columns frequently used in WHERE clauses and ORDER BY statements
    • Index foreign key columns
    • Don't index columns with low selectivity (like boolean flags)
  2. Monitoring Index Usage

   -- MySQL query to check index usage
   SELECT
       table_name,
       index_name,
       index_type,
       stat_name,
       stat_value
   FROM mysql.index_statistics
   WHERE table_name = 'properties';
Enter fullscreen mode Exit fullscreen mode

3. Select What You Need, Not Everything

One of the most common mistakes I've seen (and made!) is using select * by default. It's like going grocery shopping and buying the entire store when you only need ingredients for one meal. Here's a better approach:

// Instead of
$properties = Property::all();

// Be specific
$properties = Property::select(['id', 'title', 'price', 'city'])
    ->where('status', 'active')
    ->get();
Enter fullscreen mode Exit fullscreen mode

4. Chunk Processing for Large Datasets

When dealing with large datasets, processing everything in a single operation can overwhelm your system's resources and create bottlenecks. Instead, use Laravel's chunk method to process records in manageable batches:

Property::chunk(100, function ($properties) {
    foreach ($properties as $property) {
        // Process each property
    }
});
Enter fullscreen mode Exit fullscreen mode

5. Caching Strategies That Actually Work

Caching is like having a good assistant who remembers everything. But like any assistant, it needs clear instructions.

// Cache frequently accessed data
$properties = Cache::remember('featured_properties', 3600, function () {
    return Property::with('images')
        ->where('featured', true)
        ->get();
});
Enter fullscreen mode Exit fullscreen mode

Pro tip: Don't cache everything! Focus on:

  • Data that's frequently accessed
  • Data that's expensive to compute
  • Data that doesn't change often

Best Practices to Take Away

  1. Monitor First, Optimize Later
    Don't fall into the trap of premature optimization. Use Laravel's built-in query log or tools like Telescope to identify actual bottlenecks.

  2. Think in Sets, Not Loops
    Whenever you find yourself writing a foreach loop that queries the database, step back and ask if there's a way to handle it in a single query.

  3. Cache Strategically
    Not everything needs to be cached. Focus on frequently accessed, computationally expensive queries that don't require real-time accuracy.

  4. Index Thoughtfully
    Think of indexes as a book's table of contents—you want enough detail to find things quickly, but not so much that the table of contents becomes longer than the book itself.

Common Pitfalls to Avoid

  • Don't "eager load" relationships you don't need
  • Avoid running queries in loops (the sneaky N+1 problem in disguise)
  • Don't cache everything—sometimes the overhead of cache management outweighs the benefits
  • Be careful with orderBy on non-indexed columns in large datasets
  • Don't create indexes for columns that are rarely used in WHERE clauses
  • Avoid updating indexed columns frequently; each update requires index maintenance

Conclusion: It's a Journey, Not a Destination

Database optimization isn't a one-time task you can check off your list. It's more like tending a garden—regular maintenance and attention yield the best results. Start with these basics, monitor your application's performance, and keep refining your approach.

Remember, the goal isn't to implement every optimization technique you know. It's about finding the right balance between code maintainability and performance for your specific use case. Sometimes, a simple eager loading statement can do more for your application's performance than hours spent on complex optimization strategies.

What optimization challenges have you faced and/or solved in your Laravel projects? Let’s discuss in the comments!

Top comments (0)