MySQL 索引优化实战:从一条慢查询到执行计划

索引优化是一个被讲了无数遍、但每次遇到慢查询仍然要从头排查的话题。这篇文章用一个真实形态的订单表作为例子,走一遍“发现慢查询 → 看执行计划 → 调整索引 → 验证效果”的完整流程,重点是每一步的判断依据。

第一步:找到慢查询

不要凭感觉猜哪条 SQL 慢。先在数据库开启慢查询日志,把阈值调到 0.5 秒,跑一段时间业务流量之后再来分析:

SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.5;
SET GLOBAL log_output = 'TABLE';

随后可以从 mysql.slow_log 中按平均耗时排序,找出最值得优化的几条。需要注意的是,真正影响系统的是“执行次数乘以单次耗时”:一条跑了 5 秒但一天只执行一次的后台报表,优先级远低于一条 80 毫秒但每秒执行上千次的查询。先解决高频的那一类,收益最直接。

第二步:读懂执行计划

EXPLAIN 查看优化器的选择,重点看四列:typekeyrowsExtra

EXPLAIN SELECT id, user_id, amount, created_at
FROM orders
WHERE user_id = 1024 AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20;

type 从好到坏大致是 constrefrangeindexALL,出现 ALL 说明在做全表扫描。key 是实际使用的索引,若为 NULL 则没有走索引,此时要重点怀疑是不是发生了隐式类型转换。rows 是预估扫描行数,它和最终返回行数的差距越大,说明过滤效率越低。Extra 里如果出现 Using filesortUsing temporary,通常意味着需要额外的排序或临时表,是优化的重要信号。

第三步:设计联合索引

对上面的查询,比较合适的索引是 (user_id, status, created_at)。它遵循了联合索引的最左前缀原则:查询条件里的等值列放在前面,范围或排序列放在后面。这样 MySQL 既可以用前两列精确定位,又能利用第三列直接按顺序读取,避免额外的排序操作。

ALTER TABLE orders
  ADD INDEX idx_user_status_time (user_id, status, created_at);

还有一个容易被忽略的点:如果查询只需要少数几列,可以把它们一起放进索引,形成覆盖索引。例如 (user_id, status, created_at, amount),这样回表这一步也省掉了,Extra 中会出现 Using index。代价是索引变大、写入变慢,需要结合写入频率做权衡,不要无脑把所有列都塞进去。

第四步:识别索引失效

最常见的情况是在索引列上做运算或函数调用,例如 WHERE DATE(created_at) = '2026-09-01',优化器无法使用索引,应改写成范围条件,保证列以裸列形式出现在比较的左侧:

WHERE created_at >= '2026-09-01'
  AND created_at <  '2026-09-02'

此外,以百分号开头的模糊匹配、隐式的类型转换(字符串列传数字)、以及 OR 连接的未建索引列,都会让索引失效。还有一种隐蔽情况:联合索引的最左列没有出现在条件里,那么后面的列也用不上索引。

第五步:验证与回归

加完索引不能就结束。要重新执行 EXPLAIN 确认 keyrows 是否改善,并对比优化前后的响应时间。同时留意写入性能与磁盘占用,尤其是大表加索引这个操作本身可能造成锁等待,最好在低峰期使用在线 DDL 工具执行,避免阻塞线上写入。

索引优化没有终点,业务查询在变,数据分布也在变,今天最优的索引过几个月可能就不再被优化器选中。把它当成一次性的调优,不如把它变成一条固定的巡检流程:定期看慢日志,定期回归执行计划,让问题在用户之前被发现。