外观
一文搞懂 MySQL EXPLAIN 深读:执行计划没错,SQL 为什么还是慢?
「EXPLAIN 我看过了:type 是 ref,key 也用上了索引,rows 才几千,为什么线上这条 SQL 还是跑 5 秒?」这是数据库技术群里出现频率最高的问题之一。答案往往让人意外:执行计划「看起来没错」和执行计划「就是真实执行的路径」,中间隔着统计信息、隐式转换、优化器成本估算与并发环境好几道坎。EXPLAIN 本质上是优化器基于统计信息给出的预估快照,它只回答「我打算怎么执行」,并不保证「我实际就是这么执行的」。这篇文章以一张千万级订单表的真实排查过程为例,先讲清楚执行计划各字段应该怎么读,再拆解计划与实际执行路径产生偏差的四大根因,最后给出一套可以照抄的三步排查流程。
先把执行计划读对:type/key/rows/Extra 各管什么
先看示例表结构与目标查询,后文的排查全部基于这张表展开:
sql
-- 示例表:千万级订单表,status 取值极度不均衡(已完成约 98%)
CREATE TABLE order_info (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
order_no VARCHAR(32) NOT NULL COMMENT '业务订单号',
user_id BIGINT UNSIGNED NOT NULL COMMENT '下单用户',
status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0待支付 1已完成 2已取消',
amount DECIMAL(10, 2) NOT NULL COMMENT '订单金额',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id),
UNIQUE KEY uk_order_no (order_no),
KEY idx_status_create (status, create_time),
KEY idx_user (user_id)
) ENGINE = InnoDB COMMENT '订单表';
-- 典型慢查询:已完成订单按时间倒序取最新 20 条
EXPLAIN
SELECT id, order_no, user_id, amount
FROM order_info
WHERE status = 1
AND create_time >= '2026-09-01'
ORDER BY create_time DESC
LIMIT 20;逐列读法可以这样记:type 是访问类型,从好到差大致是 system > const > eq_ref > ref > range > index > ALL,这条查询走 range 就算合格;key 是最终选中的索引,possible_keys 只是候选名单,两者不一致时才值得深挖;key_len 能反推联合索引实际用到了前几列,结合列类型与可空性可以手算;rows 是预估扫描行数,注意它是采样估算而非精确值,filtered 表示再经过其余条件过滤后预计剩余的百分比,两者相乘才是有效行数的预估;Extra 是隐藏信息最丰富的一列——Using index 表示覆盖索引免回表,Using filesort 表示排序无法靠索引完成,Using temporary 出现通常意味着有去重或派生表物化,Using index condition 表示用上了索引条件下推。
执行计划为什么会骗人:四类偏差根因
第一类是统计信息过期。InnoDB 的统计信息来自对少量随机数据页的采样(由 innodb_stats_persistent_sample_pages 控制,默认 20 页),大批量写入、删除或数据分布倾斜之后如果不重新收集,rows 预估可能差出几个数量级,优化器据此选错索引甚至选错 join 顺序。
第二类是隐式类型转换。字符串列与数字比较时,MySQL 会把列整体转成数字再比较,索引直接失效;JOIN 两侧字符集或排序规则不一致时,其中一侧的索引也用不上。这类问题执行计划里只表现为 type=ALL,不看建表语句很难发现。
第三类是索引列被函数或运算包裹。WHERE DATE(create_time) = '2026-09-01' 这类写法会让优化器放弃该列的索引,改写成左右开区间的范围条件即可。
第四类是优化器成本误判。成本模型基于随机读与顺序读的估算常数,感知不到缓存热度与数据倾斜,偶尔会选出一个「理论便宜、实际昂贵」的路径——这正是 FORCE INDEX 与 optimizer_trace 存在的意义。
这四类问题有个共同点:都发生在「计划生成」与「计划执行」之间的缝隙里。所以排查思路也是同构的——先让数据库把真实执行过程交出来(EXPLAIN ANALYZE、Handler 计数),再反向推断缝隙里发生了什么,比反复盯着预估计划猜测要高效得多。
实操:三步验证真实执行路径
第一步,用 EXPLAIN ANALYZE 拿真实数据。 8.0.18 起可用,它会真正执行查询并回传每一步的实际耗时与实际行数:
sql
-- EXPLAIN 只给预估,EXPLAIN ANALYZE 会真正执行并回传每一步的 actual time 与 actual rows
EXPLAIN ANALYZE
SELECT id, order_no, user_id, amount
FROM order_info
WHERE status = 1
AND create_time >= '2026-09-01'
ORDER BY create_time DESC
LIMIT 20;重点看 actual rows 与预估 rows 的偏差:预估 3 万、实际 300 万,基本可以断定统计信息出了问题;再看各算子的 actual time 是骤增还是平摊,瓶颈算子一眼可辨。
第二步,刷新统计信息后重新对比。 很多「玄学慢」到这一步就解决了:
sql
-- 刷新统计信息后再对比执行计划是否变化
ANALYZE TABLE order_info;
-- 采样页不足时适当调大,让估算更接近真实分布(代价是 ANALYZE 执行更耗时)
SET GLOBAL innodb_stats_persistent_sample_pages = 64;第三步,用 Handler 计数器看真实访问量。 计划说了不算,引擎的访问计数才是实况:
sql
-- 清空会话级状态计数器 → 执行目标 SQL → 读取 Handler 计数
FLUSH STATUS;
SELECT id, order_no FROM order_info
WHERE status = 1 AND create_time >= '2026-09-01'
ORDER BY create_time DESC LIMIT 20;
SHOW SESSION STATUS LIKE 'Handler_read%';Handler_read_next 表示沿索引读取下一条记录的次数。如果 LIMIT 20 却产生了几十万次 Handler_read_next,说明过滤或排序并没有像计划展示的那样高效收口,真实路径已经与计划背离。怀疑优化器选错时,再补一个追踪:
sql
-- 打开优化器追踪,查看各候选索引与 join 顺序的成本评估明细
SET optimizer_trace = 'enabled=on';
SELECT id FROM order_info WHERE status = 1 AND create_time >= '2026-09-01' LIMIT 1;
SELECT TRACE FROM information_schema.OPTIMIZER_TRACE\G
SET optimizer_trace = 'enabled=off';trace 输出会列出每个 possible_keys 的估算成本与最终选择理由,chosen=false 的原因一目了然,是区分「优化器误判」还是「统计信息失真」的最直接证据。
避坑总结
- 永远不要只看 EXPLAIN 就下结论。 它只是预估快照,8.0.18 及以上优先用 EXPLAIN ANALYZE 拿实际行数与耗时,低版本用 FLUSH STATUS 加 Handler 计数器兜底,两条证据链互相印证。
- 排查隐式转换先对齐两侧类型与字符集。 字符串列与数字比较、JOIN 两侧字符集或排序规则不一致,都会让索引静默失效;把「等值比较两侧类型一致」纳入代码评审清单,比事后排查便宜得多。
- 统计信息要纳入日常巡检。 大批量导入、删除或跑完归档任务后主动 ANALYZE TABLE;数据倾斜严重的表适当调大采样页,别让优化器拿 20 页的样本去推断千万行的表。
- FORCE INDEX 是止血手段不是长期方案。 它锁死了访问路径,数据分布变化后可能反而更差;先看 trace 找到误判原因,再决定是改写 SQL、补索引还是修统计信息。
- 高度警惕枚举列的数据倾斜。 status 这类取值占比极端的列(如 98% 已完成),不同取值下的最优计划完全不同,必要时按业务语义拆分查询或绑定执行计划。
写在最后
执行计划是优化器写给人类的「意图说明」,而 EXPLAIN ANALYZE 与 Handler 计数器才是引擎交出的「执行回执」。把「读计划 → 对比实际 → 修复统计 → 定位误判」这套流程固化成团队习惯,绝大多数「计划没错但就是慢」的问题,都能在十分钟内水落石出。
免责声明
本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。