慢查询优化最容易走偏的地方,是一上来就说“给字段都加索引”。索引确实重要,但索引不是越多越好,也不是把 where 里的字段逐个建单列索引就完事。
更稳的顺序是:先确认慢在哪里,再看扫描了多少行,然后决定是调整索引、改写 SQL,还是回到业务口径重新设计查询。
第一步:先确认它真的慢
拿到一条慢 SQL,不要只凭感觉判断。先确认几个事实:
- 查询耗时是否稳定复现。
- 是否命中了缓存。
- 参数范围是否接近线上真实情况。
- 返回行数是否合理。
排查时可以临时使用 SQL_NO_CACHE 观察真实查询成本:
select SQL_NO_CACHE count(*)
from task
where status = 2
and operator_id = 20839
and operate_time > 1371169729
and operate_time < 1371174603
and type = 2;
如果只是在测试库拿很小数据量跑,结论往往没有参考价值。慢查询优化一定要结合数据规模、字段分布和真实访问方式。
第二步:用 Explain 看扫描范围
explain 里字段很多,日常先盯这几个:
type:访问类型,是否从全表扫描降到了范围、ref、const 等。key:实际使用了哪个索引。rows:预估扫描行数。Extra:是否出现临时表、文件排序、回表等提示。
慢查询的核心经常不是“有没有索引”,而是“这个索引能不能把扫描行数压下来”。
如果 rows 很大,就要回到 where 条件看字段区分度。状态、类型这类字段单独建索引未必有用,因为它们可能只能把数据从 100% 缩到 50% 或 20%,仍然要扫很多行。
第三步:联合索引顺序要服务查询
假设 SQL 是:
select count(*)
from task
where status = 2
and operator_id = 20839
and operate_time > 1371169729
and operate_time < 1371174603
and type = 2;
这里通常会考虑联合索引,而不是给每个字段都建一个单列索引。一个可能的方向是:
create index idx_task_query
on task(operator_id, status, type, operate_time);
为什么把 operate_time 放后面?因为范围条件会影响后续列继续利用索引。等值条件可以更灵活地让优化器调整,而范围条件通常更适合放在联合索引靠后的位置。
但索引顺序不能只看这一条 SQL。还要把同一张表的高频查询一起拿出来,判断能否通过一个联合索引覆盖多个场景,避免为了单条 SQL 建一堆互相重叠的索引。
第四步:SQL 写法也会决定执行成本
有些 SQL 加索引也救不回来,因为写法本身让数据库做了大量无效工作。
常见问题包括:
- 在索引列上套函数,例如
date(create_time) = '2026-07-02'。 - 复杂子查询先生成很大的中间结果,再和小范围数据关联。
or条件跨不同字段或不同业务语义,导致索引使用困难。order by limit没有合适索引,只能大范围排序。
例如时间条件应该尽量写成范围:
where create_time >= '2026-07-02 00:00:00'
and create_time < '2026-07-03 00:00:00'
而不是:
where date(create_time) = '2026-07-02'
后者会让每一行都先计算函数结果,再比较,索引很难正常发挥。
第五步:优化前后要对比结果
慢查询优化不能只看耗时,也要确认结果一致。
建议记录:
- 原 SQL 和改写 SQL。
- 原执行计划和新执行计划。
- 样例参数下的结果行数是否一致。
- 核心指标:耗时、扫描行数、是否排序、是否临时表。
- 新增索引对写入和存储的影响。
如果是线上高频 SQL,最好先灰度或在从库验证。索引会提升查询,也会增加写入和维护成本,尤其是大表加索引还涉及 DDL 风险。
实用结论
MySQL 慢查询优化的主线很清楚:先复现,再 Explain,再看扫描行数,最后决定索引、SQL 改写或业务口径调整。
索引设计不要只盯单条 SQL,也不要把 where 字段都建成单列索引。真正有价值的是理解字段区分度、最左匹配、范围条件位置,以及这张表整体的查询模式。这样做出来的优化才不容易今天救了一条 SQL,明天拖慢一批写入。




