MySQL执行计划稳定性最佳实践:统计信息、直方图与计划监控

举报
数据库小学妹 发表于 2026/09/24 10:06:20 2026/09/24
【摘要】 从一条日结 SQL 一夜之间慢了一百倍切入,拆解执行计划漂移的根因:InnoDB 统计信息靠随机采样、变更越阈值就重算、基数估不准、代价模型据此改选全表扫描。给出直方图、采样页数、重建统计等治本手段与索引提示的止血边界,并附三个避坑经验。

大家好,我是数据库小学妹👋 我踩过的坑,你别再踩。

同一句 SQL,周二跑 50 毫秒,周三跑 5 秒。SQL 我一行没改,表结构没动,数据量也没涨多少。我第一反应是有人偷偷改了数据。翻完两天的变更单和发布记录,什么都没有。后来我把两次 EXPLAIN 并排一放,才发现是数据库自己改了主意。

两天的 EXPLAIN,只差三列

其余列都一样,只有这三列变了。

-- 周二
-- type=ref  key=idx_status  rows=1200
-- 周三
-- type=ALL  key=NULL        rows=2100000

type 从 ref 变成 ALL,说明它放弃索引改成了全表扫。key 变成 NULL,说明它一个索引都没用上。rows 从 1200 涨到 210 万,说明它以为这个条件要命中两百多万行。这张表总共才两百多万行,实际符合条件的只有一万多条。它凭什么觉得这个条件要命中几乎整张表?

它是估的,不是数的

InnoDB 不维护精确的行数,它靠采样。默认配置下,它随机抽 20 个索引页。用这 20 页的分布去推算整张表。20 页,每页 16KB,加起来 320KB。对一张几个 G 的表来说,这就是个小样本。抽样本来就会有偏差,数据一倾斜,偏差更大。status 这列,大部分行挤在同一个值上。落进采样页的比例稍微偏一点,推出来的基数就能差出好几倍。

那为什么是周三才飘,周二不飘?因为统计信息不是每次都重算。InnoDB 会盯着表的变更量。改动超过大约 10% 的行,就自动触发一次重采样。周二到周三那次日结,刚好越过了这个阈值。重采样就是再随机抽一遍页。抽的页不一样,估出来的基数也不一样。表里的数据其实没怎么变,计划却变了。

这里还有个参数值得记住。innodb_stats_persistent_sample_pages 控制采样页数,8.0 默认是 20。页数越小,两次重采样之间的波动就越大,计划也越容易飘。同一句 SQL,统计信息刷一次就换一次计划,根子就在这。

代价是这么算出来的

基数估错,为什么就要换路?因为优化器比的是代价。它手里有两套单价,存在 mysql.server_cost 和 mysql.engine_cost 里。顺序读一页和随机读一页,价差着几倍。

关键在于,回表的代价是按行数乘出来的。它估你要回表 210 万次,就按 210 万次随机读计费。而且不去重,同一页被读一百遍,它就记一百遍的钱。乘下来,代价直接爆表。

再看全表扫。它按表的总页数算钱,顺序读完,每页只读一次。这张表的数据页撑死几万页。几万对两百多万,差了几十倍,它当然选全表扫。它没做错什么。它只是拿了一个不准的估算,做了个诚实的判断。

光看那三列还不够,EXPLAIN 还有个 JSON 格式能翻出账本。

EXPLAIN FORMAT=JSON
SELECT ... FROM orders WHERE status = 3;

输出里每条 table 节点都带着 cost_info。走索引那条会给出回表行数和预估成本,全表扫那条写的是按总页数算的成本。两边的数字一对比,你就能看见它为什么选错。我第一次看到 rows_examined_per_scan 是 210 万的时候,才明白它不是抽风,是算错了。

直方图,给倾斜的列补上分布

MySQL 8.0 开始支持给单列建直方图。这个东西值得记。

-- 给倾斜的列建直方图,32 个桶
ANALYZE TABLE orders UPDATE HISTOGRAM ON status WITH 32 BUCKETS;

-- 看它记了什么
SELECT COLUMN_NAME, HISTOGRAM
FROM information_schema.column_statistics
WHERE TABLE_NAME = 'orders'\G

原来的统计信息只有一个基数,只能告诉优化器这列有 5 个不同的值。它不知道其中某一个值占了八成行。直方图不一样,它把取值范围切成若干个桶,每个桶记区间和频次分布。值集中得厉害时,桶会退化成单值桶,一个桶只装一个值,这时候估得最准。这样 WHERE status = 3 的命中行数就能贴近真实。

它也有边界。只支持单列,多列组合的倾斜管不了。唯一性很高的列建了基本没用,桶里就一个值,等于退化成基数。建完还额外占空间。所以我只给倾斜严重的状态位、类型位建。

先止血,再谈治本

当时业务还等着出报表,我先做了两件事止血。一件是重采样。

ANALYZE TABLE orders;

重采样能立刻刷新统计信息,多数情况下计划就回来了。但它只是这一刻准了,数据再变还可能再飘。另一件是索引提示。在 SQL 里加 USE INDEX 或者 FORCE INDEX,把计划强行掰回原样。它能立刻见效,代价我也写在下面。

手段 治什么 局限
ANALYZE 重建统计信息 采样数据过期 数据再变还会飘
提高采样页数 样本太小 IO 开销跟着变大
直方图 单列数据倾斜 只支持单列,高基数列无效
索引提示 立刻恢复执行计划 治标不治本,索引变更直接报错

避坑清单

大表 ANALYZE TABLE 别在高峰期跑,它会触发索引页采样。你要是把采样页数调大了,IO 压力会更明显。我们现在的做法是把它排进低峰运维窗口,跟备份错开。

FORCE INDEX 千万别当长期方案。它把一个动态的决定,写死在了 SQL 里。索引一旦改名或被删,SQL 直接报错。更麻烦的是,它会把真正的问题盖住。统计信息一直不准下去,谁都不会再去看它。

最后一条我踩得最狠。innodb_stats_persistent 这个参数如果没打开,统计信息就不落盘。重启之后它会重新采样,执行计划可能在重启后整体变样。我们有一次大版本维护之后,第二天一批 SQL 的计划全变了。查了整整一天,最后才定位到这个开关。现在它在我们的基线配置里是必须开的,也只有开了它,前面说的采样页数才管得住。

写在最后

执行计划漂移不是数据库在抽风。它是拿着一个不准的估算,做了一次诚实的决定。所以治它要分两层。

第一层是让估算更准,采样页数和直方图都在这一层。一个治样本量太小,一个治数据倾斜。第二层是让漂移能被发现,把核心 SQL 的执行计划纳入监控,计划一变就告警。第二层我们做得很晚,吃过好几次亏。往往是业务先感知到慢,我们才知道。

别用索引提示把问题压住。 压住的是症状,统计信息该不准还是不准。我现在的顺序是先 ANALYZE 看能不能恢复。恢复了就去查它为什么会飘,是数据倾斜还是采样太少,把根因解决掉。索引提示只留给那些实在来不及治、但又必须马上恢复的核心 SQL。

你遇到过执行计划莫名其妙变差吗?最后是怎么定位的?评论区聊聊。

我是数据库小学妹,帮你少走弯路少踩坑,咱们下篇见👋

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。