MySQL 8.0 迁移后 SQL 变慢:执行计划里 3 类被忽略的成本差异

我不会起名字322 · 后端 / 算法 / 数据库

📘 技术栈 | 🧩 力扣 Hot100 | 🐹 Go 项目 | 🟥 Redis | 🐬 MySQL

文章目录

  • [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=有额外开销 关键判据

七、总结

  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 却变慢"的情况?

相关推荐
Decay__8 小时前
05_MySQL 常用 SQL 大全 —— 诊断、管理、优化辅助 SQL 与三大系统库速查
运维·数据库·sql·mysql·架构
Decay__13 小时前
04_MySQL DBA 日常运维手册 —— 会话管理、锁与阻塞、事务监控、慢查询与日志巡检
运维·数据库·sql·mysql·架构
后台模板学习20 小时前
MongoDB 与 MySQL 在 IM 即时聊天场景中该
数据库·mysql·mongodb
IT大白鼠21 小时前
渗透测试实战从靶场搭建到渗透流程——第 8 篇 · 服务型靶标(上):MySQL 双版本写文件链
数据库·mysql·渗透测试
少年的范儿1 天前
Windows 安装 MySQL 8.4 教程(ZIP 解压版):配置 my.ini、初始化数据库与启动服务
数据库·windows·mysql
神仙别闹1 天前
基于Python+MySQL实现(Web)个人博客系统
python·mysql·php
张小姐的猫1 天前
【MySQL】事务
android·mysql·adb
时间售卖铺1 天前
MySQL事务ACID与隔离级别深度解析
运维·数据库·mysql·云原生
Bushlet1 天前
Mysql建库及建表流程总结
数据库·mysql