MySQL 直方图实测:无索引倾斜列默认估 10%,建直方图后 batch 从估 19491 行降到 959
在 MySQL 8.0.46 测试库 mysql_hist_lab(自造 orders 20 万行,channel 无索引:web≈90%、batch≈0.5%)上对比:无直方图时 WHERE channel='batch' 的 filtered 固定 10%、估约 19491 行(实际 1000,高估约 19.5×);ANALYZE TABLE … UPDATE HISTOGRAM 后 filtered=0.49、估 959。再插入 4 万行 batch 不刷新直方图会低估约 35×;普通 ANALYZE TABLE 不会更新直方图 last-updated。附只读 SQL、演示脚本与 CSV。
MySQL 8.0.46 实测(测试库、自造数据,2026-10-28):channel 无索引时,优化器对等值过滤默认按 10% 估行——batch 实际 1000 行被估成约 19491(高估约 19.5×),web 实际 179940 却也被估成约 19491(低估约 9.2×)。UPDATE HISTOGRAM ON channel WITH 32 BUCKETS 后,batch 的 filtered 从 10.00 变为 0.49(估 959),web 变为 89.98。直方图过期时(再插入 4 万 batch)估行仍按旧分布,低估约 35×;必须再跑 UPDATE HISTOGRAM,普通 ANALYZE TABLE 改不了 last-updated。
适用对象
- 无直方图时,无索引列上的等值过滤常用默认 10%:本次 batch 估 19491 / 实际 1000,web 估 19491 / 实际 179940。
- ANALYZE TABLE orders UPDATE HISTOGRAM ON channel WITH 32 BUCKETS 后得到 singleton 直方图(4 个桶);batch filtered=0.49、估 959,接近真值。
- 再插入 40000 行 batch 却不刷新直方图:filtered 仍 0.49,估约 1156 / 实际 41000(低估约 35×)。
- 普通 ANALYZE TABLE 会刷新索引统计,但 COLUMN_STATISTICS 里直方图的 last-updated 不变;要改分布必须 UPDATE/DROP HISTOGRAM。
- 有合适索引的单点等值,优化器优先 index dive,直方图几乎不改行数估计——直方图主要救「没索引 / 倾斜过滤」场景。
测试库怎么造:20 万行、channel 无索引、四种取值
表结构只有主键和 customer_id 二级索引,channel 是 VARCHAR(16) 且无索引。用递归 CTE 插入 200000 行:n%200=0 → batch,n%67=0 → api,n%12=0 → app,其余 web。然后 ANALYZE TABLE orders(此时还没有直方图)。
本节判断
- 实例:MySQL 8.0.46,innodb_buffer_pool_size=1GiB,max_connections=151。
- 对比组:同库另一轮给 status 建了索引时,罕见值 status=4 的 index dive 估 1000 / 实际 1000——有索引时优化器不一定靠直方图。
| channel | 行数 | 占比 |
|---|---|---|
| web | 179940 | 89.97% |
| app | 16089 | 8.04% |
| api | 2971 | 1.49% |
| batch | 1000 | 0.50% |
| 合计 | 200000 | 100% |
默认 filtered=10%:罕见值和常见值被估成同一个数
对 WHERE channel = 'batch' 和 WHERE channel = 'web',EXPLAIN 都是 type=ALL、rows≈194908、filtered=10.00。也就是说优化器默认按「筛掉 90%」估,不区分倾斜。
EXPLAIN ANALYZE 把反差写死了:batch 估计 rows=19491,实际 1000(约 0.165–35.7ms 滤完);web 同样估 19491,实际却扫出 179940 行。同一套 10% 假设,一边高估约 19.5×,一边低估约 9.2×。
| 谓词 | filtered | 估计行 | 实际行 | 偏差 |
|---|---|---|---|---|
| channel='batch' | 10.00 | ≈19491 | 1000 | 高估约 19.5× |
| channel='web' | 10.00 | ≈19491 | 179940 | 低估约 9.2× |
UPDATE HISTOGRAM 之后:batch 估 959,web filtered=89.98
执行 ANALYZE TABLE orders UPDATE HISTOGRAM ON channel WITH 32 BUCKETS; 后,information_schema.COLUMN_STATISTICS 出现一行:histogram-type=singleton,buckets_len=4(四种取值各一桶)。
再 EXPLAIN:batch 的 filtered 变成 0.49,估计约 959 行(实际 1000);web 的 filtered 变成 89.98。全表扫描还在(列上仍无索引),但行数估计已经能用了——后面若有 JOIN / 排序 / 临时表,优化器才有机会选对。
本节判断
- 语法是 UPDATE HISTOGRAM … WITH N BUCKETS,不是「跑一次 ANALYZE 就自动有直方图」。
- 桶里的取值在 JSON 里是 base64 编码的端点;可用 FROM_BASE64 解出 api/app/batch/web。
| 谓词 | filtered | 估计行 | 实际行 |
|---|---|---|---|
| channel='batch' | 0.49 | ≈959 | 1000 |
| channel='web' | 89.98 | ≈175k | 179940 |
再插入 4 万行 batch:旧直方图低估约 35×;普通 ANALYZE 救不了
我又插入 40000 行 channel='batch'(不刷新直方图)。真值变成 batch=41000,但 EXPLAIN 仍显示 filtered=0.49,估计大约 1156 行——从「差不多准」变成低估约 35×。
再跑一遍 UPDATE HISTOGRAM 后,filtered=18.85,估计约 36731,重新靠近 41000。关键点:中途只跑 ANALYZE TABLE orders(不加 UPDATE HISTOGRAM)时,COLUMN_STATISTICS 里的 last-updated 时间戳不变。索引统计会动,列直方图不会。
本节判断
- ETL / 大迁移后若过滤列分布变了,要把「刷新直方图」写进变更清单,不要以为 ANALYZE TABLE 已经覆盖。
- 有索引的等值查询仍可能靠 index dive;本次对照实验里 status=4 有索引时估 1000/实际 1000,直方图几乎不改估计——先分清场景再开药。
| 阶段 | filtered | 估计行 | 实际行 |
|---|---|---|---|
| 插入 4 万 batch 后(旧直方图) | 0.49 | ≈1156 | 41000 |
| UPDATE HISTOGRAM 之后 | 18.85 | ≈36731 | 41000 |
生产上怎么用:只读核对 → 低峰 UPDATE HISTOGRAM → 再看 EXPLAIN
下载区的只读 SQL 会列出当前库哪些列已有直方图、last-updated、以及 innodb_index_stats。先确认「有没有图、新不新」,再决定要不要在维护窗口执行 UPDATE HISTOGRAM。
演示脚本会重建 mysql_hist_lab 并打印本文表格里的 EXPLAIN ANALYZE。不要对着生产库跑脚本。
本节判断
- 优先给:无合适索引、值域少、倾斜明显、又经常出现在 WHERE / JOIN 条件里的列。
- 值域极大的列(如 UUID)不适合 singleton;桶数与采样成本要单独评估。
- 行数估计只是输入;最终仍以 EXPLAIN ANALYZE 的 actual rows / 耗时为准。
觉得有用?转给正在做这件事的同事或朋友
参考依据
以下来源用于确认市场趋势、政策背景和术语边界;具体落地方案仍以客户的数据范围、权限和交付目标为准。
常见问题
MySQL 直方图和 ANALYZE TABLE 是一回事吗?
不是。普通 ANALYZE TABLE 刷新的是表/索引统计;列直方图要单独 UPDATE HISTOGRAM 或 DROP HISTOGRAM。本次实测里只跑 ANALYZE TABLE 时,直方图 last-updated 不变。
为什么我给有索引的列建了直方图,EXPLAIN 行数几乎没变?
单点等值且走二级索引时,优化器常用 index dive 估行,不一定读直方图。直方图更常改变「无合适索引」或倾斜过滤时的 filtered。
filtered=10.00 是不是一定错了?
在无直方图时,无索引列等值过滤经常出现默认 10%。本次 batch 实际 0.5%、web 实际 90%,都会先显示 10.00,所以要对照真值或建直方图后再看。
大批量导入后计划变差,要先做什么?
对比 information_schema.COLUMN_STATISTICS 的 last-updated 与导入时间;过期就 UPDATE HISTOGRAM,并再跑 EXPLAIN ANALYZE 看估计行是否回到量级。
这些数字能代表我的生产库吗?
不能。这是 MySQL 8.0.46 测试库和自造 20 万行数据,只说明默认 10%、直方图刷新与过期的机制和量级。生产以你库上的 EXPLAIN ANALYZE 为准。
下一步
推荐动作
你提供慢查询的 EXPLAIN ANALYZE 与 COLUMN_STATISTICS 输出(可脱敏),我们标出行数估计是否失真、该不该上直方图或改写条件;生产变更由你方授权执行。