☰
MySQL 8.0 迁移后 SQL 变慢:执行计划里 3 类被忽略的成本差异
2026/10/31 1:55:53 网站建设 项目流程

我不会起名字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 关键列怎么读(可带走物):

列关注值含义
typeALL= 全表扫描(危险);ref/range/const较好访问类型
key实际用的索引NULL= 没用索引
rows预估扫描行数(越小越好)与真实行数差太多=统计不准
filtered过滤后剩余百分比太低说明索引选择性差
ExtraUsing index condition=ICP;Using filesort/Using temporary=有额外开销关键判据

七、总结

  1. 迁移后 SQL 变慢,先怀疑优化器而不是索引(索引通常没变);
  2. 优化器靠统计信息估算成本,统计变了计划就变;
  3. 3 类差异:直方图统计(8.0 新增,可能让计划"合理地变差")、ICP 行为(回表次数)、eq_range_index_dive_limit(精算 vs 估算的分叉);
  4. 定位利器是EXPLAIN FORMAT=JSON(看真实成本)和optimizer_trace(看优化器怎么想的);
  5. 迁移后必跑ANALYZE TABLE重建统计,再逐条对比关键 SQL 的计划。

留个问题给你:你们升级 MySQL 8.0 之后,有没有跑过ANALYZE TABLE重建统计?有没有遇到过"索引没动、SQL 却变慢"的情况?

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询