Doris 实战与架构

分析型数据库 / 源码阅读 / LESSON 05

向量化计算框架——如何以列向量为单位执行查询

解释 Block、Column、SIMD 与表达式执行如何让 Doris 以列向量为单位处理查询。

阅读时间
40 分钟
学习路径
Doris 实战与架构
内容来源
Doris 深度笔记

预计阅读时间: 40 分钟 前置阅读: doc-04 (Pipeline 引擎) 下一次阅读: doc-06 (存储格式)


1. 为什么向量化——行式→列式→向量化的演进

行式计算 (Row-at-a-time):
  for i in 0..N:
    row = rows[i]
    if row.a > 10:
      result.append(row.b + row.c)
  → 每次处理 1 行, CPU 分支预测频繁失效, 函数调用开销 N 倍

列式计算 (Column-at-a-time):
  col_a = column("a")  // 整列的 int 值
  col_b = column("b")
  col_c = column("c")
  mask = col_a > 10        // SIMD: 一次比较 8 个 int
  result = col_b + col_c   // SIMD: 一次加法 8 个 int
  result.filter(mask)
  → 每次处理一整列(或一个 Block), SIMD 友好, 分支少

Doris 选的是列式 (向量化)——所有数据以 Block(多列的向量块)为单位传递, 每个 Operator 操作一个 Block。


2. 核心类型体系

向量化计算框架——如何以列向量为单位执行查询 图 01

Block 结构

Block (类比 Pandas DataFrame, 每个查询操作的单位)
├── IColumn `dt`      (ColumnVector<Date>)   → 4096 个 Date 值
├── IColumn `status`  (ColumnString)         → 4096 个 String 值
├── IColumn `amount`  (ColumnVector<Float64>)→ 4096 个 Float64 值
├── IColumn `city`    (ColumnString)         → 4096 个 String 值
└── block_info        (行数 = 4096)

所有列长度严格相同 (列存保证), 操作一个 Block = 操作 4096 行

类型层级

IDataType (接口)
├── DataTypeNumber<T>   → 数值类型 (Int8, Int16, Int32, Int64, Float32, Float64)
├── DataTypeString      → 字符串 (变长, 支持 Binary)
├── DataTypeDecimal     → 定点数 (高精度 decimal)
├── DataTypeDate        → 日期
├── DataTypeDateTime    → 日期时间
└── DataTypeArray       → 数组 (嵌套)

IColumn (接口)
├── ColumnVector<T>    → 定长列 (int/float/date)
├── ColumnString       → 变长列 (字符串)
├── ColumnNullable     → 可空列 (包装任何 IColumn, NULL bit 标记)
├── ColumnConst        → 常量列 (所有行相同值, 零内存)
└── ColumnArray        → 数组列

源码定位

文件用途
be/src/vec/core/block.hBlock 定义
be/src/vec/columns/column.hIColumn 接口
be/src/vec/columns/column_vector.hColumnVector<T> — 定长数值列
be/src/vec/columns/column_string.hColumnString — 变长字符串列
be/src/vec/data_types/data_type.hIDataType 接口
be/src/vec/data_types/data_type_number.hDataTypeNumber<T>

源码导航

文件关键类/方法职责
be/src/vec/core/block.hBlock, get_by_position(), columns()向量化数据块
be/src/vec/columns/column.hIColumn列接口基类
be/src/vec/columns/column_vector.hColumnVector<T>定长数值列
be/src/vec/columns/column_string.hColumnString变长字符串列
be/src/vec/data_types/data_type.hIDataType数据类型接口
be/src/vec/exprs/vexpr.hVExpr, VExprContext向量化表达式
be/src/vec/functions/function.hIFunction, execute_impl()函数接口
be/src/vec/aggregate_functions/aggregate_function.hIAggregateFunction聚合函数接口

3. VectorizedExpr 与函数体系

表达式树

VExpr (向量化表达式基类)
├── VLiteral              → 常量: 42, "hello"
├── VSlotRef              → 列引用: table.column
├── VCompoundPred         → 复合谓词: AND/OR
├── VInPredicate          → IN 谓词: col IN (1,2,3)
└── VScalarFnCall         → 函数调用: abs(col), concat(a,b)

函数分类

分类接口特点示例
简单一元函数FunctionSimpleUnary输入 1 列, 输出 1 列 (逐元素)abs, upper, round
简单二元函数FunctionBinaryArithmetic输入 2 列, 输出 1 列 (逐元素)+, -, *, =, >
聚合函数IAggregateFunction输入多行, 输出 1 个值SUM, COUNT, AVG, MAX
窗口函数IWindowFunction输入分区, 输出相同行数ROW_NUMBER, RANK
哈希函数FunctionHash输入 1 行→输出 hash 值murmur_hash, xxHash

函数注册与调用

初始化: 函数在 BE 启动时注册到 FunctionFactory
    FunctionFactory::register_function<FunctionAbs>("abs");
    FunctionFactory::register_function<FunctionSum>("sum");

查询时: FE Nereids 生成的 Thrift TExpr 中带函数名 →
    BE 解析 TExpr → FunctionFactory::create("abs") → IFunction
        → VExprContext::execute(Block&) → ColumnVector<double> 的结果

4. SIMD (AVX2/AVX512) 集成点

Doris 的向量化框架在关键路径中集成了 SIMD 指令。主要有两种方式:

  1. 编译器自动向量化: 对于简单的逐元素操作, C++ 编译器 (Clang 17+) 在 -O3 下自动将循环转为 SIMD
  2. 手写 SIMD Intrinsic: 对于热点路径 (Filter/Join Key Build 等), 使用 _mm256_* (AVX2) 或 _mm512_* (AVX512) 手写

be/src/vec/ 中搜索 _mm256 可定位到所有手写 SIMD 的代码位置。

SIMD 的加速效果: 对于纯数值运算 (如 Filter, Agg Sum), 通常有 2-4x 加速 (相比标量循环)。


5. 新增一个函数的完整流程 (案例 walkthrough)

以给 abs(Int32) → Int32 添加向量化实现为例:

Step 1: 在 be/src/vec/functions/ 下创建 function_abs.h
  → 定义 FunctionAbs : public IFunction
  → 实现 execute_impl(Block& block, ...)
  → 内部: 取第0列 → 循环: result[i] = abs(input[i]) → 设置输出列

Step 2: 注册函数 (在 BE 启动时)
  → FunctionFactory::instance().register_function<FunctionAbs>("abs");

Step 3: FE Nereids 识别函数
  → 在 Nereids 的函数注册表中添加 abs (自动, 如果已有定义)
  → 用户写: SELECT abs(amount) FROM t
  → Nereids 生成 TExpr(func_name="abs", children=[TExpr(SlotRef(amount))])

Step 4: BE 接收 TExpr → 创建 FunctionAbs → execute_impl → 返回新列

6. 与 Segment 列存格式的配合

向量化执行 + 列存 Segment 形成"端到端的列式处理":

Segment 文件 (磁盘列存)
  ↓ 读 Column `amount` page
IColumn (内存, 整列 4096 行)
  ↓ Block.get_by_position(2)
VExpr 操作 (SIMD 逐元素)
  ↓ 新 IColumn (结果列)
Block 输出 (4096 行结果)

关键: 数据从磁盘到 Expression 计算都是列格式, 没有"行转列"或"列转行"的开销。


7. 常见问题 / 面试题

Q1: 向量化对字符串操作优化大吗? A: 不如数值操作显著。字符串是变长的 (不能直接用 SIMD 处理), 但比较操作 (如 str1 > str2) 可以实现为 memcmp 的批量优化。对于字符串处理, Doris 的优化重点在 ColumnString 的内存布局 (连续的 data buffer 减少碎片)。

Q2: 什么情况下向量化退化为行式? A: 当列基数非常低 (如 1-10 个 distinct 值), Doris 的字典编码会自动将列编码为整数, 然后对编码后的整数进行向量化操作。因此几乎不会退化到行式。极端情况是每行数据完全不同且为复杂 JSON 格式, 此时向量化优势最小但仍然适用。

Q3: 为什么有的表达式用 ColumnVector<Int32> 而不是 ColumnVector<Int64>? A: 最小化内存占用和 SIMD 吞吐量 (256-bit AVX2 寄存器一次处理 8 个 Int32 或 4 个 Int64, 所以 Int32 吞吐量更高)。Nereids 的类型推断会选择最合适的物理类型。


向量化计算: Block → ColumnVector → SIMD

下一步