TL;DR
DuckDB 是一个嵌入式 OLAP 数据库,融合了列存储的 PAX 布局与向量化(Vectorized)执行引擎,通过 Morsel-Driven 并行实现高效分析查询。
三层设计
┌─────────────────────────────────────┐
│ Morsel-Driven 并行 │
│ ┌─────────┐ ┌─────────┐ │
│ │ Thread 1│ │ Thread 2│ ... │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
│ ┌────▼───────────▼────┐ │
│ │ 向量化执行引擎 │ │
│ │ (Vector Size = 2048)│ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ PAX 列式存储布局 │ │
│ └─────────────────────┘ │
└─────────────────────────────────────┘
PAX(Partition Attributes Across)布局
DuckDB 不采用纯列存(DSM),而是 PAX:每个 Row Group 内部按列组织:
Row Group (122,880 rows)
┌──────────────────────────────────────┐
│ Column Chunk 1 (trip_id): │
│ [val1, val2, ..., val122880] │
├──────────────────────────────────────┤
│ Column Chunk 2 (timestamp): │
│ [ts1, ts2, ..., ts122880] │
├──────────────────────────────────────┤
│ Column Chunk 3 (amount): │
│ [amt1, amt2, ..., amt122880] │
└──────────────────────────────────────┘
为什么是 PAX 而不是纯列存?
- 纯列存(DSM):按列分文件,不同列在不同文件 → 多列查询需多次 IO
- 行存(NSM):按行存 → scan 时拖入不需要的列
- PAX:折中方案,每 Row Group 内列连续存储 → 单次 IO 可读多列
向量化执行
DuckDB 的向量化执行引擎每次处理 2048 行(STANDARD_VECTOR_SIZE),而非逐行处理:
逐行执行(Volcano):
for row in table:
col_a = row.a ← 函数调用 × N
col_b = row.b
result = col_a + col_b
向量化执行(DuckDB):
vector_a = table.a[0:2048] ← 一次函数调用
vector_b = table.b[0:2048]
result = vector_add(vector_a, vector_b) ← SIMD 加速
优势:
- 减少虚函数调用(2048 行一次 vs 每行一次)
- CPU cache 友好(批量数据在 L1/L2 中)
- 可利用 SIMD 指令(SSE/AVX)批量计算
Morsel-Driven 并行
// 查询执行时,数据被切分为 Morsels
// 每个 Morsel 约包含几千行,由工作线程动态分配
while (true) {
auto morsel = pipeline->GetNextMorsel();
if (!morsel) break; // 没有更多数据
ExecuteOperator(morsel); // 在当前线程执行
}
这种模式相比分区域并行(每个线程固定分一块数据)更灵活,能自动处理数据倾斜。
性能特征
| 场景 | DuckDB 优势 |
|---|---|
| 全表扫描 | 列式扫描,跳过无关列 |
| 聚合计算 | SIMD 加速,向量批量聚合 |
| Join | Morsel-Parallel Hash Join |
| 单机分析 | 零配置,进程内嵌入 |
总结
DuckDB 通过 PAX 列式存储 + 向量化执行 + Morsel 并行的三层设计,在嵌入式场景中实现了接近大型 OLAP 系统的分析性能。对于数据分析和 ETL 场景,它是 SQLite 在 OLTP 地位的 OLAP 对应物。