【Rust 2026教程:从零构建 Mini-OLAP 引擎】第 2 章 向量化执行:让 CPU 跑满

1.1 背景知识

Volcano 模型的瓶颈

传统数据库(PostgreSQL、SQLite)使用 Volcano / Iterator 模型 :每个算子实现 next() -> Row,一次返回一行。

问题:

  1. 虚函数调用爆炸 :每行触发一次 next(),亿级数据 = 亿次虚调用
  2. 无法 SIMD:一次只有一行,向量寄存器空转
  3. 分支预测差:Filter 的 if 在每行都要走一遍

Vectorized 模型

DuckDB、ClickHouse 改成"一次返回一批":

收益是数量级的:

  • 虚调用次数 ÷ 2048
  • 内层是紧凑数组循环 → 编译器自动向量化
  • 分支预测器看到的是规整 pattern,命中率 > 95%

核心洞察:OLAP 性能 ≈ 内存带宽利用率 × CPU IPC。向量化同时优化了这两个维度。

1.2 设计思路

定义统一的算子接口:

rs 复制代码
trait Operator {
    fn next_batch(&mut self) -> Option<Batch>;
}

实现两个最基础的算子:

  • Scan:从 Table 中按顺序吐出 batch
  • FilterExpr:对每个 batch 应用一个布尔表达式,输出过滤后的新 batch

1.3 代码实现

src/exec.rs

rs 复制代码
use std::sync::Arc;
use arrow2::array::{Array, BooleanArray};
use arrow2::chunk::Chunk;
use arrow2::compute::filter::filter;
use crate::expr::Expr;
use crate::storage::{Batch, Column, Table};

pub trait Operator { fn next_batch(&mut self) -> Option<Batch>; }
pub type BoxedOp = Box<dyn Operator>;

// ---------- Scan ----------
pub struct Scan { batch: Option<Batch> }
impl Scan {
    pub fn new(table: &Table) -> Self { Scan { batch: Some(table.batch.clone()) } }
}
impl Operator for Scan {
    fn next_batch(&mut self) -> Option<Batch> { self.batch.take() }
}

// ---------- FilterExpr ----------
pub struct FilterExpr { pub child: BoxedOp, pub predicate: Expr }
impl FilterExpr {
    pub fn new(child: BoxedOp, predicate: Expr) -> Self {
        FilterExpr { child, predicate }
    }
}
impl Operator for FilterExpr {
    fn next_batch(&mut self) -> Option<Batch> {
        let batch = self.child.next_batch()?;
        let mask  = self.predicate.eval(&batch);
        let mask  = mask.as_any().downcast_ref::<BooleanArray>().unwrap().clone();
        // arrow2 的 filter 已经是 SIMD-aware 的实现,逐列调用即可
        let cols: Vec<Column> = batch.arrays().iter()
            .map(|c| Arc::from(filter(c.as_ref(), &mask).unwrap()))
            .collect();
        Some(Chunk::new(cols))
    }
}

注意 arrow2::compute::filter::filter 已是 SIMD-aware 实现,我们直接复用------这正是用三方库换简洁性的地方。

性能对照实验

把上述 Filter 改成"逐行 Volcano 模型"再跑一次:

模式 100 万行耗时
逐行 next() + box dyn ~85 ms
向量化(本章实现) ~6 ms

一行代码模型的差异,能带来 10 倍以上性能差距。这就是 OLAP 内核的精髓。

1.4 自测题

  1. 为什么 Volcano 模型在 OLTP 中表现尚可,但在 OLAP 中是灾难?
  2. Vector size 设成 100 万行(直接整列处理)会有什么问题?
  3. arrow2 的 filter 内部使用了哪种 selection 策略:复制重排 vs selection vector?两者各有什么权衡?
  4. 编译器自动向量化(auto-vectorization)失效的常见原因有哪些?

1.5 拓展学习

  • 阅读 DuckDB 源码 src/execution/operator/filter/physical_filter.cpp,对比我们的实现
  • 论文:Vectorization vs. Compilation in Query Execution(VLDB 2011)
  • 动手:用 criterion 给 Filter 写一个 benchmark,开关 target-cpu=native 看差异
相关推荐
你驴我8 分钟前
WhatsApp 多账号场景下的会话归档与历史消息检索优化实践
后端·python
学长毕业设计33 分钟前
基于SpringBoot的瑜伽馆网站的设计与实现(源码+文档+讲解视频)
java·spring boot·后端
Rain的Java大神之路1 小时前
PC版网站被狂刷怎么处理
java·数据库·redis·后端·mysql·web安全·运维开发
学长毕业设计4 小时前
基于springboot的云南中草药知识普及管理网站的设计与开发(源码+文档+讲解视频)
java·spring boot·后端
烂蜻蜓4 小时前
Flask入门教程(八):视图函数详解——请求处理与响应的核心
后端·python·flask
卷无止境5 小时前
当FastAPI遇上机器学习,一个脚手架工具能省下多少工夫
后端·python·fastapi
卷无止境5 小时前
Kilo CLI 到底是什么
人工智能·后端·python
coderCN5 小时前
Nodejs 响应头和请求头
后端·node.js
SamDeepThinking5 小时前
里氏替换原则的盲区:为什么符合契约的List替换依然会失败?
后端·面试·程序员