【Rust 2026教程:从零构建 Mini-OLAP 引擎】第 3 章 表达式系统:把 SQL 表达式变成可执行树

1.1 背景知识

SQL 中的 WHERE amount > 100 AND city = 'BJ'SELECT amount * 1.13 都是标量 表达式。数据库内核需要一个统一抽象来表示并求值它们。

主流方案有两种:

  1. 解释执行(Tree-walking interpreter) :把表达式建成树,递归 eval。简单直观,DuckDB 默认走这条
  2. JIT 编译:用 LLVM/Cranelift 把表达式编译成机器码。极致性能但复杂度高,ClickHouse、Apache DataFusion 部分场景使用

入门阶段先实现解释执行------配合向量化 batch,单机 10 亿行/秒的吞吐已经触手可及。

表达式树节点

表达式树的节点类型一般有:

  • ColumnRef:引用某一列(按下标或名字)
  • Literal:常量
  • BinaryOp+ - * / > < = AND OR
  • Functionupper(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,递归实现。

为了简洁,本章只支持 i64f64 两种数值类型 + 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 自测题

  1. 为什么字面量也要"广播"成与 batch 等长的列?直接保留标量行不行?
  2. 表达式树解释执行的主要开销在哪里?JIT 能优化掉哪些?
  3. Add(Col(0), Mul(Col(1), LitF64(0.13))) 这种嵌套表达式,每个子节点都会产生一个临时 Column,会带来什么性能问题?如何缓解?
  4. 如果要支持 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,思考短路求值在向量化下的取舍
相关推荐
GreenTea1 小时前
【Rust 2026教程:从零构建 Mini-OLAP 引擎】第 2 章 向量化执行:让 CPU 跑满
后端
GreenTea1 小时前
【Rust 2026教程:从零构建 Mini-OLAP 引擎】第 1 章 列式存储:OLAP 的物理基石
后端
rocky_rocky2 小时前
ComboBox的异步延迟加载机制
后端
接着奏乐接着舞2 小时前
spring cloud知识点
后端·spring·spring cloud
ltl3 小时前
位置编码:为什么需要它,为什么用正弦
后端
明月_清风3 小时前
Go 函数设计的工程智慧:多返回值、闭包与那些"反直觉"的选择
后端·go
却尘3 小时前
一个 `&` 引发的血案:改完配置 pipeline 装聋作哑,顺便重学了 Python/Go/Java
后端·go
倚栏听风雨3 小时前
Spring AI 实战:用 JdbcChatMemory + MySQL 给 AI 接上「长期记忆」
后端
我叫黑大帅4 小时前
最简单的生产-消费者,你都会遇到哪些问题?
后端·面试·go