add brain
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
---
|
||||
title: Query Optimization Pitfalls
|
||||
description: Common anti-patterns that silently kill performance
|
||||
tags: 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
|
||||
```sql
|
||||
-- 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:
|
||||
|
||||
```sql
|
||||
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:
|
||||
|
||||
```sql
|
||||
-- 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
|
||||
```sql
|
||||
-- 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:
|
||||
|
||||
```sql
|
||||
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.
|
||||
|
||||
```sql
|
||||
-- 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.
|
||||
|
||||
```sql
|
||||
-- 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.
|
||||
|
||||
```sql
|
||||
-- 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
|
||||
- **`OFFSET` pagination**: `OFFSET N` scans and discards N rows. Use cursor-based pagination.
|
||||
- **`SELECT *`** defeats covering indexes. Select only needed columns.
|
||||
- **`NOT IN` with NULLs**: `NOT IN (subquery)` returns no rows if subquery contains any NULL. Use `NOT EXISTS`.
|
||||
- **`COUNT(*)` vs `COUNT(col)`**: `COUNT(*)` counts all rows; `COUNT(col)` skips NULLs.
|
||||
- **Arithmetic on indexed columns**: `WHERE price * 1.1 > 100` prevents index use. Rewrite to keep the column bare: `WHERE price > 100 / 1.1`.
|
||||
Reference in New Issue
Block a user