跳到主要内容

"这个查询慢,加个索引吧"——这是数据库调优里最常见,也最容易出错的一句话。加错索引不仅不提速,还会拖慢写入、占用空间,甚至让优化器选错计划。

这份清单是我排查慢查询时实际按序执行的步骤,按顺序走能少走弯路。

先别急着加索引 ​

拿到一条慢 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 '索引%';

-- 确实需要全文匹配时,用全文索引或 Elasticsearch

4. 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;

加索引的代价 ​

索引不是免费的,每加一个都要付三笔成本:

  1. 写入变慢:每次 INSERT / UPDATE / DELETE 都要同步维护索引树
  2. 占用空间:大表上一个联合索引可能是几百 MB
  3. 优化器选错:索引越多,优化器判断成本越高,越可能选错计划

所以加索引的原则是:先删重复的、再改不合理的,最后才考虑新增。

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';

小结 ​

排查顺序固定下来就不会乱:

  1. 确认慢查询真实存在 → 2. 看表现有索引 → 3. EXPLAIN 看 type/key/rows/Extra → 4. 用最左前缀和覆盖索引修正 → 5. 清理无效索引

不要凭感觉加索引,让 EXPLAIN 告诉你答案。

最后更新于:

本站内容采用 CC BY-NC 4.0 许可