
1.1 背景知识
SQL 中的 WHERE amount > 100 AND city = 'BJ'、SELECT amount * 1.13 都是标量 表达式。数据库内核需要一个统一抽象来表示并求值它们。
主流方案有两种:
- 解释执行(Tree-walking interpreter) :把表达式建成树,递归 eval。简单直观,DuckDB 默认走这条
- JIT 编译:用 LLVM/Cranelift 把表达式编译成机器码。极致性能但复杂度高,ClickHouse、Apache DataFusion 部分场景使用
入门阶段先实现解释执行------配合向量化 batch,单机 10 亿行/秒的吞吐已经触手可及。
表达式树节点
表达式树的节点类型一般有:
- ColumnRef:引用某一列(按下标或名字)
- Literal:常量
- BinaryOp :
+ - * / > < = AND OR - Function :
upper(s)、abs(x)...
求值结果统一为一个 Column(注意:标量也要"广播"成与 batch 等长的列,便于后续算子统一处理)。

1.2 设计思路
rs
enum Expr {
Col(usize),
LitI64(i64), LitF64(f64), LitStr(String),
Gt(Box<Expr>, Box<Expr>),
Eq(Box<Expr>, Box<Expr>),
And(Box<Expr>, Box<Expr>),
Add(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
}
求值函数 eval(&self, batch: &Batch) -> Column,递归实现。
为了简洁,本章只支持 i64 和 f64 两种数值类型 + Utf8 等值比较。生产引擎会通过 macro 或代码生成扩展到上百种类型组合(DuckDB 有专门的 "type matrix" 模板)。
1.3 代码实现
src/expr.rs(核心片段):
rs
#[derive(Debug, Clone)]
pub enum Expr {
Col(usize),
LitI64(i64), LitF64(f64), LitStr(String),
Gt(Box<Expr>, Box<Expr>),
Eq(Box<Expr>, Box<Expr>),
And(Box<Expr>, Box<Expr>),
Add(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
}
impl Expr {
pub fn eval(&self, batch: &Batch) -> Column {
match self {
Expr::Col(i) => batch.arrays()[*i].clone(),
Expr::LitI64(v) => broadcast_i64(*v, batch.len()),
Expr::LitF64(v) => broadcast_f64(*v, batch.len()),
Expr::LitStr(s) => broadcast_str(s, batch.len()),
Expr::Gt(l, r) => cmp(l, r, batch, Op::Gt),
Expr::Eq(l, r) => cmp(l, r, batch, Op::Eq),
Expr::Add(l, r) => arith(l, r, batch, ArithOp::Add),
Expr::Mul(l, r) => arith(l, r, batch, ArithOp::Mul),
Expr::And(l, r) => bool_and(&l.eval(batch), &r.eval(batch)),
}
}
}
cmp / arith 内部按 (lhs_type, rhs_type) 做手动 dispatch,分别处理 Int64×Int64、Float64×Float64、Utf8×Utf8 三组(完整代码见仓库 src/expr.rs)。
把 Filter 改造成接收 Expr,立刻得到一个通用的过滤算子。表达 amount > 100 AND city = 'BJ':
rs
use Expr::*;
let pred = And(
Box::new(Gt(Box::new(Col(2)), Box::new(LitF64(100.0)))),
Box::new(Eq(Box::new(Col(1)), Box::new(LitStr("BJ".into())))),
);
1.4 自测题
- 为什么字面量也要"广播"成与 batch 等长的列?直接保留标量行不行?
- 表达式树解释执行的主要开销在哪里?JIT 能优化掉哪些?
Add(Col(0), Mul(Col(1), LitF64(0.13)))这种嵌套表达式,每个子节点都会产生一个临时Column,会带来什么性能问题?如何缓解?- 如果要支持
NULL语义(三值逻辑),上面的实现需要哪些改动?
1.5 拓展学习
- 论文:Everything You Always Wanted to Know About Compiled and Vectorized Queries But Were Afraid to Ask(VLDB 2018)
- DataFusion 的 PhysicalExpr trait 设计
- 动手:实现
Case When ... Then ... Else,思考短路求值在向量化下的取舍