Doris architecture

ANALYTICAL DATABASE / SOURCE READING / LESSON 05

Vectorized execution

Explain how blocks, columns, SIMD, and expression execution drive Doris vectorized query processing.

Reading
40 min
Track
Doris architecture
Source
Chinese source notes

The source notes for this track are currently maintained in Chinese.

预计阅读时间: 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

下一步