3.8 KiB
title, description, tags
| title | description | tags |
|---|---|---|
| Query Optimization Pitfalls | Common anti-patterns that silently kill performance | mysql, query-optimization, anti-patterns, performance, indexes |
Query Optimization Pitfalls
These patterns look correct but bypass indexes or cause full scans.
Non-Sargable Predicates
A sargable predicate can use an index. Common non-sargable patterns:
- functions/arithmetic on indexed columns
- implicit type conversions
- leading wildcards (
LIKE '%x') - some negations (
!=,NOT IN,NOT LIKE) depending on shape/data
Functions on Indexed Columns
-- BAD: function prevents index use on created_at
WHERE YEAR(created_at) = 2024
-- GOOD: sargable range
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'
MySQL 8.0+ can use expression (functional) indexes for some cases:
CREATE INDEX idx_users_upper_name ON users ((UPPER(name)));
-- Now this can use idx_users_upper_name:
WHERE UPPER(name) = 'SMITH'
Implicit Type Conversions
Implicit casts can make indexes unusable:
-- If phone is VARCHAR, this may force CAST(phone AS UNSIGNED) and scan
WHERE phone = 1234567890
-- Better: match the column type
WHERE phone = '1234567890'
LIKE Patterns
-- BAD: leading wildcard cannot use a B-Tree index
WHERE name LIKE '%smith'
WHERE name LIKE '%smith%'
-- GOOD: prefix match can use an index
WHERE name LIKE 'smith%'
For suffix search, consider storing a reversed generated column + prefix search:
ALTER TABLE users
ADD COLUMN name_reversed VARCHAR(255) AS (REVERSE(name)) STORED,
ADD INDEX idx_users_name_reversed (name_reversed);
WHERE name_reversed LIKE CONCAT(REVERSE('smith'), '%');
For infix search at scale, use FULLTEXT (when appropriate) or a dedicated search engine.
OR Across Different Columns
OR across different columns often prevents efficient index use.
-- Often suboptimal
WHERE status = 'active' OR region = 'us-east'
-- Often better: two indexed queries
SELECT * FROM orders WHERE status = 'active'
UNION ALL
SELECT * FROM orders WHERE region = 'us-east';
MySQL can sometimes use index_merge, but it's frequently slower than a purpose-built composite index or a UNION rewrite.
ORDER BY + LIMIT Without an Index
LIMIT does not automatically make sorting cheap. If no index supports the order, MySQL may sort many rows (Using filesort) and then apply LIMIT.
-- Needs an index on created_at (or it will filesort)
SELECT * FROM orders ORDER BY created_at DESC LIMIT 10;
-- For WHERE + ORDER BY, you usually need a composite index:
-- (status, created_at DESC)
SELECT * FROM orders
WHERE status = 'pending'
ORDER BY created_at DESC
LIMIT 10;
DISTINCT / GROUP BY
DISTINCT and GROUP BY can trigger temp tables and sorts (Using temporary, Using filesort) when indexes don't match.
-- Often improved by an index on (status)
SELECT DISTINCT status FROM orders;
-- Often improved by an index on (status)
SELECT status, COUNT(*) FROM orders GROUP BY status;
Derived Tables / CTE Materialization
Derived tables and CTEs may be materialized into temporary tables, which can be slower than a flattened query. If performance is surprising, check EXPLAIN and consider rewriting the query or adding supporting indexes.
Other Quick Rules
OFFSETpagination:OFFSET Nscans and discards N rows. Use cursor-based pagination.SELECT *defeats covering indexes. Select only needed columns.NOT INwith NULLs:NOT IN (subquery)returns no rows if subquery contains any NULL. UseNOT EXISTS.COUNT(*)vsCOUNT(col):COUNT(*)counts all rows;COUNT(col)skips NULLs.- Arithmetic on indexed columns:
WHERE price * 1.1 > 100prevents index use. Rewrite to keep the column bare:WHERE price > 100 / 1.1.