我不会起名字322· 后端 / 算法 / 数据库
📘 技术栈 | 🧩 力扣 Hot100 | 🐹 Go 项目 | 🟥 Redis | 🐬 MySQL
文章目录
- MySQL 8.0 迁移后 SQL 变慢:执行计划里 3 类被忽略的成本差异
- 一、先搞清优化器的成本模型
- 二、差异 1:直方图统计信息(8.0 新增)
- 三、差异 2:索引下推(ICP)行为
- 四、差异 3:eq_range_index_dive_limit 导致的估算分叉
- 五、用 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 成本 | 处理行的代价 | 每行的比较、函数调用 |
| 统计信息 | 优化器"以为"的数据分布 | 索引统计、直方图 |
核心问题:优化器不知道真实数据分布,它靠统计信息估算。统计信息不准 → 成本估算错 → 选错计划。
-- 看优化器的成本常量SELECT*FROMmysql.server_cost;SELECT*FROMmysql.engine_cost;-- 看一张表的统计信息SHOWINDEXFROMyour_table;SELECT*FROMmysql.innodb_index_statsWHEREtable_name='your_table';SELECT*FROMmysql.innodb_table_statsWHEREtable_name='your_table';二、差异 1:直方图统计信息(8.0 新增)
5.7 只有索引的基数统计(cardinality),对没有索引的列一无所知。8.0 引入了直方图(histogram),可以精确描述列的值分布。
-- 创建直方图(8.0 独有)ANALYZETABLEyour_tableUPDATEHISTOGRAMONstatus,cityWITH64BUCKETS;-- 查看直方图SELECT*FROMinformation_schema.column_statisticsWHEREtable_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 里能用到索引的条件,下推到存储引擎层过滤,减少回表次数。
-- 看 ICP 是否生效EXPLAINSELECT*FROMtWHEREa=1ANDbLIKE'x%';-- Extra 列显示 "Using index condition" 表示 ICP 生效为什么变慢:如果 8.0 因为成本估算变化关闭了 ICP,回表次数暴增,性能断崖下跌。
-- 查看 optimizer_switch 里 ICP 的状态SELECT@@optimizer_switch\G-- 关注: index_condition_pushdown=on在EXPLAIN FORMAT=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 时退化为统计估算。
SHOWVARIABLESLIKE'eq_range_index_dive_limit';问题场景:WHERE id IN (…199 个值…)会精算,WHERE id IN (…200 个值…)直接退化成估算,两者计划可能完全不同。迁移时这个默认值如有变化,同样的 SQL 就会选不同计划。
看真实成本:
-- 8.0 独有的 JSON 格式,能看 estimated_costEXPLAINFORMAT=JSONSELECT*FROMtWHEREidIN(1,2,3,...,200)\G输出里的cost_info会给出query_cost、read_cost、eval_cost——这是优化器自己算的成本,比传统 EXPLAIN 的信息量大得多。
五、用 optimizer_trace 复现优化器决策
这是最硬核的手段:让优化器把"它怎么想的"完整打印出来。
SETSESSIONoptimizer_trace='enabled=on';SETSESSIONoptimizer_trace_max_mem_size=1000000;-- 跑你的 SQLSELECT*FROMtWHEREstatus='active'ANDcity='beijing'\G-- 读优化器的完整思考过程SELECTTRACEFROMinformation_schema.OPTIMIZER_TRACE\GSETSESSIONoptimizer_trace='enabled=off';TRACE 里能看到:每个候选索引的估算行数、估算成本、为什么最终选了这个。迁移动荡期,这是唯一能解释"为什么计划变了"的工具。
# 命令行下把 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 却变慢"的情况?