原文发布于 quant67.com,转载请保留出处。
MonetDB/X100 确立的 向量批执行 被 DuckDB 与 ClickHouse 共同继承;DuckDB 在并行层采用 morsel-driven (HyPer 论文传统):Row Group 切成 morsel,线程池动态抢任务,而非静态 partition。本文从物理算子讲到 src/parallel/executor.cpp,并对照 ClickHouse 向量化(第 04 篇) 与 PostgreSQL 执行器 的 volcano 逐行模型。
版本锚定:DuckDB 1.x。
一、Volcano vs Vector vs Morsel
| 模型 | 数据单位 | 并行 | 代表 |
|---|---|---|---|
| Volcano | 1 row | 难 | PG 传统 plan |
| Vector | batch(如 1024 行) | 算子内 SIMD | MonetDB/X100 |
| Morsel-driven | batch + 动态 work queue | 核间负载均衡 | HyPer, DuckDB |
PostgreSQL 11+ 有部分 vectorization,但 OLTP 主路径仍是 tuple-at-a-time;DuckDB 纯分析 全路径向量 + morsel。
二、Column Vector 与 Selection Vector
DuckDB DataChunk(概念名,源码见 src/common/types/data_chunk.cpp)持有:
- 多列
Vector; - 行数
count; - 可选 Selection Vector(filter 后活跃行索引,避免物理删除)。
Filter 典型路径:
- 比较产生
SelectionVector; - 下游算子只处理选中行;
- 常配合 字典编码 列避免物化。
ClickHouse ColumnUInt8 filter 类似,实现在 src/Columns/------第 04 篇 已述 Block 结构。
三、Physical Operator 与 Pipeline
物理计划树分解为 Pipeline :线性算子链 + 一个 Source + 一个 Sink。
- Pipeline breaker :Hash Join Build、Hash Aggregate 等会 Barrier------先完成 build 再 probe。
- Meta-Pipeline :复杂 JOIN 树多 pipeline 由
Executor调度顺序。
源码入口:
src/execution/physical_operator.cppsrc/execution/operator/scan/physical_table_scan.cppsrc/execution/operator/join/physical_hash_join.cppsrc/execution/operator/aggregate/physical_hash_aggregate.cpp
四、Morsel-Driven 并行
4.1 划分
Table Scan 按 Row Group → morsel(默认与 Row Group 对齐或可再切)。每个 morsel 是一个 ParallelTask。
4.2 调度
TaskScheduler 维护 worker 线程;任务完成 push 新 morsel。负载均衡:慢 morsel(高 selectivity 后仍大)不阻塞快线程。
对比 ClickHouse:
- CH:
max_threads+ parts 级并行 + MergeTree range 读; - DuckDB:单表 scan 也 morsel 并行,无 shard。
4.3 threads setting
sql
SET threads = 8;
超过 CPU 核数可能因 contention 变慢------须本地 benchmark,本文不给倍数。
五、Hash Join 实现要点
DuckDB 默认 hash join(无 index nested loop 对事实表大 scan 不友好)。
Build 侧:
- 向量化 insert hash table;
- 字符串用 salted hash;
- 大 build 可 external hash join spill 到
temp_directory。
Probe 侧:
- morsel 并行 probe;
- semi/anti join 优化消除不必要列。
ClickHouse ConcurrentHashJoin / grace hash 在 src/Processors/------分布式时还有 GLOBAL 广播(第 09 篇)。
六、Aggregation
6.1 单层聚合
PhysicalHashAggregate:hash table 键 = GROUP BY 列,值 = aggregate state。
6.2 两阶段 / 分区
高 cardinal GROUP BY:
- 先 partition 再 partial aggregate;
- spill 类似 join。
ClickHouse group_by_two_level_threshold 同族思路------配置(第 16 篇)。
6.3 DISTINCT
Often GROUP BY + COUNT 或专用 PhysicalDistinct------优化器 rewrite。
七、Optimizer 与 Pipeline 的衔接
src/optimizer/ 规则影响物理 plan:
- Predicate pushdown 到 scan(Parquet row group stats);
- Join order 动态规划 / 启发式;
- Common subexpression elimination;
- Top-N 下推(LIMIT + ORDER BY)。
sql
EXPLAIN SELECT ...;
EXPLAIN ANALYZE SELECT ...;
EXPLAIN ANALYZE 输出 实际 timing------仅本地有效,不可跨文引用具体毫秒。
八、Table Scan 与 Segment Skip
读 ColumnSegment 时:
- 读 segment footer statistics;
- 若
max < predicate constant→ skip entire segment; - 否则 decompress + vector filter。
与 ClickHouse Mark Range + 跳数索引(第 07 篇) 同族 zonemap pruning。
Parquet 扫描复用 row group metadata ------read_parquet 不经过 DuckDB storage 仍享 skip。
九、Spill 与内存
Settings:
| Setting | 作用 |
|---|---|
memory_limit |
进程软上限 |
max_temp_directory_size |
spill 上限 |
preserve_insertion_order |
是否保序(影响并行) |
OOM 时 DuckDB 尝试 spill;仍失败则报错------嵌入式 Python 需 catch。
ClickHouse OOM 见 max_memory_usage 与 经典故障(第 15 篇)。
十、并行 COPY 与 INSERT
sql
COPY large_table FROM 'big.csv' (HEADER, DELIMITER ',');
Parser + 向量化 insert 可并行读文件------瓶颈常在磁盘或 CSV parse。
对比 ClickHouse INSERT FORMAT CSV + ingest 背压------DuckDB 单机 无 parts_to_throw_insert,但超大事务 checkpoint 压力存在。
十一、UDF 与向量边界
用户标量 UDF 若未向量化,可能 逐行回调 Python------destroy 性能。推荐:
- DuckDB 原生 SQL / 宏;
- Arrow 零拷贝接口;
- 扩展 C++ UDF。
ClickHouse 同理:Python UDF 慢于原生 executable 或 SQL。
十二、与 ClickHouse Processors 对照
| 概念 | DuckDB | ClickHouse |
|---|---|---|
| 批 | DataChunk | Block |
| 图 | Pipeline + MetaPipeline | Processor DAG |
| 并行 | Morsel task queue | max_threads, parts |
| 远程 | 无 | RemoteQueryExecutor |
| EXPLAIN | EXPLAIN ANALYZE | EXPLAIN PLAN, query_log |
读 CH 源码 src/Processors/Executors/ExecutingGraph.cpp 与 DuckDB src/parallel/executor.cpp 可感受 调度哲学差异。
十三、PostgreSQL 执行器对照
PG 执行器(第 12 篇 PG) ExecProcNode 逐 plan node 递归;DuckDB 编译为 扁平 pipeline + 向量 primitive 。PG 向 OLAP 的 columnar store(cstore_fdw 等)不在 PG 核内------pg_duckdb 是旁路加速。
十四、Profiling
sql
PRAGMA enable_profiling;
SELECT ...;
PRAGMA profiling_output = 'query.json';
Chrome trace 风格分析 morsel 并行度------工具随版本演进,以文档为准。
ClickHouse 用 query_log、trace_log(监控(第 14 篇))。
十五、实验框架(须本地执行)
15.1 并行度对比
sql
SET threads=1;
SELECT sum(v) FROM big WHERE g > 0;
SET threads=8;
SELECT sum(v) FROM big WHERE g > 0;
记录 EXPLAIN ANALYZE 中 total time------仅本地。
15.2 Join spill
sql
SET memory_limit='128MB';
SELECT count(*) FROM big a JOIN big b ON a.id = b.id;
观察是否 spill 或失败。
十六、学术谱系:X100 → Morsel → DuckDB Pipeline
| 阶段 | 文献 / 系统 | 贡献 |
|---|---|---|
| 2005 | MonetDB/X100, CIDR | 向量批执行奠基(与 第 04 篇 同源) |
| 2014 | Leis et al., Morsel-Driven Parallelism, SIGMOD | 动态任务窃取;抗 skew;HyPer 传统 |
| 2018 | Kersten et al., PVLDB | compiled vs vectorized 实证争论 |
| 2019+ | DuckDB | 全路径向量 + morsel;嵌入式进程内(Raasveldt & Mühleisen, SIGMOD 2019 demo) |
与 ClickHouse 对照 :CH 用 PipelineExecutor 调度 IProcessor(第 04 篇);DuckDB 用 morsel 队列在 核间 抢 Row Group 切片。两者都属 X100 谱系,并行调度哲学不同------不是「谁更向量化」。
16.1 争论:静态分区 vs morsel
- 静态 partition:实现简单,skew 时核空转。
- Morsel / work stealing(Leis 2014):负载均衡好,调度与缓存局部性有代价。
DuckDB 选后者服务 单机多核分析 ;CH 服务端还要面对 多查询 + merge 线程 争抢------并行模型不可直接照搬。
16.2 工程间隙
| 论文 / HyPer 设定 | DuckDB 1.x 嵌入 |
|---|---|
| 独占服务器、长查询 | notebook / 应用进程共享内存 |
| 少谈持久化 merge | .duckdb checkpoint 与 CH Part merge 不同账单 |
| 理想 SIMD 对齐 | Selection Vector + 字典列使路径分支更多 |
| 无多租户 | 单文件写锁限制并发 ingest |
16.3 开放问题
- Spill 阈值与 morsel 粒度如何联合调优? 可检验:降
memory_limit后EXPLAIN ANALYZE的 spill 与线程空闲(入口:Leis SIGMOD'14;DuckDB Memory Management)。 - 嵌入式场景是否应默认更激进 codegen? 与第 04 篇 CH 问题对称;Kersten 2018 仍是坐标。
- 与 CH Processors 在同一硬件上的并行效率------须固定 query mix 实测,本文不排名。
十七、小结
DuckDB 执行层 = 向量批 + morsel-driven 任务窃取 ;优化器把 skip 下推到 Segment/Parquet。理解 Pipeline breaker 与 spill,就理解嵌入式 OLAP 在 单机核数 上的扩展上限。
附录 A、物理算子目录(源码)
| 算子 | 路径方向 |
|---|---|
| Table Scan | operator/scan/physical_table_scan.cpp |
| Filter | operator/filter/physical_filter.cpp |
| Hash Join | operator/join/physical_hash_join.cpp |
| Cross Product | operator/join/physical_cross_product.cpp |
| Hash Aggregate | operator/aggregate/physical_hash_aggregate.cpp |
| Window | operator/aggregate/physical_window.cpp |
| Order By | operator/order/physical_order.cpp |
| Limit | operator/limit/physical_limit.cpp |
| Insert | operator/persistent/physical_insert.cpp |
附录 B、Pipeline Breaker 列表
Hash Join Build、Hash Aggregate、Sort、部分 Window 会 barrier------先完成 build 再 probe/输出。理解 breaker 数量解释 为何某些查询并行度低。
附录 C、Grace Hash Join
Build 侧大于 memory 时分区 spill 到 temp_directory,逐分区 join。ClickHouse 类似 grace hash in processors。调小 memory_limit 可强制 spill 测试(测试库)。
附录 D、Optimizer 规则(代表性)
Filter pushdown、Join reorder、Common subexpression、Top-N pushdown、Duplicate grouping removal。EXPLAIN 输出 logical → physical 变化。
上一篇 :DuckDB 架构
参考资料
核心论文
- Boncz et al., MonetDB/X100: Hyper-Pipelining Query Execution, CIDR 2005(A 级)。
- Leis et al., Morsel-Driven Parallelism: A NUMA-Aware Query Evaluation Framework for the Many-Core Age, SIGMOD 2014(A 级)。
- Kersten et al., Everything You Always Wanted to Know About Compiled and Vectorized Queries..., PVLDB 2018(A 级)。
- Raasveldt & Mühleisen, DuckDB: an Embeddable Analytical Database, SIGMOD 2019 demo(B/A 级系统介绍)。
规范 / 源码 / 文档
- DuckDB Documentation, Execution / Parallelism(1.x;A 级)。
- DuckDB Source,
src/execution/、src/parallel/(A 级)。 - ClickHouse Source,
src/Processors/(对照;A 级)。