MySQL 5.7 升级 8.0 后索引统计信息不准导致执行计划偏差怎么修复

文章导读
MySQL 5.7 升级 8.0 后执行计划偏差的核心修复手段是手动执行 ANALYZE TABLE 刷新统计信息,并检查 optimizer_switch 配置变更。此方法适用于升级后 EXPLAIN 显示 type 从 ref 退化为 ALL、rows 估算值严重偏离实际的场景,操作前需避开业务高峰以防 MDL 读锁阻塞。
📋 目录
  1. 命令速用版
  2. 为什么会这样
  3. 分步处理
  4. 怎么验证是否生效
  5. 常见坑
  6. 常见问题
  7. 参考来源
A A

MySQL 5.7 升级 8.0 后执行计划偏差的核心修复手段是手动执行 ANALYZE TABLE 刷新统计信息,并检查 optimizer_switch 配置变更。此方法适用于升级后 EXPLAIN 显示 type 从 ref 退化为 ALL、rows 估算值严重偏离实际的场景,操作前需避开业务高峰以防 MDL 读锁阻塞。

先说结论:升级后索引统计信息不准导致执行计划劣化,优先手动刷新统计信息并验证优化器开关配置,而非重建索引或修改 SQL。

  • 先定位:对比升级前后 EXPLAIN 输出,确认 type、rows、Extra 字段是否发生显著变化
  • 先做:对核心表执行 ANALYZE TABLE 刷新统计信息,检查 INFORMATION_SCHEMA.INNODB_TABLESTATS 中 last_update 时间
  • 再验证:重新执行 EXPLAIN 确认执行计划回归,监控慢查询日志观察实际耗时变化

命令速用版

以下命令可直接在 MySQL 8.0 环境中执行,用于快速刷新统计信息并验证效果:

SELECT last_update FROM INFORMATION_SCHEMA.INNODB_TABLESTATS WHERE TABLE_NAME = 'your_table';
ANALYZE TABLE your_table_name;
ANALYZE TABLE your_table_name WITH SYNC;
SHOW INDEX FROM your_table_name;
SELECT @@optimizer_switch;
SET SESSION optimizer_switch='hash_join=off,skip_scan=off';
EXPLAIN FORMAT=JSON SELECT * FROM your_table WHERE condition;

为什么会这样

执行计划偏差的根本原因是 MySQL 8.0 优化器基于新的统计信息采样算法和成本模型做决策,但升级时旧表的统计信息不会自动刷新。

MySQL 5.7 升级到 8.0 后,InnoDB 的表统计信息(INNODB_TABLESTATS)和索引统计信息(INNODB_INDEXSTATS)中的 last_update 时间仍停留在升级前。优化器基于过时的行数估算和索引基数选择执行路径,可能跳过本该使用的索引。同时 8.0 默认启用更多优化器开关(如 hash_join=on、index_merge=on、skip_scan=on),这些新特性在统计信息不准时容易触发误判。

innodb_stats_auto_recalc=ON 只对单次变更超 10% 行数的表触发自动更新,冷表、配置表、历史归档表永远不会自动刷新统计信息,必须手动干预。

分步处理

步骤一:确认统计信息是否陈旧

查询 INNODB_TABLESTATS 检查 last_update 时间是否早于升级时间。若 last_update 早于升级时间点,说明统计信息未更新。

SELECT TABLE_NAME, last_update FROM INFORMATION_SCHEMA.INNODB_TABLESTATS WHERE TABLE_SCHEMA = 'your_database';

交叉验证索引基数:对比 SHOW INDEX 输出的 Cardinality 和实际唯一值数量,差异超过 10 倍基本确认失真。

SHOW INDEX FROM your_table_name;
SELECT COUNT(DISTINCT your_column) FROM your_table_name;

步骤二:手动刷新统计信息

对核心表执行 ANALYZE TABLE,该操作会加 MDL 读锁,大表需避开业务高峰。

ANALYZE TABLE orders;

大表建议加 WITH SYNC 强制同步更新,避免后台异步任务延迟。

ANALYZE TABLE large_table WITH SYNC;

批量处理时过滤 system 库,避免卡住。

MySQL 5.7 升级 8.0 后索引统计信息不准导致执行计划偏差怎么修复
SELECT CONCAT('ANALYZE TABLE ', table_schema, '.', table_name, ';') FROM information_schema.tables WHERE table_schema = 'your_db' AND table_rows > 10000;

步骤三:检查优化器开关配置

对比升级前后 optimizer_switch 输出,重点关注 hash_join、skip_scan、index_merge、derived_merge 等新增开启项。

SELECT @@optimizer_switch;

临时关闭可疑开关验证是否改善执行计划。

SET SESSION optimizer_switch='hash_join=off,skip_scan=off,index_merge=off';

步骤四:针对索引合并问题处理

若 EXPLAIN 输出中出现 using_index_merge,说明 8.0.19+ 自动合并了单列索引。临时绕过方法是 IGNORE INDEX 禁用想合并的单列索引。

SELECT * FROM t WHERE a=1 AND b=2 IGNORE INDEX (idx_a,idx_b) FORCE INDEX (idx_a_b);

根本解决是删除冗余单列索引,减少优化器的错误联想。

怎么验证是否生效

执行 ANALYZE TABLE 后不能只看命令返回 OK,必须检查 Cardinality 是否实际变化。

SHOW INDEX FROM your_table_name;

重新执行 EXPLAIN 对比关键字段:type 是否从 ALL 回归 ref 或 range,rows 估算值是否接近实际返回行数,Extra 列是否不再出现 Using filesort 或 Using temporary。

EXPLAIN SELECT * FROM your_table WHERE condition;

使用 EXPLAIN FORMAT=JSON 查看 optimization_phase 和 chosen_plan 字段,定位具体哪个优化规则被启用或禁用。

EXPLAIN FORMAT=JSON SELECT * FROM your_table WHERE condition;

监控慢查询日志,观察 Handler_read_key 和 Handler_read_rnd 指标变化。若 Handler_read_key 回升、Handler_read_rnd 下降,说明索引访问恢复正常。

SHOW STATUS LIKE 'Handler_read%';

常见坑

ANALYZE TABLE 加 PERSISTENT FOR ALL 语法在 8.0 已废弃

MySQL 8.0 的 ANALYZE TABLE 默认已采集持久化统计信息,加 PERSISTENT FOR ALL 是 5.6 时代遗留语法,执行会报错 ERROR 1064。正确做法是直接运行 ANALYZE TABLE your_table_name。

MySQL 5.7 升级 8.0 后索引统计信息不准导致执行计划偏差怎么修复

information_schema 查询仍显示旧数据

MySQL 8.0+ 默认启用 information_schema 缓存,受 information_schema_stats_expiry 控制,默认值 24 小时。即使刚执行 ANALYZE TABLE,只要缓存未过期,查询仍返回旧值。需先设置 SET GLOBAL information_schema_stats_expiry = 0 禁用缓存。

FORCE INDEX 在 8.0.19+ 可能被忽略

8.0.19 起引入更激进的索引合并优化,当优化器认为多个单列索引组合比强制指定的复合索引更快时,会忽略 FORCE INDEX。这不是 bug,是优化器逻辑变更。需用 IGNORE INDEX 配合使用。

直方图不是万能解药

直方图仅提升 WHERE 单列过滤的基数估算精度,不改索引、不优化 JOIN 或 GROUP BY。若慢查询主因是缺失索引或排序策略变化,建直方图无意义。建之前需确认列分布严重倾斜且当前没有可用索引。

ANALYZE TABLE 不等于重建索引

ANALYZE TABLE 只读取少量数据页更新统计信息,不会整理 B+ 树碎片或回收空间。若需优化存储结构,应使用 OPTIMIZE TABLE,但该操作会锁表且耗时更长。

常见问题

升级后必须对所有表执行 ANALYZE TABLE 吗?

不需要。优先处理被慢查询日志反复抓到、且 EXPLAIN 里 rows 远大于实际结果行数的表。纯写入几乎不查的表(如日志表)可暂缓,JOIN 中的驱动表和 WHERE 条件含高选择性字段的主表必须优先处理。

ANALYZE TABLE 会影响线上业务吗?

会加 MDL 读锁,不阻塞写但会阻塞 DROP TABLE、ALTER TABLE 等 DDL 操作。并发执行另一个 ANALYZE TABLE 也会等锁。大表建议在业务低峰期执行,或使用 WITH SYNC 控制同步更新避免后台异步延迟。

为什么执行 ANALYZE TABLE 后 Cardinality 还是没变?

可能原因包括 innodb_stats_persistent=OFF 导致统计信息只存内存、采样页数不足(innodb_stats_persistent_sample_pages 默认 20)、表被长期锁定或 MVCC 版本链过长导致采样失败。可调高采样页数后重新执行。

optimizer_switch 修改后需要重启 MySQL 吗?

会话级修改(SET SESSION)不需要重启,但连接池复用或应用重启后容易失效。生产环境不建议长期依赖会话级修改,应评估是否需在 my.cnf 里固化配置,或重构 SQL 绕过该路径。

参考来源

  • MySQL 官方文档 - ANALYZE TABLE Statement
  • MySQL 8.0 Release Notes - Optimizer Changes
  • Percona Blog - MySQL 8.0 Query Optimization Changes
  • Oracle MySQL Documentation - InnoDB Persistent Statistics