我不会起名字322 · 后端 / 算法 / 数据库
文章目录
- [MySQL 8.0 迁移后 SQL 变慢:执行计划里 3 类被忽略的成本差异](#MySQL 8.0 迁移后 SQL 变慢:执行计划里 3 类被忽略的成本差异)
-
- 一、先搞清优化器的成本模型
- [二、差异 1:直方图统计信息(8.0 新增)](#二、差异 1:直方图统计信息(8.0 新增))
- [三、差异 2:索引下推(ICP)行为](#三、差异 2:索引下推(ICP)行为)
- [四、差异 3:eq_range_index_dive_limit 导致的估算分叉](#四、差异 3:eq_range_index_dive_limit 导致的估算分叉)
- [五、用 optimizer_trace 复现优化器决策](#五、用 optimizer_trace 复现优化器决策)
- 六、迁移后的自查清单
- 七、总结
MySQL 8.0 迁移后 SQL 变慢:执行计划里 3 类被忽略的成本差异
"同一条 SQL,5.7 上 20ms,8.0 上 800ms。"
迁移完成后,业务方甩过来这句话。我第一反应是"索引没建"------查了,索引一模一样。第二反应是"统计信息没更新"------跑了 ANALYZE TABLE,没变化。
问题在优化器本身变了。MySQL 8.0 换了成本模型、加了直方图,很多在 5.7 上"碰巧选对"的执行计划,在 8.0 上会"合理地选错"。这篇讲清优化器怎么算成本,再拆 3 类被忽略的差异。
一、先搞清优化器的成本模型
MySQL 优化器选执行计划,本质是在估算每个候选计划的成本,取最小。成本由两部分组成:
总成本 = IO 成本 + CPU 成本
| 成本项 | 含义 | 关键参数 |
|---|---|---|
| IO 成本 | 读页的代价 | innodb_page_size、optimizer_switch |
| CPU 成本 | 处理行的代价 | 每行的比较、函数调用 |
| 统计信息 | 优化器"以为"的数据分布 | 索引统计、直方图 |
核心问题 :优化器不知道真实数据分布 ,它靠统计信息估算。统计信息不准 → 成本估算错 → 选错计划。
sql
-- 看优化器的成本常量
SELECT * FROM mysql.server_cost;
SELECT * FROM mysql.engine_cost;
-- 看一张表的统计信息
SHOW INDEX FROM your_table;
SELECT * FROM mysql.innodb_index_stats WHERE table_name = 'your_table';
SELECT * FROM mysql.innodb_table_stats WHERE table_name = 'your_table';
二、差异 1:直方图统计信息(8.0 新增)
5.7 只有索引的基数统计(cardinality),对没有索引的列一无所知 。8.0 引入了直方图(histogram),可以精确描述列的值分布。
sql
-- 创建直方图(8.0 独有)
ANALYZE TABLE your_table UPDATE HISTOGRAM ON status, city WITH 64 BUCKETS;
-- 查看直方图
SELECT * FROM information_schema.column_statistics
WHERE table_name = 'your_table'\G
为什么会因此变慢 :8.0 有了直方图,优化器对"某列的选择性"判断更精确了 。原先 5.7 因为"不知道分布"而保守地选了索引扫描,8.0 精确计算后发现"全表扫描更便宜",于是改了计划------如果你原有的索引恰好是快的那条路,计划一改就慢了。

修法 :用 EXPLAIN 对比两版计划,如果确认是统计信息导致,要么删掉误导性的直方图 ,要么用 FORCE INDEX 锁定计划 ,要么调 optimizer_switch。
三、差异 2:索引下推(ICP)行为
索引条件下推(Index Condition Pushdown, ICP)在 5.6 引入,但8.0 的默认行为有调整(配合 optimizer switch 和成本模型)。
ICP 的作用:把 WHERE 里能用到索引的条件,下推到存储引擎层过滤,减少回表次数。
sql
-- 看 ICP 是否生效
EXPLAIN SELECT * FROM t WHERE a = 1 AND b LIKE 'x%';
-- Extra 列显示 "Using index condition" 表示 ICP 生效
为什么变慢 :如果 8.0 因为成本估算变化关闭了 ICP,回表次数暴增,性能断崖下跌。
sql
-- 查看 optimizer_switch 里 ICP 的状态
SELECT @@optimizer_switch\G
-- 关注: index_condition_pushdown=on
在 EXPLAIN FORMAT=JSON 里能直接看到:
json
{
"table": {
"access_type": "range",
"attached_condition": "...",
"using_index_condition": true
}
}
四、差异 3:eq_range_index_dive_limit 导致的估算分叉
这是最隐蔽的一个。MySQL 估算范围查询的命中行数有两种方式:
- index dive(索引潜水):真正去索引里数一遍(准确但慢);
- 索引统计估算:用统计信息猜(快但不准)。
由参数 eq_range_index_dive_limit 决定(默认 200):当 IN 列表里的值个数 < 200 时用 index dive,≥ 200 时退化为统计估算。
sql
SHOW VARIABLES LIKE 'eq_range_index_dive_limit';
问题场景 :WHERE id IN (...199 个值...) 会精算,WHERE id IN (...200 个值...) 直接退化成估算,两者计划可能完全不同。迁移时这个默认值如有变化,同样的 SQL 就会选不同计划。
看真实成本:
sql
-- 8.0 独有的 JSON 格式,能看 estimated_cost
EXPLAIN FORMAT=JSON
SELECT * FROM t WHERE id IN (1,2,3,...,200)\G
输出里的 cost_info 会给出 query_cost、read_cost、eval_cost------这是优化器自己算的成本,比传统 EXPLAIN 的信息量大得多。
五、用 optimizer_trace 复现优化器决策
这是最硬核的手段:让优化器把"它怎么想的"完整打印出来。
sql
SET SESSION optimizer_trace = 'enabled=on';
SET SESSION optimizer_trace_max_mem_size = 1000000;
-- 跑你的 SQL
SELECT * FROM t WHERE status = 'active' AND city = 'beijing'\G
-- 读优化器的完整思考过程
SELECT TRACE FROM information_schema.OPTIMIZER_TRACE\G
SET SESSION optimizer_trace = 'enabled=off';
TRACE 里能看到:每个候选索引的估算行数、估算成本、为什么最终选了这个。迁移动荡期,这是唯一能解释"为什么计划变了"的工具。
bash
# 命令行下把 TRACE 存成文件慢慢看
mysql -e "SELECT TRACE FROM information_schema.OPTIMIZER_TRACE\G" > trace.txt
六、迁移后的自查清单
| 检查项 | 命令 | 期望 |
|---|---|---|
| 统计信息是否重建 | ANALYZE TABLE t; |
迁移后必做 |
| 直方图是否需要 | column_statistics |
视情况创建/删除 |
| ICP 是否生效 | EXPLAIN 看 Using index condition |
关键查询应为 on |
| dive_limit 是否一致 | SHOW VARIABLES LIKE 'eq_range%' |
与 5.7 对齐 |
| 计划是否变化 | EXPLAIN FORMAT=JSON 对比 |
逐条差异定位 |
| 成本常量是否调整 | mysql.server_cost / engine_cost |
通常保持默认 |
EXPLAIN 关键列怎么读(可带走物):
| 列 | 关注值 | 含义 |
|---|---|---|
type |
ALL = 全表扫描(危险);ref/range/const 较好 |
访问类型 |
key |
实际用的索引 | NULL = 没用索引 |
rows |
预估扫描行数(越小越好) | 与真实行数差太多=统计不准 |
filtered |
过滤后剩余百分比 | 太低说明索引选择性差 |
Extra |
Using index condition=ICP;Using filesort/Using temporary=有额外开销 |
关键判据 |
七、总结
- 迁移后 SQL 变慢,先怀疑优化器而不是索引(索引通常没变);
- 优化器靠统计信息估算成本,统计变了计划就变;
- 3 类差异:直方图统计 (8.0 新增,可能让计划"合理地变差")、ICP 行为 (回表次数)、
eq_range_index_dive_limit(精算 vs 估算的分叉); - 定位利器是
EXPLAIN FORMAT=JSON(看真实成本)和optimizer_trace(看优化器怎么想的); - 迁移后必跑
ANALYZE TABLE重建统计,再逐条对比关键 SQL 的计划。
留个问题给你 :你们升级 MySQL 8.0 之后,有没有跑过 ANALYZE TABLE 重建统计?有没有遇到过"索引没动、SQL 却变慢"的情况?