第四章 运行时统计信息收集:基数估计修正、数据倾斜检测、资源使用监控
说实话,很多人在讲自适应查询执行时,都喜欢先聊那些花哨的决策算法。但我个人觉得,如果没有可靠的运行时统计信息,再好的决策引擎也是空中楼阁。你想想看,一个错误的基数估计,能让优化器选出一个差十倍的执行计划。
我在项目中遇到过好几次这样的场景:查询跑着跑着突然变慢,一看执行计划,中间某个算子估算出来只有 1000 行,实际处理了 100 万行。这就是典型的「统计信息失真」问题。所以这一章,我们就来聊聊运行时怎么收集信息,怎么修正偏差,怎么发现数据倾斜,以及怎么监控资源使用。
4.1 基数估计修正:从静态到动态
传统优化器依赖的是统计信息收集任务产出的直方图、频次图。但这些信息有个致命问题——它是静态的。数据在变,查询模式在变,但统计信息可能还是昨天凌晨收集的。
运行时修正的思路其实很简单:让执行引擎在执行过程中,把实际看到的行数反馈给优化器。我习惯把这个过程叫做「反馈回路」。
核心机制:每个算子执行完毕后,将实际输出行数(actual rows)与优化器估算行数(estimated rows)的比值,作为修正因子传递给父算子或全局统计信息缓存。
具体怎么做?我总结了一个三步流程:
- 采集:在每个算子(Scan、Join、Aggregate 等)的输出端埋点,记录实际行数。
- 对比:将实际行数与优化器估算行数做对比,计算偏差比例。
- 传播:将偏差信息沿执行计划树向上传播,修正后续算子的估算。
举个例子,假设一个 Hash Join 的左侧是 Table Scan,优化器估算它会输出 5000 行。实际执行时,Scan 输出了 50000 行。偏差因子就是 10。这个因子会传递给 Hash Join 算子,让它重新估算自己的输出行数,而不是继续用那个错误的 5000。
我的经验:修正因子不要直接乘到估算值上,最好用指数移动平均(EMA)平滑一下。我曾经见过一个场景,数据波动特别大,直接乘法导致优化器反复横跳,反而更慢。
4.2 数据倾斜检测:发现那些「不听话」的数据
数据倾斜是查询性能的隐形杀手。一个 Join 键如果分布不均匀,某些分区可能处理 90% 的数据,其他分区闲得发慌。运行时检测倾斜,核心就是监控每个算子的数据分布特征。
我常用的检测方法有两种:
| 方法 | 原理 | 适用场景 | 开销 |
|---|---|---|---|
| 采样统计 | 对输入数据流进行随机采样,统计各分区/键值的频次 | 数据量较大,需要快速判断 | 低 |
| 精确计数 | 使用 HyperLogLog 或 Count-Min Sketch 等概率数据结构 | 需要精确判断倾斜程度 | 中 |
我个人更推荐第二种。为什么?因为采样统计在数据量小的时候还行,一旦数据量上来,采样率低了容易漏掉倾斜的「大头」,采样率高了又影响性能。而 Count-Min Sketch 这种结构,固定内存开销,误差可控,非常适合运行时场景。
注意:检测到倾斜后,不要立即触发重优化。我曾经犯过这个错误——一检测到倾斜就重新生成执行计划,结果频繁重优化导致查询总时间反而增加了。正确的做法是设置一个阈值,比如偏差超过 3 倍且持续超过 100ms,才触发修正。
检测到倾斜后,常见的处理策略包括:
- 倾斜 Join 的运行时重分区:将热点键值单独提取出来,用 Broadcast Join 处理。
- 动态调整并行度:给热点分区分配更多线程或资源。
- 数据打散:对热点键值添加随机后缀,分散到不同分区后再聚合。
4.3 资源使用监控:别让内存和 CPU 成为瓶颈
基数修正和数据倾斜检测,解决的是「数据特征」问题。但查询执行还受资源约束。你想想看,一个查询就算计划再好,如果内存不够导致频繁 spill,或者 CPU 被其他查询抢走,性能照样上不去。
运行时资源监控,我关注三个核心指标:
- 内存使用率:每个算子实际消耗的内存 vs 分配的内存。
- CPU 时间:算子实际消耗的 CPU 周期,包括用户态和内核态。
- I/O 吞吐:磁盘读写量,尤其是 spill 到磁盘的数据量。
这些指标怎么用?我举个例子。假设一个 Sort 算子,优化器估算它需要 2GB 内存,实际运行时发现它已经用了 1.8GB,而且还在增长。这时候系统可以提前预警,甚至主动切换到外部排序,而不是等到内存溢出才处理。
关键点:资源监控不是事后分析,而是实时反馈。我习惯在每个算子的执行循环中,每隔一定行数(比如 10000 行)检查一次资源使用情况。这样既能及时发现异常,又不会因为频繁检查而影响性能。
4.4 知识体系总览
下面这张图,我把运行时统计信息收集的核心逻辑画了出来。你可以看到,从数据流入到算子执行,再到信息反馈,形成了一个闭环。
4.5 实践中的避坑指南
讲了这么多理论,最后分享几个我在实际项目中踩过的坑。
避坑 1:不要全量收集。我曾经在一个分布式系统中,对每个算子的每行数据都做统计,结果统计信息本身占用了 30% 的 CPU。后来改成采样+周期上报,开销降到了 3% 以下。
避坑 2:注意统计信息的时效性。基数修正信息如果传播太慢,等传到下游算子时,上游的数据特征可能已经变了。我建议设置一个「过期时间」,比如 500ms 后未更新的统计信息直接丢弃,用优化器的原始估算兜底。
避坑 3:资源监控要分层。不要只看全局内存使用率,要细化到每个算子、每个线程。我见过一个案例,全局内存还有 40% 空闲,但某个 Sort 算子已经触发了 spill,因为它的内存池被其他算子挤占了。
好了,这一章的内容就到这里。运行时统计信息收集,说白了就是让执行引擎「睁着眼睛干活」。基数修正解决「看错数据量」的问题,倾斜检测解决「数据分布不均」的问题,资源监控解决「硬件瓶颈」的问题。三者配合,才能让自适应查询执行真正「自适应」起来。