【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 小时前
我用 ASP.NET Core 做了个水稻病虫害检查系统
后端·asp.net
谢亮_vipxieliang9 小时前
用 PHP 构建轻量级 REST API:从路由到鉴权的完整实践
开发语言·后端·php
码事漫谈11 小时前
三步改掉 AI 味,附可直接复制的去 AI 味提示词
后端
IT_陈寒11 小时前
Redis卡顿的锅,这次真不是大key的错
前端·人工智能·后端
可乐鸡翅yeah_11 小时前
video.js 集成 hls.js 开发 M3U8 播放器,新手高频踩坑
开发语言·前端·javascript·后端·ecmascript·m3u8·音视频在线播放
程序猿_极客12 小时前
【免费】分享一套优质的基于SpringBoot的服装商城管理系统的设计与实现(带可视化图表、协同过滤功能),源码+文档+视频详解(讲解)
java·spring boot·后端·服装商城管理系统
明月_清风12 小时前
面对陌生的 GitHub 项目无从下手?这 4 个网站帮你快速读懂源码
前端·后端·github
bug菌12 小时前
🤔同事突然问我:Spring的注解 @Component 和 @Service 有何不同?
java·spring boot·后端
leobertlan13 小时前
痛苦系列 | DSP-02 频域切片:DTFT与DFT的探索
android·后端
加我攻城狮13 小时前
当大模型把「景区」定位到附近公共厕所:我的 AI 旅行小程序踩坑实录
后端·微信