【Rust 2026教程:从零构建 Mini-OLAP 引擎】第 5 章 SQL → 逻辑计划 → 物理计划

1.1 背景知识

到目前为止我们手写算子树,用户体验糟糕。一个真正的数据库需要让用户写 SQL。完整管道是:

DuckDB、DataFusion、Spark 都是这个套路,差别仅在于优化器的复杂程度和物理算子的丰富度。

本章只做最小闭环:

  • sqlparser-rs 直接解析 SQL
  • 实现一个超简化的 Planner:AST 直转算子树(跳过逻辑计划层和优化器)
  • 真正的优化器在第 6 章简单介绍

1.2 设计思路

支持的 SQL 子集:

sql 复制代码
SELECT col1, col2, AGG(col3)
FROM table
[WHERE condition]
[GROUP BY col1]

约束:

  • 单表,无 JOIN
  • 无 ORDER BY、LIMIT(留作练习)
  • 列引用按名字解析
  • 表名 → Table 通过简单的 Catalog 哈希表映射

1.3 代码实现

src/catalog.rs

rust 复制代码
use std::collections::HashMap;
use crate::storage::Table;

#[derive(Default)]
pub struct Catalog { tables: HashMap<String, Table> }

impl Catalog {
    pub fn register(&mut self, t: Table) { self.tables.insert(t.name.clone(), t); }
    pub fn get(&self, name: &str) -> Option<&Table> { self.tables.get(name) }
}

src/planner.rs(核心,约 80 行,完整版见仓库):

rust 复制代码
pub fn run_sql(sql: &str, catalog: &Catalog) -> Batch {
    let ast = Parser::parse_sql(&GenericDialect{}, sql).unwrap();
    let Statement::Query(q) = &ast[0] else { panic!("only SELECT") };
    let SetExpr::Select(sel) = q.body.as_ref() else { panic!() };

    // 1. FROM
    let TableFactor::Table { name, .. } = &sel.from[0].relation else { panic!() };
    let tname = name.0[0].value.clone();
    let table = catalog.get(&tname).unwrap();
    let resolve = |c: &str| table.col_index(c).unwrap();

    let mut op: BoxedOp = Box::new(Scan::new(table));

    // 2. WHERE
    if let Some(w) = &sel.selection {
        op = Box::new(FilterExpr::new(op, build_expr(w, &resolve)));
    }

    // 3. GROUP BY + AGG
    if let GroupByExpr::Expressions(g, _) = &sel.group_by {
        if !g.is_empty() {
            let key  = build_expr(&g[0], &resolve);
            let aggs = collect_aggs(&sel.projection, &resolve);
            op = Box::new(HashAggregate::new(op, key, aggs));
        }
    }

    op.next_batch().unwrap()
}

跑一段端到端 SQL:

rust 复制代码
let mut cat = Catalog::default();
cat.register(Table::from_columns("orders", vec![
    ("city_id", col_i64(&[1,2,1,2,1,3,3,3])),
    ("amount",  col_f64(&[10.,20.,30.,40.,50.,60.,70.,80.])),
]));

let result = run_sql(
    "SELECT city_id, SUM(amount), COUNT(amount) \
     FROM orders WHERE amount > 15 GROUP BY city_id",
    &cat,
);
println!("{:?}", result);

至此,MiniDuck 已经是一个可用的嵌入式 OLAP 引擎------能解析 SQL、构建算子树、向量化执行、输出列式结果。

1.4 自测题

  1. 为什么真实数据库要分"逻辑计划"和"物理计划"两层,而不是 AST 直转算子?
  2. 列裁剪(Column Pruning)应该在逻辑计划还是物理计划阶段做?为什么?
  3. 当前的 Table::clone 性能糟糕,怎么改成零拷贝?提示:Arc<Table>(仓库已用 Arc 实现)。
  4. 如果要支持子查询(SELECT ... FROM (SELECT ...)),Planner 需要怎么改?

1.5 拓展学习

  • DataFusion 的 LogicalPlanPhysicalPlan 模块
  • 论文:Volcano---An Extensible and Parallel Query Evaluation System(Goetz Graefe, 1994)
  • 动手:实现 ORDER BY + LIMIT,思考 TopN 算法(堆 vs 排序)
相关推荐
GreenTea1 小时前
【Rust 2026教程:从零构建 Mini-OLAP 引擎】第 4 章 哈希聚合:GROUP BY 的核心
后端
IT_陈寒2 小时前
Vue的v-for为什么不加key也能工作?我差点翻车
前端·人工智能·后端
GreenTea2 小时前
【Rust 2026教程:从零构建 Mini-OLAP 引擎】第 3 章 表达式系统:把 SQL 表达式变成可执行树
后端
GreenTea2 小时前
【Rust 2026教程:从零构建 Mini-OLAP 引擎】第 2 章 向量化执行:让 CPU 跑满
后端
GreenTea2 小时前
【Rust 2026教程:从零构建 Mini-OLAP 引擎】第 1 章 列式存储:OLAP 的物理基石
后端
rocky_rocky2 小时前
ComboBox的异步延迟加载机制
后端
接着奏乐接着舞2 小时前
spring cloud知识点
后端·spring·spring cloud
ltl3 小时前
位置编码:为什么需要它,为什么用正弦
后端
明月_清风3 小时前
Go 函数设计的工程智慧:多返回值、闭包与那些"反直觉"的选择
后端·go