alitrack

DuckDB 数 1250 亿行 join 结果 0.06 秒,join 没发生

四张表 join 起来,结果 1250 亿行,count(*) 多久跑完?

我在这台机器上实测:DuckDB 原生执行计划 67.7 秒。装上昨天刚进官方社区扩展库的 factorize,0.058 秒。同一个数:125,000,000,000。

一天前,2026 年 10 月 23 日,factorize 被合并进 duckdb/community-extensions,版本 0.2.0,MIT 协议。它是 Max De Marzi 写的 DuckDB 扩展,思路来自今年 PVLDB 的一篇论文:Lehner & Neumann,《The Data World Is Not Flat》。

● ● ●

它解决什么问题

join 的结果可以比输入大得多。多对多 join 尤其狠——两张表各 100 万行,join 完可能是上万亿行。你要的不是那万亿行本身,只是一个 count、一个 sum。

DuckDB 的执行器再快,也得把中间结果一个个过一遍。数 1250 亿个空 tuple,就要 67 秒。

factorize 的做法来自 factorised execution:join 结果不摊平,保持因子化表示——独立的分支存乘积,不枚举每个组合。计数时从表示里直接读出来,join 从头到尾没有发生。

records 按加法增长,tuples 按乘法增长。这就是差距的来源。

● ● ●

我实测的数据

环境:WSL,DuckDB v2.0.0 正式版 CLI,100 万行 edges 表,s 列 2 万个不同值,四表自 join:

SELECT count(*)
  FROM edges a, edges b, edges c, edges d
 WHERE a.s = b.s AND b.s = c.s AND c.s = d.s;
模式
结果
耗时
factorize_mode='off'(原生)
125,000,000,000
67.664 s
factorize_mode='auto'
125,000,000,000
0.058 s
同一条 SQL 的两种走法:原生摊平 join 逐条数 vs factorize 因子化直接读数

同一条 SQL 的两种走法:原生摊平 join 逐条数 vs factorize 因子化直接读数

1166 倍。两个模式结果一致,这是它挂在嘴边的正确性保证:auto 与 off bit-exact,因子化路径如果出问题,会退回被替换的原计划。

开启方式就一行:

INSTALL 'factorize' FROM 'community_extensions';
LOAD factorize;
SET factorize_mode = 'auto';

● ● ●

为什么默认是 off

这个扩展最有意思的地方,是它明确告诉你什么时候不该用它。

README 里写得很直白:对于 DuckDB 已经能搞定的查询,factorize 反而慢。普通的 count(*),DuckDB 不带任何 payload 列穿过 join,数空 tuple 比建因子化表示还快。作者在基准上测过,每条符合条件的查询都触发的话,整体慢 11 倍。

所以它内置了一个 cost model,逐条判断。auto 模式拒绝的次数远多于触发——拒绝一次花 2-4 毫秒做统计采样,100ms 以上的查询里这点开销看不见。

我实测时开了 factorize_explain=true,能看到 gate 的判断过程:它估算了每边 100 万条记录、flat 表示 5.4e+07 字节、自己的方案 128ms、DuckDB 原生 263ms,然后接管。一个两表 join 的小查询它同样判断后接管,0.03 秒出结果。

● ● ●

能做什么,不能做什么

支持的范围:count()、count(c)、sum、avg、min、max、bool_and/bool_or、sum(xy),作用在无环多对多等值 join 图上,整数和 DECIMAL 列,带 GROUP BY、EXISTS、LIMIT。其他一律拒绝,原计划照跑。

不支持的:WASM(刻意不做——表示的大小要对着 memory_limit 调整,wasm32 地址空间没意义)、写操作、非等值 join。

作者的基准数字:在 cardinality estimation 基准 442 条结果超 1e9 的查询里,244 条 stock DuckDB 60 秒内跑不完的 count(*),factorize 中位数 0.09 秒答完。最好的一条,10.8 万亿结果 24 秒。

min/max 走的是另一条路——semijoin reducer,只读输入不建 join,可跑通的语料上 3.9× 和 3.7×。

● ● ●

论文到扩展,一个多月

时间线值得说一句。Lehner & Neumann 的论文发表在 PVLDB 19(11),2026 年。作者 9 月 1 日建仓,clean-room 实现——只看论文,不读任何 GPLv3 源码,所以能以 MIT 发布。10 月 23 日进社区扩展库。

DECISIONS.md 值得一读:127 条编号决策,每条带日期和测量方法,包括那些没做成的。成本模型的系数是在一台机器上拟合的,换个机器要重跑 scripts/calibrate-synthetic.py 重新拟合——README 里连这个都直说了。

回到开头那个问题:四表 join 数 1250 亿,67.7 秒变成 0.058 秒,靠的是承认一个事实——你要的从来不是那 1250 亿行,只是一个数。下次再碰到一条 count 跑不完的 join,先看看它是不是无环等值 join 图,装个 factorize 试试。你的工作里有没有那种「结果大到没法跑」的 join?评论区说说。

● ● ●

参考来源

  1. 01
    factorize 扩展的 PR(含完整说明与测量)

https://github.com/duckdb/community-extensions/pull/3346

  1. 01
    maxdemarzi/factorize 仓库(README 与 DECISIONS.md)

https://github.com/maxdemarzi/factorize

  1. 01
    论文:Lehner & Neumann, The Data World Is Not Flat: Efficient Factorized Execution for Relational Systems, PVLDB 19(11):3006–3019, 2026
  1. 01
    社区扩展库中的 factorize 条目

https://community-extensions.duckdb.org

  1. 01
    本文所有实测数字(125,000,000,000 / 0.058s / 67.664s)出自本机 DuckDB v2.0.0 + factorize 0.2.0,100 万行合成数据,脚本见文中 SQL