"这个查询慢,加个索引吧"——这是数据库调优里最常见,也最容易出错的一句话。加错索引不仅不提速,还会拖慢写入、占用空间,甚至让优化器选错计划。
这份清单是我排查慢查询时实际按序执行的步骤,按顺序走能少走弯路。
先别急着加索引
拿到一条慢 SQL,先确认三件事:
1. 真的慢吗?慢在哪?
sql
-- 开启慢查询日志(线上建议 long_query_time = 1)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
-- 实时看当前在跑什么
SHOW FULL PROCESSLIST;注意区分两种慢:一直慢(通常是索引问题)和偶尔慢(通常是锁等待或资源抖动)。后者加索引毫无用处。
2. 表有多大?
sql
SELECT table_name, table_rows,
ROUND(data_length/1024/1024, 2) AS data_mb,
ROUND(index_length/1024/1024, 2) AS index_mb
FROM information_schema.tables
WHERE table_schema = 'your_db' ORDER BY data_length DESC;几千行的表,全表扫描比走索引还快,加索引纯属自找麻烦。
3. 索引已经有哪些?
sql
SHOW INDEX FROM orders;重复索引是高频问题:已经有了 (a, b),又单独建了 (a),后者纯浪费。
看懂 explain
sql
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 'PAID';只盯四个字段就够用:
| 字段 | 看什么 | 理想值 |
|---|---|---|
type | 访问方式 | ref / range 以上;出现 ALL(全表扫描)就要警惕 |
key | 实际用到的索引 | 不为 NULL;不是你期望的那个就要查原因 |
rows | 预计扫描行数 | 越小越好,和结果集行数接近 |
Extra | 附加信息 | 看到 Using filesort / Using temporary 要处理 |
type 从好到坏:system > const > eq_ref > ref > range > index > ALL。
用 EXPLAIN ANALYZE 看真实数据
MySQL 8.0.18+ 支持 EXPLAIN ANALYZE,它真的执行查询并返回实际耗时和行数。预估值和实际值差很多时,说明统计信息过期了:
sql
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 100;
-- 必要时先更新统计信息
ANALYZE TABLE orders;注意别在生产主库上对大表跑 EXPLAIN ANALYZE,它会真的执行。
最左前缀原则
联合索引 (user_id, status, created_at) 相当于同时拥有:
- ✅
(user_id) - ✅
(user_id, status) - ✅
(user_id, status, created_at)
但不包含 (status) 或 (status, created_at)。跳过最左列,索引直接失效。
sql
-- ❌ 用不上索引:跳过了最左的 user_id
SELECT * FROM orders WHERE status = 'PAID';
-- ✅ 用得上
SELECT * FROM orders WHERE user_id = 100 AND status = 'PAID';常见失效场景
按出现频率排序:
1. 索引列上做了运算或函数
sql
-- ❌ 对列做了函数运算
SELECT * FROM orders WHERE DATE(created_at) = '2026-08-15';
-- ✅ 改成范围查询
SELECT * FROM orders
WHERE created_at >= '2026-08-15 00:00:00'
AND created_at < '2026-08-16 00:00:00';2. 隐式类型转换
sql
-- ❌ phone 是 varchar,传了数字,触发隐式转换,索引失效
SELECT * FROM users WHERE phone = 13800138000;
-- ✅ 类型对齐
SELECT * FROM users WHERE phone = '13800138000';这个坑极其隐蔽,因为 SQL 不报错,只是悄悄变慢。
3. 前导通配符的 LIKE
sql
-- ❌ 走不了索引
SELECT * FROM articles WHERE title LIKE '%索引%';
-- ✅ 可以用索引
SELECT * FROM articles WHERE title LIKE '索引%';
-- 确实需要全文匹配时,用全文索引或 Elasticsearch4. OR 连接了非索引列
sql
-- ❌ remark 没索引,整条退化成全表扫描
SELECT * FROM orders WHERE user_id = 100 OR remark = 'vip';
-- ✅ 拆成 UNION,各自走索引
SELECT * FROM orders WHERE user_id = 100
UNION
SELECT * FROM orders WHERE remark = 'vip';5. 回表太多导致优化器放弃索引
sql
-- ❌ SELECT * 导致大量回表,优化器可能直接选全表扫描
SELECT * FROM orders WHERE status = 'PAID';
-- ✅ 覆盖索引:要的字段都在索引里,无需回表
ALTER TABLE orders ADD INDEX idx_status_time (status, created_at, amount);
SELECT created_at, amount FROM orders WHERE status = 'PAID';
-- Extra 出现 Using index 即为覆盖索引生效分页深翻页
LIMIT 1000000, 20 会先扫描 100 万行再丢弃。改成基于游标的写法:
sql
SELECT * FROM orders WHERE id > :last_id ORDER BY id LIMIT 20;加索引的代价
索引不是免费的,每加一个都要付三笔成本:
- 写入变慢:每次 INSERT / UPDATE / DELETE 都要同步维护索引树
- 占用空间:大表上一个联合索引可能是几百 MB
- 优化器选错:索引越多,优化器判断成本越高,越可能选错计划
所以加索引的原则是:先删重复的、再改不合理的,最后才考虑新增。
sql
-- 查看哪些索引从没被用过(需先开启统计)
SELECT object_schema, object_name, index_name
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL AND count_star = 0
AND object_schema = 'your_db';小结
排查顺序固定下来就不会乱:
- 确认慢查询真实存在 → 2. 看表现有索引 → 3.
EXPLAIN看 type/key/rows/Extra → 4. 用最左前缀和覆盖索引修正 → 5. 清理无效索引
不要凭感觉加索引,让 EXPLAIN 告诉你答案。