【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 排序)
相关推荐
程序员老赵12 小时前
Docker 部署 XXL-JOB:轻松搭建分布式任务调度平台
后端·docker·开源
用户2333768521812 小时前
Docker镜像找不到排障实录-CRLF跨平台暗雷
后端
程序员cxuan12 小时前
从 0 到 1 速通 WorkBuddy
人工智能·后端·程序员
薛定谔的算法12 小时前
M01:环境搭建与第一个 Java 程序
后端
盗_心i13 小时前
OCPP 1.6-J 配置管理手册:GetConfiguration / ChangeConfiguration 与标准配置键
后端
传奇开心果编程13 小时前
【Go入门练中学】第1课:从零开始
后端·学习·golang
isfox13 小时前
Python 的 @classmethod 和 @staticmethod:什么时候用哪个,一次说清楚
后端
薛定谔的算法13 小时前
前端视角秒懂:Java 和 Spring Boot 的关系,就像 JS 和 React
后端
颜进强13 小时前
从零搭一套 WorkBuddy Skill 骨架:调接口 → 生成 HTML 报告的分层设计与流转拆解
前端·后端·ai编程