Resolving MySQL Index Selection Failures in High-Volume Tables

A significant production incident recent occurred involving a database table containing over 80 million records. During peak traffic, slow query logs spiked to 140,000 requests per minute, causing connection exhaustion and system-wide latency. The offending SQL statement followed this pattern:

SELECT 
  * 
FROM 
  user_activity_logs 
WHERE 
  region_id = 42 
  AND category = 12 
ORDER BY 
  id DESC 
LIMIT 1;

Despite the simplicity of the query, execution times reached 40 seconds. Examination of the execution plan revealed that the MySQL optimizer ignored valid composite indexes in favor of the primary key.

Root Cause Identifictaion

The table structure included several indexes intended to optimize this type of lookup:

-- Existing indexes
KEY `idx_region_cat` (`region_id`, `category`),
KEY `idx_composite_extended` (`region_id`, `category`, `created_at`)

When executing EXPLAIN on the query, the output showed the following behavior:

  • possible_keys: idx_region_cat
  • key: PRIMARY
  • rows: 2150
  • Extra: Using where

MySQL's optimizer chose the PRIMARY index because the query includde an ORDER BY id DESC with a LIMIT 1. The optimizer's cost model assumed that since the primary key is already sorted, it could scan the table backwards and find the first matching row faster than using a composite index and then sorting the results in memory.

However, in this specific case, the combination of region_id = 42 and category = 12 yielded zero results for a large portion of the data set. This forced MySQL to perform a massive scan of the primary key, iterating through millions of rows only to find nothing, resulting in severe performance degradation.

Index Selection Logic and Sampling

The optimizer estimates the number of rows (cardinality) using statistics gathered via data page sampling. InnoDB samples $N$ random pages to estimate the distribution of values. If the data distribution is uneven or the sampling is outdated, the estimated rows value in the EXPLAIN output can be significantly lower than the actual effort required.

In this scenario, the optimizer underestimated the cost of the primary key scan and overestimated the cost of using idx_region_cat due to the perceived overhead of filesort.

Mitigation Strategies

Several methods can be employed to rectify this behavior and ensure the optimizer selects the most efficient path.

1. Index Hinting

The most immediate fix is to force the optimizer to use the correct index, bypassing its internal cost calculation.

SELECT 
  * 
FROM 
  user_activity_logs FORCE INDEX(idx_region_cat)
WHERE 
  region_id = 42 
  AND category = 12 
ORDER BY 
  id DESC 
LIMIT 1;

This approach reduces execution time to milliseconds but introduces maintenance overhead, as index renames or deletions in the future would break the application logic.

2. Composite Index with Sorting Column

A more robust solution is to include the sorting column in the composite index. By adding id to the end of the existing index, the optimizer realizes that the composite index can provide the data in the required order without a separate sorting step.

CREATE INDEX idx_region_cat_id ON user_activity_logs (region_id, category, id);

With this index, the optimizer sees a perfectly ordered set for the filter criteria, making it the clear choice over a full primary key scan.

3. Adjusting the LIMIT Threshold

Increasing the LIMIT value can sometimes change the optimizer's cost preference. If the LIMIT is large enough, the cost of scanning the primary key outweighs the cost of using a composite index plus a filesort. However, this is often unreliable and acts more as a side-effect than a deliberate optimization.

4. Subquery Optimization

Wrapping the filtering logic in a subquery can force the evaluation order. By isolating the filtered ID search, you can ensure the composite index is utilized before the final limit is applied.

SELECT 
  * 
FROM 
  user_activity_logs 
WHERE 
  id = (
    SELECT id 
    FROM user_activity_logs 
    WHERE region_id = 42 AND category = 12 
    ORDER BY id DESC 
    LIMIT 1
  );

Tags: MySQL Database Optimization indexing Slow Query Performance Tuning

Posted on Wed, 08 Jul 2026 16:54:42 +0000 by jamiet757