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. 核心类型体系

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.h | Block 定义 |
be/src/vec/columns/column.h | IColumn 接口 |
be/src/vec/columns/column_vector.h | ColumnVector<T> — 定长数值列 |
be/src/vec/columns/column_string.h | ColumnString — 变长字符串列 |
be/src/vec/data_types/data_type.h | IDataType 接口 |
be/src/vec/data_types/data_type_number.h | DataTypeNumber<T> |
源码导航
| 文件 | 关键类/方法 | 职责 |
|---|---|---|
be/src/vec/core/block.h | Block, get_by_position(), columns() | 向量化数据块 |
be/src/vec/columns/column.h | IColumn | 列接口基类 |
be/src/vec/columns/column_vector.h | ColumnVector<T> | 定长数值列 |
be/src/vec/columns/column_string.h | ColumnString | 变长字符串列 |
be/src/vec/data_types/data_type.h | IDataType | 数据类型接口 |
be/src/vec/exprs/vexpr.h | VExpr, VExprContext | 向量化表达式 |
be/src/vec/functions/function.h | IFunction, execute_impl() | 函数接口 |
be/src/vec/aggregate_functions/aggregate_function.h | IAggregateFunction | 聚合函数接口 |
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 指令。主要有两种方式:
- 编译器自动向量化: 对于简单的逐元素操作, C++ 编译器 (Clang 17+) 在 -O3 下自动将循环转为 SIMD
- 手写 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 的类型推断会选择最合适的物理类型。

下一步
- 存储格式: doc-06-storage-tablet-rowset-segment.md——读的列数据在磁盘上怎么存