云选科普

MySQL 执行计划漂移:从统计信息到直方图的排查方法

解析 MySQL 查询计划突然变慢的常见原因,给出统计信息检查、采样调整、直方图和应急索引提示的完整处理路径。

SQL 文本没有变化,响应时间却从毫秒级跌到秒级,问题往往不在“SQL 突然写坏了”,而在优化器对数据分布的判断发生了变化。对 MySQL 8.0 来说,执行计划依赖统计信息与代价模型;当行数估算偏差足够大时,优化器可能放弃索引,改走全表扫描。

这类问题最难排查的地方,是数据库通常没有报错。业务看到的只是延迟升高,监控可能只显示扫描行数、磁盘读取或 CPU 突然增加。因此,定位时应先证明“计划变了”,再判断是统计信息过期、采样不稳定、数据倾斜,还是索引本身已经不适合当前查询。

先确认是不是执行计划漂移

不要只比较 SQL 文本和表结构。把正常时段与异常时段的执行计划、参数值和运行指标放在一起看,重点关注:

  • type 是否从 ref、range 变成 ALL;
  • key 是否从某个索引变成 NULL;
  • rows 与 filtered 的估算是否明显偏离实际结果;
  • 连接顺序、访问方法和回表次数是否发生变化;
  • 同一查询使用的参数值是否属于不同的数据分布区间。

可以先查看静态估算:

EXPLAIN FORMAT=JSON
SELECT id, user_id, amount
FROM orders
WHERE status = 3;

JSON 输出中的 rows_examined_per_scan、rows_produced_per_join 和 cost_info,能帮助判断优化器为何认为某条路径更便宜。若测试环境允许真正执行查询,可进一步使用 EXPLAIN ANALYZE 对比估算行数与实际行数:

EXPLAIN ANALYZE
SELECT id, user_id, amount
FROM orders
WHERE status = 3;

EXPLAIN ANALYZE 会运行语句,不应直接拿高成本查询在生产高峰期试验。更稳妥的做法是先在只读副本、影子环境或带严格资源限制的会话中验证。

为什么统计信息会让优化器改主意

MySQL 的优化器需要估算每条访问路径要读取多少行、执行多少次随机或顺序访问,然后根据代价选择计划。EXPLAIN 里的 rows 是估算值,并不是执行后精确计数。当某个条件实际只返回少量记录,而统计信息却认为它会命中表中很大比例的数据时,索引回表的估算成本可能高于全表扫描,计划就会改变。

InnoDB 的持久化索引统计信息通过抽样索引页来估算基数。MySQL 8.0 中,innodb_stats_persistent_sample_pages 的默认值为 20;增加采样页数通常能提高统计信息的准确性和稳定性,但也会增加 ANALYZE TABLE 的 I/O 和执行时间。这个参数只有在表启用持久化统计信息时才生效。

先检查实际配置,而不是直接套用默认值:

SHOW VARIABLES LIKE 'innodb_stats_persistent%';

SELECT table_name, stat_name, stat_value, sample_size
FROM mysql.innodb_table_stats
WHERE database_name = 'app_db'
  AND table_name = 'orders';

数据倾斜会进一步放大误差。例如状态列只有几个取值,但“待处理”占绝大多数,“已取消”只占很小比例。仅知道不同值的数量,并不足以准确判断某个具体值的选择性。

三类修复手段如何选择

1. 统计信息陈旧:先重新采集

如果数据规模或分布已经明显变化,可以在合适的运维窗口执行:

ANALYZE TABLE orders;

这会更新优化器统计信息,但不保证从此不再漂移。大表执行前应在同规模环境评估耗时和 I/O,避开备份、批处理和流量高峰。如果重新采集后计划恢复,还要继续确认统计信息为何失真,而不是把定时 ANALYZE TABLE 当成唯一方案。

2. 抽样波动较大:提高采样页数

可按表配置采样页数,避免全局修改影响所有业务:

ALTER TABLE orders
  STATS_PERSISTENT = 1,
  STATS_SAMPLE_PAGES = 128;

ANALYZE TABLE orders;

采样页数不是越大越好。应以执行计划稳定性、统计信息准确度和维护窗口成本为依据逐步调整,并观察采集统计时的磁盘负载。

3. 单列分布倾斜:评估直方图

MySQL 8.0 可以用 ANALYZE TABLE ... UPDATE HISTOGRAM 为列生成直方图:

ANALYZE TABLE orders
UPDATE HISTOGRAM ON status WITH 32 BUCKETS;

直方图记录列值的频率分布,适合帮助优化器估算与常量比较的选择性。它按需创建和更新,不会随着每次数据写入自动保持新鲜,因此数据分布显著变化后需要重新评估和更新。

查看直方图:

SELECT SCHEMA_NAME, TABLE_NAME, COLUMN_NAME, HISTOGRAM
FROM information_schema.COLUMN_STATISTICS
WHERE SCHEMA_NAME = 'app_db'
  AND TABLE_NAME = 'orders'
  AND COLUMN_NAME = 'status';

直方图不是所有场景都有效。官方文档指出,当范围优化器或索引下探能够给出更合适的估算时,优化器未必使用直方图;它也不能直接解决多列相关性问题。上线前应使用真实参数集验证计划,而不是只看直方图是否创建成功。

索引提示只适合应急止血

当核心业务正在受影响,FORCE INDEX 或索引级优化器提示可以临时约束访问路径:

SELECT /*+ INDEX(orders idx_status) */ id, user_id, amount
FROM orders
WHERE status = 3;

但提示会把原本动态的选择固化到 SQL 中。数据分布、索引结构或查询条件变化后,被强制的计划可能反而更慢;如果使用传统的 USE INDEX、FORCE INDEX 并删除了对应索引,语句还可能执行失败。MySQL 8.0.20 起提供了 INDEX、JOIN_INDEX 等索引级优化器提示,官方文档也说明这些提示用于替代传统索引提示。

因此,应急流程可以是“提示止血—验证统计信息—修复根因—移除提示—回归测试”,而不是永久保留强制索引。

建立一套可复用的排查顺序

面对计划漂移,可以按以下顺序处理:

  1. 确认慢查询指纹、参数和数据量是否一致;
  2. 对比正常与异常计划,记录访问类型、索引、估算行数和连接顺序;
  3. 用 EXPLAIN ANALYZE 在安全环境比较估算值与实际值;
  4. 检查持久化统计、采样页数、最近统计更新时间及数据分布;
  5. 根据根因选择重新采集、提高采样量或建立直方图;
  6. 只有在恢复时限紧迫时使用索引提示,并设置清理期限;
  7. 将关键 SQL 的计划摘要、扫描行数和延迟纳入持续监控。

对数据库托管实例或自建 MySQL 都一样,升配只能缓解全表扫描带来的资源压力,不能修正错误的基数估算。真正有效的治理,是让统计信息更接近真实分布,并在计划变化影响业务之前发现它。

继续浏览

还想继续看,可以再看这些文章

适合想继续在同一主题下横向阅读、对比不同切入角度的用户。