2026年6月16日,DuckDB合并了一个推送请求:https://github.com/duckdb/duckdb/pull/23287,与我以前的思路(https://blog.csdn.net/l1t/article/details/148504369 https://blog.csdn.net/l1t/article/details/148504384)一致。
ROLLUP / CUBE / Grouping Sets 的级联聚合优化
目前,ROLLUP / CUBE / Grouping Sets 的实现方式是使用多个哈希表。然而,它们通常可以通过级联聚合来实现。与其为每个分组集使用一个哈希表,我们可以先计算"最宽"的哈希表,然后将该哈希表作为输入,用于后续的聚合计算,依此类推。
例如,假设我们有如下查询:
sql
SELECT l_returnflag, l_linestatus, SUM(l_quantity)
FROM lineitem
GROUP BY CUBE(l_returnflag, l_linestatus)
ORDER BY ALL;
实际上,我们需要计算四个分组集:
l_returnflag, l_linestatusl_returnflagl_linestatus- (空) // 即未分组
通过采用级联分组,我们可以节省大量时间------第一个哈希表 l_returnflag, l_linestatus 只有四行数据。因此,我们不需要将 lineitem 中的每一行都推入四个不同的哈希表,而只需将其推入一个哈希表,然后在该哈希表的基础上进行重新聚合即可。
利用物化 CTE 和聚合状态,我们具备了实现这一优化所需的所有组件。上述查询可以有效地重写为:
sql
with
group_by_returnflag_linestatus as (
select l_returnflag, l_linestatus, sum(l_quantity) export_state l_quantity_state from lineitem group by l_returnflag, l_linestatus
),
group_by_returnflag as (
select l_returnflag, NULL AS l_linestatus, combine_aggr(l_quantity_state) l_quantity_state from group_by_returnflag_linestatus group by l_returnflag
),
group_by_linestatus as (
select NULL AS l_returnflag, l_linestatus, combine_aggr(l_quantity_state) l_quantity_state from group_by_returnflag_linestatus group by l_linestatus
),
ungrouped as (
select NULL AS l_returnflag, NULL AS l_linestatus, combine_aggr(l_quantity_state) l_quantity_state from group_by_returnflag
)
SELECT l_returnflag, l_linestatus, finalize(l_quantity_state) AS l_quantity_sum FROM group_by_returnflag_linestatus
UNION ALL
SELECT l_returnflag, l_linestatus, finalize(l_quantity_state) AS l_quantity_sum FROM group_by_returnflag
UNION ALL
SELECT l_returnflag, l_linestatus, finalize(l_quantity_state) AS l_quantity_sum FROM group_by_linestatus
UNION ALL
SELECT l_returnflag, l_linestatus, finalize(l_quantity_state) AS l_quantity_sum FROM ungrouped;
实际上,我们先计算第一次聚合,并导出聚合状态------然后在该聚合结果的基础上进行"级联"聚合。最后,通过完成(finalize)已收集的聚合状态,即可得到最终结果。
在上述示例中,使用 CUBE 后我们获得了大约 2 倍的性能提升(TPC-H SF100):
| 版本 | 耗时(秒) |
|---|---|
| v1.5 | 0.8s |
| 新版本 | 0.4s |
我从相应的自动构建
https://github.com/duckdb/duckdb/actions/runs/27599923021 中提取的二进制文件,重命名为duckdb0616。
测试结果如下:
sql
C:\d>duckdb0616
DuckDB v1.6.0-dev8751 (Development Version, 99fa1b5f55)
Enter ".help" for usage hints.
memory D CREATE TABLE lineitem(l_orderkey BIGINT NOT NULL, l_partkey BIGINT NOT NULL, l_suppkey BIGINT NOT NULL, l_linenumber BIGINT NOT NULL, l_quantity DECIMAL(15,2) NOT NULL, l_extendedprice DECIMAL(15,2) NOT NULL, l_discount DECIMAL(15,2) NOT NULL, l_tax DECIMAL(15,2) NOT NULL, l_returnflag VARCHAR NOT NULL, l_linestatus VARCHAR NOT NULL, l_shipdate DATE NOT NULL, l_commitdate DATE NOT NULL, l_receiptdate DATE NOT NULL, l_shipinstruct VARCHAR NOT NULL, l_shipmode VARCHAR NOT NULL, l_comment VARCHAR NOT NULL);
memory D COPY lineitem FROM 'tpch1/lineitem.csv' (FORMAT 'csv', force_not_null ('l_orderkey', 'l_partkey', 'l_suppkey', 'l_linenumber', 'l_quantity', 'l_extendedprice', 'l_discount', 'l_tax', 'l_returnflag', 'l_linestatus', 'l_shipdate', 'l_commitdate', 'l_receiptdate', 'l_shipinstruct', 'l_shipmode', 'l_comment'), quote '"', delimiter ',', header 1);
memory D .timer on
memory D SELECT l_returnflag, l_linestatus, SUM(l_quantity)
FROM lineitem
GROUP BY CUBE(l_returnflag, l_linestatus)
ORDER BY ALL;
┌──────────────┬──────────────┬─────────────────┐
│ l_returnflag │ l_linestatus │ sum(l_quantity) │
│ varchar │ varchar │ decimal(38,2) │
├──────────────┼──────────────┼─────────────────┤
│ A │ F │ 3774200.00 │
│ A │ NULL │ 3774200.00 │
│ N │ F │ 95257.00 │
│ N │ O │ 7679822.00 │
│ N │ NULL │ 7775079.00 │
│ R │ F │ 3785523.00 │
│ R │ NULL │ 3785523.00 │
│ NULL │ F │ 7654980.00 │
│ NULL │ O │ 7679822.00 │
│ NULL │ NULL │ 15334802.00 │
└──────────────┴──────────────┴─────────────────┘
10 rows 3 columns
Run Time (s): real 0.026 user 0.015625 sys 0.015625
memory D with
group_by_returnflag_linestatus as (
select l_returnflag, l_linestatus, sum(l_quantity) export_state l_quantity_state from lineitem group by l_returnflag, l_linestatus
),
group_by_returnflag as (
select l_returnflag, NULL AS l_linestatus, combine_aggr(l_quantity_state) l_quantity_state from group_by_returnflag_linestatus group by l_returnflag
),
group_by_linestatus as (
select NULL AS l_returnflag, l_linestatus, combine_aggr(l_quantity_state) l_quantity_state from group_by_returnflag_linestatus group by l_linestatus
),
ungrouped as (
select NULL AS l_returnflag, NULL AS l_linestatus, combine_aggr(l_quantity_state) l_quantity_state from group_by_returnflag
)
SELECT l_returnflag, l_linestatus, finalize(l_quantity_state) AS l_quantity_sum FROM group_by_returnflag_linestatus
UNION ALL
SELECT l_returnflag, l_linestatus, finalize(l_quantity_state) AS l_quantity_sum FROM group_by_returnflag
UNION ALL
SELECT l_returnflag, l_linestatus, finalize(l_quantity_state) AS l_quantity_sum FROM group_by_linestatus
UNION ALL
SELECT l_returnflag, l_linestatus, finalize(l_quantity_state) AS l_quantity_sum FROM ungrouped;
┌──────────────┬──────────────┬────────────────┐
│ l_returnflag │ l_linestatus │ l_quantity_sum │
│ varchar │ varchar │ decimal(38,2) │
├──────────────┼──────────────┼────────────────┤
│ A │ F │ 3774200.00 │
│ N │ F │ 95257.00 │
│ N │ O │ 7679822.00 │
│ R │ F │ 3785523.00 │
│ R │ NULL │ 3785523.00 │
│ A │ NULL │ 3774200.00 │
│ N │ NULL │ 7775079.00 │
│ NULL │ F │ 7654980.00 │
│ NULL │ O │ 7679822.00 │
│ NULL │ NULL │ 15334802.00 │
└──────────────┴──────────────┴────────────────┘
10 rows 3 columns
Run Time (s): real 0.022 user 0.015625 sys 0.000000
memory D
memory D explain SELECT l_returnflag, l_linestatus, SUM(l_quantity)
FROM lineitem
GROUP BY CUBE(l_returnflag, l_linestatus)
ORDER BY ALL;
┌─────────────────────────────┐
│┌───────────────────────────┐│
││ Physical Plan ││
│└───────────────────────────┘│
└─────────────────────────────┘
┌───────────────────────────┐
│ ORDER_BY │
│ ──────────────────── │
│ l_returnflag ASC │
│ l_linestatus ASC │
│ sum(l_quantity) ASC │
│ │
│ ~600,573 rows │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ CTE │
│ ──────────────────── │
│ CTE Name: │
│ __grouping_sets_cte_9 │
│ ├──────────────┐
│ Table Index: 9 │ │
│ │ │
│ ~600,573 rows │ │
└─────────────┬─────────────┘ │
┌─────────────┴─────────────┐┌─────────────┴─────────────┐
│ PROJECTION ││ CTE │
│ ──────────────────── ││ ──────────────────── │
│__internal_decompress_strin││ CTE Name: │
│ g(#0) ││ __grouping_sets_cte_16 │
│__internal_decompress_strin││ ├──────────────┐
│ g(#1) ││ Table Index: 16 │ │
│ #2 ││ │ │
│ ││ │ │
│ ~300,286 rows ││ ~600,573 rows │ │
└─────────────┬─────────────┘└─────────────┬─────────────┘ │
┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐
│ PERFECT_HASH_GROUP_BY ││ HASH_GROUP_BY ││ UNION │
│ ──────────────────── ││ ──────────────────── ││ │
│ Groups: ││ Groups: #0 ││ │
│ #0 ││ ││ │
│ #1 ││ Aggregates: ││ │
│ ││ combine_aggr(#1) ││ ├──────────────┬────────────────────────────┬────────────────────────────┐
│ Aggregates: ││ ││ │ │ │ │
│ sum(#2) EXPORT_STATE ││ ││ │ │ │ │
│ ││ ││ │ │ │ │
│ ~300,286 rows ││ ~150,143 rows ││ │ │ │ │
└─────────────┬─────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘ │ │ │
┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐
│ PROJECTION ││ PROJECTION ││ PROJECTION ││ PROJECTION ││ PROJECTION ││ PROJECTION │
│ ──────────────────── ││ ──────────────────── ││ ──────────────────── ││ ──────────────────── ││ ──────────────────── ││ ──────────────────── │
│ l_returnflag ││ #1 ││ NULL ││ #0 ││ #0 ││ NULL │
│ l_linestatus ││ #2 ││ NULL ││ NULL ││ #1 ││ #0 │
│ l_quantity ││ ││ finalize(#0) ││ finalize(#1) ││ finalize(#2) ││ finalize(#1) │
│ ││ ││ ││ ││ ││ │
│ ~600,572 rows ││ ~300,286 rows ││ ~1 row ││ ~150,143 rows ││ ~300,286 rows ││ ~150,143 rows │
└─────────────┬─────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘
┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐
│ PROJECTION ││ CTE_SCAN ││ UNGROUPED_AGGREGATE ││ HASH_GROUP_BY ││ CTE_SCAN ││ CTE_SCAN │
│ ──────────────────── ││ ──────────────────── ││ ──────────────────── ││ ──────────────────── ││ ──────────────────── ││ ──────────────────── │
│__internal_compress_string_││ CTE Index: 9 ││ Aggregates: ││ Groups: #0 ││ CTE Index: 9 ││ CTE Index: 16 │
│ utinyint(#0) ││ ││ combine_aggr(#0) ││ ││ ││ │
│__internal_compress_string_││ ││ ││ Aggregates: ││ ││ │
│ utinyint(#1) ││ ││ ││ combine_aggr(#1) ││ ││ │
│ #2 ││ ││ ││ ││ ││ │
│ ││ ││ ││ ││ ││ │
│ ~600,572 rows ││ ~300,286 rows ││ ~1 row ││ ~150,143 rows ││ ~300,286 rows ││ ~150,143 rows │
└─────────────┬─────────────┘└───────────────────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘└───────────────────────────┘└───────────────────────────┘
┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐
│ SEQ_SCAN │ │ PROJECTION ││ PROJECTION │
│ ──────────────────── │ │ ──────────────────── ││ ──────────────────── │
│ Table: │ │ #1 ││ #0 │
│ memory.main.lineitem │ │ ││ #2 │
│ │ │ ││ │
│ Type: Sequential Scan │ │ ││ │
│ │ │ ││ │
│ Projections: │ │ ││ │
│ l_returnflag │ │ ││ │
│ l_linestatus │ │ ││ │
│ l_quantity │ │ ││ │
│ │ │ ││ │
│ ~600,572 rows │ │ ~150,143 rows ││ ~300,286 rows │
└───────────────────────────┘ └─────────────┬─────────────┘└─────────────┬─────────────┘
┌─────────────┴─────────────┐┌─────────────┴─────────────┐
│ CTE_SCAN ││ CTE_SCAN │
│ ──────────────────── ││ ──────────────────── │
│ CTE Index: 16 ││ CTE Index: 9 │
│ ││ │
│ ~150,143 rows ││ ~300,286 rows │
└───────────────────────────┘└───────────────────────────┘
Run Time (s): real 0.005 user 0.000000 sys 0.000000
memory D .exit
从上面的执行计划可见,group by cube被自动转成了CTE查询。
sql
memory D SELECT GROUPING_ID(), l_returnflag, l_linestatus, SUM(l_quantity)
FROM lineitem
GROUP BY CUBE(l_returnflag, l_linestatus)
ORDER BY ALL;
┌────────────┬──────────────┬──────────────┬─────────────────┐
│ GROUPING() │ l_returnflag │ l_linestatus │ sum(l_quantity) │
│ int64 │ varchar │ varchar │ decimal(38,2) │
├────────────┼──────────────┼──────────────┼─────────────────┤
│ 0 │ A │ F │ 3774200.00 │
│ 0 │ N │ F │ 95257.00 │
│ 0 │ N │ O │ 7679822.00 │
│ 0 │ R │ F │ 3785523.00 │
│ 1 │ A │ NULL │ 3774200.00 │
│ 1 │ N │ NULL │ 7775079.00 │
│ 1 │ R │ NULL │ 3785523.00 │
│ 2 │ NULL │ F │ 7654980.00 │
│ 2 │ NULL │ O │ 7679822.00 │
│ 3 │ NULL │ NULL │ 15334802.00 │
└────────────┴──────────────┴──────────────┴─────────────────┘
10 rows 4 columns
比起手工方法,它提供了正确的GROUPING_ID()来辨别所属分组。
改用1.5.3版本
sql
C:\d>duckdb153
DuckDB v1.5.3 (Variegata)
Enter ".help" for usage hints.
memory D CREATE TABLE lineitem(l_orderkey BIGINT NOT NULL, l_partkey BIGINT NOT NULL, l_suppkey BIGINT NOT NULL, l_linenumber BIGINT NOT NULL, l_quantity DECIMAL(15,2) NOT NULL, l_extendedprice DECIMAL(15,2) NOT NULL, l_discount DECIMAL(15,2) NOT NULL, l_tax DECIMAL(15,2) NOT NULL, l_returnflag VARCHAR NOT NULL, l_linestatus VARCHAR NOT NULL, l_shipdate DATE NOT NULL, l_commitdate DATE NOT NULL, l_receiptdate DATE NOT NULL, l_shipinstruct VARCHAR NOT NULL, l_shipmode VARCHAR NOT NULL, l_comment VARCHAR NOT NULL);
memory D COPY lineitem FROM 'tpch1/lineitem.csv' (FORMAT 'csv', force_not_null ('l_orderkey', 'l_partkey', 'l_suppkey', 'l_linenumber', 'l_quantity', 'l_extendedprice', 'l_discount', 'l_tax', 'l_returnflag', 'l_linestatus', 'l_shipdate', 'l_commitdate', 'l_receiptdate', 'l_shipinstruct', 'l_shipmode', 'l_comment'), quote '"', delimiter ',', header 1);
memory D explain SELECT l_returnflag, l_linestatus, SUM(l_quantity)
FROM lineitem
GROUP BY CUBE(l_returnflag, l_linestatus)
ORDER BY ALL;
┌─────────────────────────────┐
│┌───────────────────────────┐│
││ Physical Plan ││
│└───────────────────────────┘│
└─────────────────────────────┘
┌───────────────────────────┐
│ ORDER_BY │
│ ──────────────────── │
│ l_returnflag ASC │
│ l_linestatus ASC │
│ sum(l_quantity) ASC │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ HASH_GROUP_BY │
│ ──────────────────── │
│ Groups: │
│ #0 │
│ #1 │
│ │
│ Aggregates: │
│ sum_no_overflow(#2) │
│ │
│ ~5 rows │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ PROJECTION │
│ ──────────────────── │
│ l_returnflag │
│ l_linestatus │
│ l_quantity │
│ │
│ ~600,572 rows │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ SEQ_SCAN │
│ ──────────────────── │
│ Table: │
│ memory.main.lineitem │
│ │
│ Type: Sequential Scan │
│ │
│ Projections: │
│ l_returnflag │
│ l_linestatus │
│ l_quantity │
│ │
│ ~600,572 rows │
└───────────────────────────┘
memory D
memory D .timer on
memory D SELECT l_returnflag, l_linestatus, SUM(l_quantity)
FROM lineitem
GROUP BY CUBE(l_returnflag, l_linestatus)
ORDER BY ALL;
┌──────────────┬──────────────┬─────────────────┐
│ l_returnflag │ l_linestatus │ sum(l_quantity) │
│ varchar │ varchar │ decimal(38,2) │
├──────────────┼──────────────┼─────────────────┤
│ A │ F │ 3774200.00 │
│ A │ NULL │ 3774200.00 │
│ N │ F │ 95257.00 │
│ N │ O │ 7679822.00 │
│ N │ NULL │ 7775079.00 │
│ R │ F │ 3785523.00 │
│ R │ NULL │ 3785523.00 │
│ NULL │ F │ 7654980.00 │
│ NULL │ O │ 7679822.00 │
│ NULL │ NULL │ 15334802.00 │
└──────────────┴──────────────┴─────────────────┘
10 rows 3 columns
Run Time (s): real 0.024 user 0.046875 sys 0.046875
大概是由于数据量小,时间差别不明显。