第 6
篇 里的 Volcano 逐行 next() 在 OLAP
扫描百万行时,函数调用与分支预测成本会压过磁盘带宽。columnar-engine
第 4 篇 从 ClickHouse Block 与
IProcessor 讲了 存储对齐的执行
batch;本篇从 分布式 OLAP 查询引擎
视角回答:Trino、DuckDB
如何把「一批列向量」嵌进执行框架,morsel-driven
并行与 Page 流如何分工。
一、为什么必须批量化
Boncz 等 MonetDB/X100: Hyper-Pipelining Query Execution(CIDR 2005)论证:列存 OLAP 若仍逐行 dispatch,CPU 大量耗在 interpretation overhead 而非数据运算。向量化执行的核心等式:
\[ \text{吞吐} \approx \frac{\text{每批行数} \times \text{批处理效率}}{\text{每批固定开销}} \]
| 模式 | 数据单元 | 表达式 | 典型瓶颈 |
|---|---|---|---|
| 行 Volcano | 1 row | 每行一次虚调用 | 分支、cache miss |
| 向量化 | \(N\) rows / batch | tight loop / SIMD | 内存带宽、hash 表 |
| 编译执行 | batch + codegen | LLVM 融合算子 | 编译延迟、plan 变化 |
本系列 Trino 主线采用 Page 级向量化;DuckDB 采用 DataChunk + morsel;Spark 3.x 在 Whole-Stage Codegen 与 columnar batch 间折中(第 13 篇)。
flowchart LR
VOL["Row Volcano<br/>next() x N"]
VEC["Vectorized<br/>batch x N/k"]
VOL -->|"OLAP"| VEC
二、列向量 batch 的结构
2.1 逻辑布局
一批数据在内存中按 列连续 存放(与 columnar-engine 列存基础 一致):
| 组件 | 含义 |
|---|---|
| Values | 定长类型为 T[];变长类型为 offset +
blob |
| Validity mask | 可选 bitmap 标记 NULL |
| Selection vector | 活跃行下标列表,filter 后避免物理拷贝 |
Filter 理想路径:对条件列向量化求值 → 生成
selection →
后续算子只处理选中行(ClickHouse
IColumn::filter;DuckDB Vector
文档)。
2.2 Flat vs Dictionary 编码
| 编码 | 布局 | 适用 |
|---|---|---|
| Flat | 每行一个物理值 | 高基数、已物化列 |
| Dictionary | 索引数组 + 小字典 | 低基数 string/enum、Parquet DICT |
Dictionary 上 filter 可先 扫字典 再映射索引,跳过大量重复解码(与 lakehouse Parquet 篇 字典页呼应)。Connector 读湖时若保留 dictionary block,Filter 算子需识别编码(第 8 篇)。
2.3 与 columnar-engine/04 的对照
| 概念 | ClickHouse(04 篇) | Trino | DuckDB |
|---|---|---|---|
| 批容器 | Block |
Page + Block |
DataChunk / Vector |
| 调度 | IProcessor pipeline |
Driver + Operator |
Pipeline +
PipelineExecutor |
| 默认 batch 量级 | max_block_size(可达 65536) |
Page ~4096 positions | STANDARD_VECTOR_SIZE(2048,版本以文档为准) |
| 并行 | Read pool + pipeline 线程 | 多 Driver / task | Morsel + 工作窃取 |
三者在 「列数组 + 批算子」 上同构,差异在 分布式 Page exchange(Trino)与 存储耦合深度(ClickHouse MergeTree)。
三、Trino:Page 与 Operator 链
Trino worker 上算子间传递 Page(A
级:Trino Documentation Developer
guide;io.trino.spi.block.Block)。
| 字段 | 含义 |
|---|---|
getPositionCount() |
批内行数 |
getChannelCount() |
列数 |
getBlock(channel) |
单列 Block |
Block 类型包括
LongArrayBlock、IntArrayBlock、VariableWidthBlock、DictionaryBlock
等;DictionaryBlock
即字典编码列在引擎内的表示。
flowchart TB
PS["PageSource<br/>getOutput()"]
FIL["FilterOperator"]
PRJ["ProjectOperator"]
AGG["AggregationOperator"]
PS -->|"Page"| FIL
FIL --> PRJ
PRJ --> AGG
向量化点:FilterOperator
对整列生成 mask 或 compacted
Page;ProjectOperator 调用
ExpressionCompiler 生成的 batch 函数。相对 第 6 篇
的行 Volcano,Trino 在 Operator 内部
完成批处理,Driver 仍按 Page 推拉。
MPP 边界:跨 Worker 时 Page 序列化经 Exchange(第 11 篇);batch 越大,shuffle 压缩比与延迟权衡越敏感。
四、Morsel-Driven 并行(DuckDB 主线)
Morsel-driven(Leis et al., Morsel-Driven Parallelism,2014)把表划分成固定大小 morsel,线程动态领取 morsel 执行 pipeline,配合 work-stealing 平衡负载。
DuckDB 执行模型(A 级:DuckDB Documentation Internals / Execution):
| 概念 | 职责 |
|---|---|
| Pipeline | 无 pipeline breaker 的线性算子链 |
| MetaPipeline | 含 Hash Join build 等多子 pipeline |
| ParallelTask | 线程池中的执行单元 |
| Morsel | Scan 划分出的行范围 |
flowchart TB
T1["Thread 1<br/>morsel 0-2047"]
T2["Thread 2<br/>morsel 2048-4095"]
T3["Thread 3<br/>steal morsel ..."]
SCAN["TableScan"]
SCAN --> T1
SCAN --> T2
SCAN --> T3
与 Trino 按 Split 开多 Driver 类似:二者都是 shared-nothing 线程 + 划分输入,区别是 DuckDB 在 单进程 内完成,Trino 在 多 Worker 上还要考虑 split locality(第 10 篇)。
NUMA 感知:DuckDB 文档描述 morsel 调度可结合 NUMA-local 分配;Trino 依赖 OS 与 connector 的 split host 提示,不保证 NUMA 最优。
五、SIMD 与引擎边界
向量化 不等于 手写 AVX intrinsic 遍布源码。常见分层:
- 编译器 auto-vectorize tight loop(filter、算术)。
- 运行时选 kernel(DuckDB
UnifiedVectorFormat路径)。 - 专用库(如 xsimd、libdivide 用于 hash)。
引擎责任是保证 内存对齐、连续访问、少分支;是否生成 SIMD 指令取决于编译器与类型宽度。不在此篇给出未标注来源的「加速比」。
六、本机 DuckDB 1.5.4 实验
脚本:post/db/query-engine/reproduce/run_duckdb_experiments.py。数据:tpch.orders
100 000 行、tpch.customer
5 000 行(内存生成,非官方 TPCH
文件)。
环境:WSL2(Linux 6.6.87.2)、i9-12900K / 32 GiB、DuckDB 1.5.4(Python 绑定)。
查询 q_join(customer ⋈ orders,日期 filter
+ GROUP BY + LIMIT 10):
| 指标 | 结果 |
|---|---|
EXPLAIN 叶算子 |
SEQ_SCAN(orders /
customer)、HASH_JOIN |
| Wall time(3 次取中位数) | 0.0028 s |
cd post/db/query-engine/reproduce
python3 run_duckdb_experiments.py解读(机制,非跨引擎排名):
- Scan 以 vectorized batch 读内存表;Join 走 HASH_JOIN,与 第 9 篇 build/probe 模型一致。
- 100k 行规模下全内存,未观测 spill;更大
fact 或更低
memory_limit才会触发外排 hash(DuckDB 文档 Optimization)。
同脚本的 q_filter(count(*) +
等值谓词)叶算子为 SEQ_SCAN + filter
pushdown 到 scan,可用于对照 第
8 篇 的谓词位置。
七、三种执行语义并排
| 维度 | Volcano 行(第 6 篇) | 向量化 batch | Morsel-driven |
|---|---|---|---|
| 并行粒度 | 算子内并行有限 | 批内 SIMD + 多 pipeline | 输入划分 + 窃取 |
| 内存局部性 | 行跳转 | 列扫描友好 | 同左,+ 划分 |
| 代表 | PG OLTP | Trino Page、CH Block | DuckDB |
| MPP | Exchange | + 网络 shuffle | 嵌入式为主 |
八、边界
- ClickHouse
MergeTree读路径与PREWHERE→ columnar-engine 第 5 篇。 - Hash join / agg 内存与 spill → 第 9 篇。
- Spark Tungsten 二进制 row → 第 13 篇。
参考资料
| 类型 | 来源 |
|---|---|
| 论文 | Boncz, P. A. et al.; MonetDB/X100; CIDR 2005 |
| 论文 | Leis, V. et al.; Morsel-Driven Parallelism; 2014 |
| 文档 | DuckDB 1.5.x Documentation — Internals, vectorized execution |
| 文档 | Trino 476+ Documentation — developer guide,
Page / Block |
| 系列 | columnar-engine/04 向量化执行 |
| 实验 | post/db/query-engine/reproduce/run_duckdb_experiments.py(本机
WSL2,2026-07-07) |
系列导航:系列索引 · 上一篇:Volcano 迭代器模型 · 下一篇:Scan、Filter 与 Project
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【分布式 OLAP 查询引擎】DuckDB 与 DataFusion:嵌入式分析对照
从单进程向量化 pipeline 与 morsel-driven 并行出发,对照 DuckDB 1.5.4 与 Apache DataFusion 的 planner/executor 边界;说明何时选嵌入式读湖、何时必须上 Trino MPP;与 columnar-engine DuckDB 存储篇分工,并用本机实测 EXPLAIN 与 Parquet 投影下推数据锚定结论。
【分布式 OLAP 查询引擎】OLAP 查询引擎全景:从单进程到 MPP
从 OLTP/OLAP/HTAP 边界、嵌入式 DuckDB 与分布式 Trino/Spark 分工、批式扫描与交互式查询延迟口径出发,闭合 lakehouse 与 stream-processing 之间的查询层缺口,并给出本系列 18 篇地图。
【分布式 OLAP 查询引擎】SQL 解析、分析与逻辑计划
从 Parser/AST、Analyzer 与 Catalog 元数据到 LogicalPlan 算子树;对照 PostgreSQL parse/rewrite/plan 边界,并用 DuckDB 1.5.4 实测 EXPLAIN 与 Trino 476+ 文档中的 logical plan 结构对读。
【分布式 OLAP 查询引擎】Join 重排与物理算子选择
Join order enumeration、Hash/Merge/Nested Loop 适用条件;Trino broadcast vs partitioned join 与 shuffle 网络代价;Dynamic partition pruning 与 runtime filter;DuckDB HASH_JOIN 实测与 Spark AQE 对照边界。