DuckDB 2.0 升级后,视图查询慢了 37 倍
10 月 21 日,DuckDB 2.0.0 LTS 发布。10 月 27 日,一个会让部分查询慢几十倍的回归被修进 main 分支,同一批修复正在往 2.0 的 backport 分支合。
如果你在 2.0 里查视图感觉变慢了,先别怀疑自己的数据量:把过滤条件挪个位置,能快 285 倍。这个数字是我本机实测的,复现方法在下面。
● ● ●
慢在哪
10 月 6 日,有用户提了 issue #26510:视图的 SELECT 列表里带一个相关子查询(correlated subquery),对这个视图做点查询,WHERE 的条件推不进表扫描,每次都全表扫一遍。
拿 issue 里的例子,facts 表 200 万行:
CREATE TABLE facts AS SELECT i AS id, (i % 25) AS customer_id,
[ (i % 50), ((i + 1) % 50) ] AS dim_ids
FROM range(2000000) r(i);
CREATE TABLE dims AS SELECT i AS dim_id, 'name ' || i AS name
FROM range(50) r(i);
CREATE VIEW v AS
SELECT f.id, f.customer_id,
(SELECT list(d.name ORDER BY d.name)
FROM UNNEST(f.dim_ids) AS u(dim_id)
JOIN dims d ON d.dim_id = u.dim_id) AS dim_names
FROM facts f;
查一行:
SELECT id, dim_names FROM v
WHERE customer_id = 7 AND id = 1007;
DuckDB 把相关子查询重写成 delim join,WHERE 条件要推到扫描必须穿过这个 join。老版本只允许条件穿过 INNER 和 LEFT 两种 delim join,而标量子查询重写出来的是 SINGLE join,SELECT 列表里的 EXISTS 和 IN 是 MARK join——都不在白名单里。
结果就是:条件停在 join 上方,delim CTE 拿到全部 200 万行,子查询对每一行都跑一遍,跑完再过滤。
● ● ●
本机实测
我用 pip 装了两个版本各跑一遍,同机同负载,结果如下:
三条都是 2.0.0 发布版实测。PR 作者在正文里报的数字更狠:main 分支上同一条查询 3.378 秒,1.5.6 只要 0.009 秒——主仓开发版一度比 1.5.6 慢 375 倍。2.0.0 发布时已经部分好转,但回归仍在。
1.5.6 与 2.0.0 三组查询耗时对比
数据在 Parquet 文件上时更惨:条件进不了扫描,文件裁剪(file pruning)跟着失效。issue 作者测过,26 个分区文件全读,哪怕命中数据只在 1 个文件里。
● ● ●
一行改写,快 285 倍
修复进 main 了(PR #27760,10 月 27 日合并),但发布版用户现在等不到。等 backport 落进下一个 patch 之前,有个立竿见影的改写——把 WHERE 写到子查询下面:
SELECT f.id,
(SELECT list(d.name)
FROM UNNEST(f.dim_ids) AS u(dim_id)
JOIN dims d ON d.dim_id = u.dim_id) AS dim_names
FROM (SELECT * FROM facts
WHERE customer_id = 7 AND id = 1007) f;
实测(2.0.0,本机):
285 倍。结果完全一致。视图没法改的话,把查询写成派生表、在子查询外面包一层 WHERE 也没用——条件还是停在 join 上方,必须写进 FROM 子句里。
中间还有个折中:如果你的视图在别的库或别人维护的代码里,至少检查一下它有没有 SELECT 列表里带相关子查询的列。SELECT 里不选那一列也救不了——issue 里测了,子查询照样计划、照样全表扫。
● ● ●
顺带修的另一个:1560 个子查询规划 44 秒
同一个 PR 还修了第二件事,影响的是另一拨人:用 UNION ALL 拼大量子查询的统计分析查询。
场景很具体:做相关性分析(互信息、Cramér's V 这类)的库,习惯对 N 列数据的每一对列生成一个子查询分支,40 列就是 40×39=1560 个分支,用 UNION ALL 拼起来。issue #22737 从 5 月挂到现在。
根因在 FlattenDependentJoins:它对每个算子子节点都调一次 RewriteCorrelatedBindings,这个函数会走一遍整棵计划树。UNION ALL 在解析器里是带 K 个孩子的 set operation,每层都全树走一遍,规划时间 O(K²)。
我本机实测(1 行源表,每个分支带 4 个 CTE 和窗口函数,纯规划开销):
两个发布版都是平方增长,2.0 还比 1.5.6 慢一倍。PR 作者报的数字:1560 分支的规划时间从 3.9 秒降到 0.06 秒,3540 分支(60 列)从约 100 秒降到 3.52 秒。
UNION ALL 分支数与规划时间
修法只有 4 行:correlated_aliases 为空时跳过那遍全树重写。带相关列的查询仍走原逻辑,超线性问题在那类查询上还留着,作者明说了留待以后。
● ● ●
什么时候能等到正式修复
PR #27760 合进了 main(对应 2.1 系列)。它带着 backport v2.0-cyanoptera 标签,10 月 27 日下午有人开了 #27965,把包括它在内的一批修复打包往 v2.0 分支合,截至发稿还是 open 状态。
也就是说下一个 2.0.x patch 大概率会带上这两处修复。在那之前:
查视图慢:用上面的改写,条件写进 FROM 子查询 UNION ALL 拼上千个子查询:拆成多次查询,或者等 patch 数据在 Parquet 上:优先确认分区裁剪有没有生效, EXPLAIN ANALYZE看 File Count
另外两个已知的残余问题,升级后如果还慢可以对照:一是修复后条件仍停在 delim CTE 内部扫描的上方(和 1.5.6 行为一致),每个 row group 还是会被读;二是一个视图里有两个相关子查询时,条件只能穿过其中一个 delim join,作者实测那种查询 2.8 秒对 main 的 5.7 秒——好了一半,没好透。
顺手一提:同一批提交里还有一条给 2.1 定了代号 Islandica(Barrow's goldeneye,冰岛丑鸭),按发布历排在 2027 上半年。离 2.1 还远,先把 2.0 的坑填完。
你在 2.0 上遇到过查询变慢吗?是升级后立刻发现的,还是查了半天 profiler 才定位到?评论区聊聊。
● ● ●
参考来源
- 01
issue #26510(视图过滤推不下推到表扫描)https://github.com/duckdb/duckdb/issues/26510 - 02
issue #22737(UNION ALL 多子查询平方级规划)https://github.com/duckdb/duckdb/issues/22737 - 03
PR #27760(修复正文,含官方 benchmark 数据)https://github.com/duckdb/duckdb/pull/27760 - 04
PR #27965(2.0 backport 批量修复,进行中)https://github.com/duckdb/duckdb/pull/27965 - 05
DuckDB 2.0.0 LTS 发布公告 https://duckdb.org/2026/10/21/announcing-duckdb-200 - 06
DuckDB 发布日历 https://duckdb.org/release_calendar.html