2374 字
约 7 分钟
21
S11-A19 · 统计信息的时效:优化器看到的是什么时候的表
2024-12-31
2026-11-09

一条一直很快的查询突然变慢。SQL 没改、索引没改、数据量只涨了 8%。查下来是优化器选错了执行计划:它以为某个条件能过滤掉九成数据,实际上因为近期数据分布变化,那个条件现在只过滤掉一成。它的判断依据是两周前收集的统计信息。

优化器不是在看你的表,它在看关于你表的一份摘要。这份摘要过期之后,优化器的每个决定都建立在一个不再成立的假设上——而它不会告诉你。


1. 统计信息包含什么

内容 作用
表行数 决定全表扫描还是索引
列的基数(不同值个数) 决定过滤性估计
数据分布(直方图) 决定范围条件的选择性
索引的页数与层级 决定索引扫描成本

还有一个容易忽略的点:统计信息本身也有"采样"这层近似。多数数据库默认采样一部分页而不是全表,所以它给出的不是精确值,而是估计值。当某个值恰好落在采样之外时,估计会错得很离谱,而这类错误在数据倾斜的表上最严重。

第二项与第三项常被混淆:基数告诉你"有多少种值",直方图告诉你"这些值怎么分布"。数据倾斜严重时,只有基数没有直方图,优化器会按均匀分布算,误差可能达一个数量级。

2. 统计信息什么时候失效

触发 机制
数据量变化超过阈值 多数数据库有自动收集门槛
大量删除或更新 分布改变但行数没大变
新增枚举值 基数变化,直方图未更新
分区表新增分区 新分区可能没有统计
批量导入 导入后统计滞后

一个具体的数值感受:如果一列有一千种值、均匀分布,优化器认为每个值命中 0.1% 的行;实际某个值占了 40%,那么按 0.1% 选出的计划会低估扫描量 400 倍。计划不会因此报错,它只是选了另一条路。

第二项是最隐蔽的:行数没变,分布变了。自动收集通常按"变化行数比例"触发,删一半再插一半,比例可能正好低于阈值,统计信息一直不更新,而查询的选择性估计已经完全错了。

3. 自动收集够不够

场景 自动收集 需要手动
平稳写入的常规表 通常够 否
有明显倾斜的列 可能不够 是(调直方图桶数)
大批量导入后 滞后 是(导入后立即收集)
分区表 可能漏分区 是(逐分区确认)
临时表/中间表 常被忽略 是

第四项在分区表上很常见:新分区刚建时没有统计信息,查询落到新分区就按默认估计走,计划可能是全扫。

-- 检查统计信息的最后收集时间(MySQL 8.0)
SELECT table_name, column_name, last_analyzed, histogram
FROM information_schema.column_statistics
WHERE table_name = 'orders';
-- 没有记录 → 该列从未收集过直方图,倾斜列的选择性估计可按均匀分布算

-- 检查表级统计的更新时间
SELECT table_name, update_time, table_rows
FROM information_schema.tables WHERE table_name = 'orders';

4. 手动收集的时机与代价

时机 代价
大批量导入后 收集耗时随表大小增长
显著删除后 需要足够的采样
上线新查询前 一次性成本
定期(如每周) 需要与业务低峰对齐

第一项最应该固定下来:导入任务的成功条件应当包含"统计信息已更新",而不是"数据写完了"。缺这一步,导入后的第一批查询就会跑在旧统计上。

收集成本也要估:大表全量收集可能比导入本身还慢。所以实践中常用增量收集或按分区收集:

-- 按分区收集,避免一次扫全表
ANALYZE TABLE orders PARTITION (p202611) UPDATE HISTOGRAM ON status;
-- 或调大自动收集的触发阈值以外的采样比例(按版本核对参数名)
-- MySQL: innodb_stats_sample_pages;PostgreSQL: default_statistics_target
-- 导入后的标准收尾:收集统计并确认
ANALYZE TABLE orders UPDATE HISTOGRAM ON status, channel WITH 128 BUCKETS;
-- 桶数按倾斜程度定:均匀列 64 够用,倾斜列要更多
-- 注意:收集会占用 IO,大表应放在低峰

5. 稳定性:让计划不随统计漂移

手段 作用 代价
固定执行计划(如 hint、plan baseline) 不随统计变化 需要人工维护
提高采样比例 估计更准 收集更慢
定期重收集 减少漂移 仍有窗口
改写查询避免依赖估计 从根上减少影响 需要设计

还有一种更轻的做法:定期比对执行计划的变化。把关键查询的计划哈希存下来,变了就告警,比固定计划更灵活,也更早发现统计漂移。

// 每日记录关键查询的计划哈希,变化即告警
mysql -N -e "EXPLAIN FORMAT=JSON SELECT ... " | sha256sum >> plan-history.txt
tail -2 plan-history.txt | awk '{print $1}' | uniq | wc -l   # 为 2 说明计划已变

第一种在关键查询上很实用:一条核心链路的 SQL,计划稳定比"可能更优"更重要。但它的维护成本高,每次改 SQL 都要重新评估。

-- 固定计划的两种做法(示意,按具体数据库版本核对语法)
SELECT /*+ INDEX(orders idx_user_status) */ id FROM orders WHERE user_id=? AND status=?;
-- 或使用计划基线(plan baseline)绑定已知良好的计划

6. 排错:五种"计划突然变了"

现象 成因 处理方向
同一条 SQL 突然变慢 统计过期导致估计错误 收集统计后复测
加减一个条件计划大变 基数估计对边界敏感 看直方图是否覆盖该值
新分区查询很慢 分区无统计 逐分区收集
早上快晚上慢 统计在夜间被刷新 对比两个时段的计划
复制库上计划不同 主从统计不一致 从库单独收集

这也解释了另一个常见现象:同一段代码在测试库快、在生产库慢。两边的数据分布完全不同,优化器按各自的统计选计划,而测试库的分布往往过于均匀。

第五项在读写分离架构里很常见:主库统计新、从库统计旧,同一查询在两边选不同计划,于是"从库慢"成了长期未解的问题。

统计信息的另一个使用方常被忽略:索引选择之外的 JOIN 顺序。多表连接时,优化器按各表的行数估计决定谁先谁后;一张表的统计过期,整个连接顺序可能倒过来,性能差几十倍。

7. 边界:什么情况不必关心统计

场景 为什么 做法
极小的表(万行内) 全扫成本可忽略 不关心
纯主键点查 计划几乎固定 不关心
只追加不删除的日志表 分布稳定 依赖自动收集
已有固定计划的查询 不随统计变 定期校验计划未变

判断口径一句话:这条查询的性能是否依赖"优化器对数据分布的估计"。只按主键取值,不依赖;带范围或多条件组合,一定依赖。


统计过期还会带来一个反直觉的后果:同一条 SQL 在某些参数上快、某些参数上慢。因为优化器对不同的参数值给出不同的估计,估计准的那个快,估计错的慢。这类问题在"偶发慢查询"里占相当比例,而只看 SQL 文本永远找不到规律。

8. 自测

  1. 统计信息包含哪四项?"基数"与"直方图"分别回答什么问题?
  2. 为什么"行数没变、分布变了"是最隐蔽的失效情形?自动收集为什么可能不触发?
  3. 分区表上新分区为什么容易出现计划问题?如何确认?
  4. 为什么"导入完成"应当包含"统计已更新"这一步?不包含会发生什么?
  5. 固定执行计划解决了什么问题、带来了什么代价?什么查询值得这么做?
  6. 读写分离场景下,为什么同一查询会在主从选出不同计划?
S11-A19 · 统计信息的时效:优化器看到的是什么时候的表
https://www.clxhxhhr.top/posts/6425/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。