一篇 31 年前的数据库论文,让 DuckDB 快了 622 倍
DuckDB 的查询优化器刚刚合并了一个 PR,把一类查询从 1.86 秒压到了 0.003 秒。
靠的不是新硬件,也不是并行化——是一篇 1995 年的 VLDB 论文。
PR #23506 给 PartialAggregatePushdown 优化器加上了 Eager Aggregation(提前聚合)。一句话:把 GROUP BY 推到 JOIN 前面去做。
● ● ●
为什么 JOIN 之后 GROUP BY 是蠢的?
假设你有两张事实表 fact_a(15 万行)和 fact_b(15 万行),通过一张维度表 dim(2000 行)做星型连接,最后按维度属性分组统计。
你写出来的 SQL 大概长这样:
SELECT d.attr, count(*), avg(fa.val), avg(fb.val) FROM fact_a fa JOIN dim d ON fa.key = d.key JOIN fact_b fb ON d.key = fb.key GROUP BY d.attr
数据库的默认执行计划:先把三张表 JOIN 起来,产生一个巨大的中间结果,然后在这个中间结果上做 GROUP BY。在这个例子中,JOIN 产生的中间行数是 4.43 亿行。然后你再从 4.43 亿行里压出几十行结果。
Eager aggregation 说:为什么不先在每张事实表上按 JOIN key 做一次预聚合,把行数压到几千行,再做 JOIN?
这就是 1995 年 Yan & Larson 在 VLDB 上发表的 "Eager Aggregation and Lazy Aggregation" 的核心思想。31 年后,DuckDB 把它装进了自己的优化器。
● ● ●
两种策略,覆盖不同场景
PR #23506 实现了两种 eager aggregation 策略:
策略一:单侧下推(one-sided pushdown)。把聚合的中间状态导出到 JOIN 的一侧输入,在 JOIN 之前就先做部分聚合,然后在 JOIN 之上用 combine/finalize 合并回来。适用于任何可以导出中间状态的聚合函数。
策略二:双侧下推(double-eager)。对于 fact×fact 的 JOIN,两张事实表都在 JOIN key 上做预聚合,然后在上层根据每个 key 的部分结果和行数重建最终度量值。支持的聚合函数包括 sum、count、min、max,avg 作为 sum/count 处理。
● ● ●
622 倍的 benchmark 从哪来?
PR 作者构造了一个 "自我放大" 的 fact×fact JOIN 场景:
fact_a150K 行,fact_b150K 行,通过dim(2000 行,唯一键)连接- Zipf 分布让热点 key 集中,JOIN 的笛卡尔积膨胀到 443,270,469 行
- 查询:
count(*)+avg,按维度属性分组
结果:
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 查询时间 | 1.866 秒 | 0.003 秒 | **~622×** |
优化前的计划把整个 4.43 亿行的交叉乘积算了出来。优化后的计划在每张事实表上先按 key 做预聚合,行数降到几千,JOIN 轻如鸿毛。
count(*) 完全精确;avg 的浮点累加重排序产生了 ~3e-9 的相对误差——这在浮点聚合里是正常且可预期的。
● ● ●
TPC-DS SF100:3 胜 0 负
在标准的 TPC-DS SF100 基准上,19 个查询的计划被改写。结果:
| 查询 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| Q4 | 2.220s | 1.146s | **1.94×** |
| Q74 | 1.175s | 0.735s | **1.60×** |
| Q36 | 0.341s | 0.242s | **1.41×** |
| Q70 | 0.314s | 0.278s | 1.13× |
其余 15 个查询保持不变或落在噪声范围内。零退化,零结果差异。
TPCH 完全不受影响——这个优化器改动只对特定查询模式生效,标准星型模型 workload 完全不受干扰。
● ● ●
不用担心跑偏
DuckDB 不是无脑下推。这个重写塞了好几道保险:
代价门控。只在高扇出 JOIN(JOIN 后行数暴增)和聚合侧会塌缩的 JOIN 上触发。选择性 JOIN 完全不碰。
正确性保护。确定性求和(kahan_sum)永远不会被下推,避免浮点运算重排序。精确类型分解中可能溢出的路径直接排除。
另外,唯一受影响的是浮点 sum——但浮点 GROUP BY 本来就是顺序相关的,所以这不是新引入的不确定性,只是把已有的不确定性挪了个位置。
● ● ●
什么时候能用上?
这个优化默认开启,不需要改 SQL,不需要改配置。查询模式符合条件,DuckDB 自动重写执行计划。
做 BI 分析(星型模型 + 事实表 JOIN + GROUP BY)的话,影响不大——代价门控会跳过标准星型模式。
做数据工程、多张事实表对 JOIN、JOIN 后数据量急剧膨胀的话——下次版本升级后,这些查询可能会快几个数量级。
PR 今天刚合入 main 分支,预计下一个 DuckDB 版本发布。
相关 PR:github.com/duckdb/duckdb/pull/23506
Yan & Larson (1995):"Eager Aggregation and Lazy Aggregation", VLDB 1995