9.2 Pipeline、Push 执行与查询代码生成
表达式 JIT 仍保留 operator boundary。更激进的 query compilation 让 operators 通过 produce/consume 生成一个 pipeline loop:scan 产生 tuple/batch,filter 直接判断,projection 直接计算,sink 直接写入 hash table 或 result。
Pull、push 与 compiled pipeline
Pull iterator
parent 向 child 请求下一行/批:
root.next()
-> filter.next()
-> scan.next()Push execution
source 主动把 batch 推给 downstream consumer:
scan -> filter -> projection -> sinkpush 不自动等于 native code generation。DuckDB 当前文档描述的是 push-based vectorized model,DataChunks 在 physical operators 间流动;这与“整条查询编译为 LLVM machine code”是两件事。
Compiled produce/consume
operator 参与生成共同 loop:
for (const auto& row : scan(table)) {
if (row.level >= 30) {
hash_table.insert(row.adventurer_id, row.name);
}
}上面是生成结果的概念代码,不是某数据库 source API。
Pipeline breaker
pipeline 可以在不要求看完全部 input 的 operators 间 streaming。以下状态通常形成边界:
- hash join build:probe 前需建表;
- full sort:输出首行前需形成/合并 runs;
- hash aggregate:最终 groups 需要完整/partition input;
- materialize/spool;
- window function 的某些 frame/order;
- exchange/shuffle;
- blocking UDF/external call。
示例:
Pipeline A: Scan(adventurers) -> Filter -> HashBuild
[breaker: hash table]
Pipeline B: Scan(quests) -> HashProbe -> AggregateBuild
[breaker: group states]
Pipeline C: AggregateScan -> Sort -> Result
[breaker: sorted runs]breaker 不一定把 data 写成通用 rows;中间状态可能是 hash table、run files、compressed vectors 或 partition buffers。
Fusing 的收益
- 减少 materialized intermediate tuples;
- 消除 operator dispatch;
- column values 保持在 registers/local variables;
- filter 后才 decode expensive output columns;
- constants/types/NULLability 可 specialize;
- compiler 可跨 expressions 做 common-subexpression elimination。
Fusing 的风险
- giant function 增加 compile time/code size;
- instruction cache pressure;
- register pressure 导致 spills;
- rare branches/UDF 让 code复杂;
- skew/runtime cardinality 与 compile assumptions 偏离;
- debugging/profiling/generated-code mapping 困难。
最优 pipeline 不是“能融合多少就融合多少”。engine 可在 operator/fragment 处切分,保持 code size 与 compile latency。
HyPer 的研究路线
HyPer 的 produce/consume code generation 将 relational operators 编译为 data-centric pipelines,核心目标是让 data 留在 CPU registers/cache,并避免 iterator overhead。它是 query compilation 研究的重要代表。
阅读入口:Thomas Neumann, Efficiently Compiling Efficient Query Plans for Modern Hardware(VLDB 2011)。论文结果建立在特定 prototype/workload/hardware 上,不能直接写成“任何 TPC-H 都比 PostgreSQL 快 10–100 倍”。
DuckDB 不应归类为“全查询 JIT”
DuckDB 使用 vectorized execution,Vector/DataChunk 是主要传输格式,当前 internals 文档描述 push-based vectorized physical execution。它可以通过 typed/vector kernels、constant/dictionary vectors 和 operator pipelines 获得低 overhead,但这不是 LLVM query JIT。
官方资料:
“C++ templates 在编译 DuckDB binary 时展开”也不等于 runtime 把每条 SQL 编译成专属 native function。
Adaptive/tiered compilation
系统可以先用 interpreter/vector engine立即执行,同时后台编译 hot pipeline,达到阈值后切换:
start quickly with generic code
-> collect rows/types/selectivity/skew
-> compile hot path
-> switch at safe boundary类似 JVM tiered compilation 的思想降低 cold-query latency,但实现要保证:
- state 在 generic/compiled code 间 compatible;
- snapshot/exception semantics 不变;
- compiled assumptions 可 guard/deopt;
- code cache 有 eviction;
- concurrent compilation 有资源上限。
Compilation cache key
可复用 generated code 至少依赖:
- logical/physical plan shape;
- schema/type/nullability;
- functions/operators/collation;
- constants 是否 baked in;
- CPU ISA/features;
- security/tenant context;
- engine version/config。
cache key 太具体命中低,太宽又可能错误复用。schema migration、extension/UDF replacement 和 CPU heterogeneous cluster 都需 invalidation/dispatch。
Parameter specialization
把 constant parameter 编进 code 可做 branch elimination,却为每个 value 生成 code;generic compiled function 复用高,但 optimization 少。
可选择:
- generic parameters;
- specialize hot values/shape;
- runtime guard + fallback;
- code cache size/TTL;
- profile-guided recompile。
这与 optimizer generic/custom plan 问题相连:先选 plan,再决定 code specialization,两个层次都可能受 parameter skew 影响。
编译不改变 SQL 语义
generated code 必须保留:
- three-valued NULL logic;
- overflow/decimal precision;
- collation/timezone;
- floating NaN/order;
- volatile function 调用次数与顺序约束;
- exception/error timing允许的边界;
- transaction snapshot 与 cancellation;
- memory/resource accounting。
compiler 的常量折叠和重排不能使用 C/C++ 的普通算术语义替代数据库 type semantics。
Benchmark 框架
分开报告:
parse/bind/optimize time
code generation/optimization/emission time
first-row latency
steady-state execution CPU
total wall time
code cache hit/miss and bytes
rows/batches, branch/cache counters
peak memory and spill
result correctness至少包含:short OLTP、medium repeated query、long CPU scan、I/O-bound query、complex UDF 和 parameter skew。只测 long scan 会夸大 compilation 收益。
验收清单
- [ ] push/vectorized 与 JIT/codegen 分开描述;
- [ ] pipeline breakers 按状态依赖识别;
- [ ] compile time 计入端到端 latency;
- [ ] code size/cache/invalidation 有边界;
- [ ] SQL NULL/type/collation semantics 被保留;
- [ ] 首次执行与复用执行分别测量;
- [ ] DuckDB 不再被称为整查询 LLVM 编译器。
本章小结
查询编译从 expression JIT 逐步扩展到 pipeline code generation。它能减少 dispatch/materialization,却引入 compile latency、code-cache 与 specialization 风险。下一章回到另一条 CPU 优化路线:不为每行生成专属控制流,而是用 vector batches 和 SIMD 对同一类型数据批量处理。