StarRocks 字典表达式扩展模块(ExprDict)深入解析:模块边界、核心实现与执行原理
2026/9/15 19:11:14 网站建设 项目流程

StarRocks 字典表达式扩展模块(ExprDict)深入解析:模块边界、核心实现与执行原理

【免费下载链接】starrocksThe world's fastest open query engine for sub-second analytics both on and off the data lakehouse. With the flexibility to support nearly any scenario, StarRocks provides best-in-class performance for multi-dimensional analytics, real-time analytics, and ad-hoc queries. A Linux Foundation project.项目地址: https://gitcode.com/GitHub_Trending/st/starrocks

本篇技术指南以 StarRocks 仓库中 be/src/exprs_ext/dict/AGENTS.md 模块说明文档为骨架,结合be/src/exprs_ext/dict/目录下的源码实现,系统讲解后端(BE)字典表达式扩展模块(ExprDict)的职责边界、依赖规则、工厂注册机制,以及DictMappingExprDictQueryExprDictionaryGetExprdict_encode四类字典相关表达式从openevaluate_checked的完整执行链路。读者读完本文将掌握:StarRocks 全局字典优化与字典缓存功能在后端是如何落地为具体表达式节点的、各节点与Expr/ComputeEnv/Storage等底层模块的协作关系,以及新增字典表达式时应当遵守的模块边界约束。

一、模块定位:什么是 ExprDict

be/src/exprs_ext/dict/目录对应 BE 模块清单(be/module_boundary_manifest.json)中 id 为exprdict、doc_label 为ExprDict的模块。官方摘要对其职责的定义是:

Dictionary expression extensions and dict expression factory registration above Expr, ComputeEnv, and Storage integration.

即:建立在ExprComputeEnvStorage之上的字典表达式扩展,以及字典表达式的工厂注册逻辑。它属于 StarRocks 后端exprs_ext(表达式扩展)体系的一部分,与exprs_ext/dict平级的扩展目录还包括 table function 等扩展,其共同点是在核心表达式基础设施之上提供需要具体存储/计算环境支撑的高级表达式能力。

从模块边界清单可以看到该模块的完整约束(be/module_boundary_manifest.json):

维度内容
模块 id / 标签exprdict/ExprDict
模块文档be/src/exprs_ext/dict/AGENTS.md
拥有的构建目标ExprDict
允许的内部 include 前缀exprs_ext/dict/exprs/compute_env/storage/storage_primitive/exec/exec_env.hplatform/runtime/column/types/common/base/gutil/gen_cpp/
允许依赖的构建目标ExprComputeEnvStorageStoragePrimitiveExecPlatformRuntimeColumnSortCoreChunkCoreColumnCoreTypesCommonBaseGutilStarRocksGen
禁止 include 前缀service/http/agent/connector/

这一边界约束的含义可以归纳为两点:

  1. 向上依赖受限:字典表达式可以自由依赖列/Chunk/Types 等底层基础设施(column/types/base/等)、表达式基础设施(exprs/)、计算环境(compute_env/)与存储(storage/storage_primitive/),但不能反向侵入service/http/agent/connector/等服务层与连接器层,从而保证扩展模块的可复用性与可测试性。
  2. 职责划分清晰:如 AGENTS.md 中 remediation 条款所述——字典专属的表达式实现应放在 ExprDict 内;共享的表达式契约应归属 Expr 模块;更上层的查询执行编排(query execution orchestration)应位于扩展层之上。换言之,ExprDict 是"薄薄的一层字典专用适配",负责把字典语义翻译成底层Expr+ComputeEnv+Storage可以执行的形式。

二、目录结构与源码全景

be/src/exprs_ext/dict/下共有 9 个文件,按职责可分为四组:

文件职责
expr_factory_dict_exprs_extension.cpp表达式工厂注册:通过ExprFactory::set_non_core_create_post_hook挂钩三类字典表达式节点
dictmapping_expr.h / dictmapping_expr.cppDictMappingExpr:全局字典优化(Global Dictionary Optimization)中的字典映射表达式
dict_query_expr.h / dict_query_expr.cppDictQueryExpr:基于TableReader直接对字典表做 point 查询(multi_get)的表达式
dictionary_get_expr.h / dictionary_get_expr.cppDictionaryGetExpr:基于 ComputeEnv 中的 Dictionary Cache 做键值探测(probe)的表达式
dict_functions.h / dict_functions.cppDictFunctionsdict_encode标量函数,把常量 VARCHAR 预编码为全局字典码

这些表达式统一继承自核心表达式基类Expr(见 be/src/exprs/expr.h),并重写prepare/open/evaluate_checked/close生命周期方法——这正是它们能无缝接入 BE 向量化执行引擎(Chunk批处理执行)的前提。

三、工厂注册机制:三类字典表达式如何被创建

与核心内置表达式不同,字典表达式属于"非核心扩展",因此采用注册式挂钩而非直接写死在工厂 switch 中。expr_factory_dict_exprs_extension.cpp 展示了完整机制:

Status expr_factory_dict_create_post_hook(ObjectPool* pool, const TExprNode& texpr_node, Expr** expr, RuntimeState* state) { (void)state; if (*expr != nullptr) { return Status::OK(); } switch (texpr_node.node_type) { case TExprNodeType::DICT_EXPR: *expr = pool->add(new DictMappingExpr(texpr_node)); break; case TExprNodeType::DICT_QUERY_EXPR: *expr = pool->add(new DictQueryExpr(texpr_node)); break; case TExprNodeType::DICTIONARY_GET_EXPR: *expr = pool->add(new DictionaryGetExpr(texpr_node)); break; default: break; } return Status::OK(); } struct ExprFactoryDictExprsExtensionRegistrar { ExprFactoryDictExprsExtensionRegistrar() { ExprFactory::set_non_core_create_post_hook(expr_factory_dict_create_post_hook); } }; ExprFactoryDictExprsExtensionRegistrar k_expr_factory_dict_exprs_extension_registrar;

要点分析:

  • 通过一个文件级静态对象k_expr_factory_dict_exprs_extension_registrar的构造函数,在程序加载期把expr_factory_dict_create_post_hook注册为ExprFactorynon_core_create_post_hook,实现"零侵入"的扩展点接入;
  • 挂钩函数是post-hook:只有当核心工厂尚未创建出表达式(*expr == nullptr)时,才按TExprNodeType分发创建;这保证了扩展与核心工厂的优先级不冲突;
  • 三种节点类型对应三种表达式类:DICT_EXPR → DictMappingExprDICT_QUERY_EXPR → DictQueryExprDICTIONARY_GET_EXPR → DictionaryGetExpr
  • 这些节点类型定义于 gensrc/thrift/Exprs.thrift 的TExprNodeType枚举中,说明 FE 生成的 Thrift 计划节点与 BE 的表达式创建是一一对应的。

四、DictMappingExpr:全局字典优化中的字典映射

4.1 设计背景

DictMappingExpr服务于Global Dictionary Optimization(全局字典优化)。在低基数字符串列的加速场景下,StarRocks 会把字符串列编码为全局字典(Global Dictionary),查询执行直接基于整型字典码进行,避免逐行字符串比较。其类注释(dictmapping_expr.h)阐明了节点结构:

The original expression will be rewritten as a dictionary mapping function in the global field optimization. child(0) was input lowcardinality dictionary column (input was ID type). child(1) was origin expr (input was string type).

即:

  • child(0):输入的低基数字典列(列内存储的是 ID 整型码);
  • child(1)原始字符串表达式

在全局字典优化过程中,用字典列作为输入来构造新的字典映射时,BE 需要保留原始表达式(child(1)),以便在执行期把字典码映射回真实值并继续求值。

4.2 关键接口与懒重写机制

class DictMappingExpr final : public Expr, public DictMappingExprInterface { public: template <class Rewrite> Status rewrite(Rewrite&& rewriter) { std::call_once(*_rewrite_once_flag, [&]() { DCHECK(dict_func_expr == nullptr); auto rewrite_result = rewriter(); _rewrite_status = rewrite_result.status(); if (_rewrite_status.ok()) { dict_func_expr = rewrite_result.value(); DCHECK(dict_func_expr != nullptr); } }); return _rewrite_status; } SlotId slot_id() const; // 取 child(0) 字典列的 slot id Expr* dict_mapping_origin_expr() const; // 返回 child(1) 原始表达式 void set_output_id(SlotId id); // 设置输出 slot void disable_open_rewrite(); // 禁用 open 阶段的自动重写 ... };

实现要点:

  • 同时继承Expr与接口类DictMappingExprInterface(定义于 be/src/exprs/dictmapping_expr_interface.h),后者提供了dict_mapping_slot_id()dict_mapping_origin_expr()rewrite_dict_mapping_expr()set_dict_mapping_output_id()disable_dict_mapping_open_rewrite()等抽象方法,使上层代码可以面向接口统一处理所有字典映射表达式
  • 重写是幂等的、线程安全的:用std::call_once+std::once_flag保证rewrite只真正执行一次,多次调用只会返回第一次的结果状态_rewrite_status
  • dict_func_expr是重写产出的"真正的字典函数表达式",其输入列是字典码列;slot_id()通过down_cast<const ColumnRef*>(get_child(0))直接取子节点的 slot,表明 child(0) 必须是ColumnRef类型。

五、DictQueryExpr:直连字典表的点查表达式

5.1 语义与 Thrift 参数

DictQueryExpr的语义是:以若干 key 字段为条件,对一张字典表执行 point query(multi_get),取回 value 字段。其执行参数封装在TDictQueryExpr(gensrc/thrift/Exprs.thrift):

struct TDictQueryExpr { 1: required string db_name // 字典表所在数据库 2: required string tbl_name // 字典表名 3: required map<i64, i64> partition_version // 分区版本 4: required list<string> key_fields // 参与查询的 key 字段列表 5: required string value_field // 要取回的 value 字段(仅单列) 6: required bool strict_mode // 严格模式:记录不存在时报错 }

5.2 执行期元数据获取:open()

open(dict_query_expr.cpp)负责初始化表读取器,关键流程:

  1. 通过 RPC 向 FE 拉取表元数据:构造TGetDictQueryParamRequest(含db_nametbl_name),调用ThriftRpcHelper::rpc<FrontendServiceClient>getDictQueryParam,得到包含 schema、分区参数(partition)、节点信息(location/nodes_info)的响应;RPC 超时 30000ms;
  2. 初始化TableReader:把 FE 返回的 schema、partition、nodes_info 以及_dict_query_expr.partition_version组装进TableReaderParams,创建并init一个TableReader实例(TableReader定义于 be/src/storage/table_reader.h),这正是 ExprDict 模块依赖Storage的体现;
  3. 构建 key/value schema:基于OlapTableSchemaParam解析出的列定义,用StorageSchemaHelper::convert_field把与key_fields匹配的列构造成_key_schema(每列一个 field),把value_field匹配的列构造成_value_schema
  4. 建立 slot id 映射:遍历响应中的slot_descs,为 key 列填充_key_slot_ids、为 value 列填充_value_slot_id

5.3 批处理求值:evaluate_checked()

evaluate_checked(dict_query_expr.cpp)是向量化执行的核心,流程如下:

  1. 求值子表达式:依次对children()求值得到各输入列(child(0) 之外是 key 列),常数列用ColumnHelper::unpack_and_duplicate_const_column展开为size行;
  2. 组装 key Chunk:key 列取自columns[1] ~ columns[1 + key_fields.size()],组装成带_key_schemaChunk,并注册 slot id 到列索引的映射;随后做空值校验:key 列不允许为 NULL,否则返回Status::InternalError("invalid parameter : get NULL paramenter");nullable 列会被转换为非 nullable;
  3. 执行 multi_get:调用_table_reader->multi_get(*key_chunk, {value_field}, found, *value_chunk)found数组标记每个 key 是否命中;
  4. 组装结果:以value_chunk列克隆出可空的结果列,逐行遍历:命中则append_datum真实值;未命中时,若strict_mode为 true(null_if_not_found = false)则返回Status::NotFound,否则append_nulls(1)补 NULL。

可以看到DictQueryExpr的"字典查询"走的是直连存储引擎的点查路径TableReader::multi_get),适合数据量较小的字典表按 key 精确取值。

六、DictionaryGetExpr:基于 Dictionary Cache 的探测表达式

6.1 语义与 Thrift 参数

DictionaryGetExpr服务于ComputeEnv 中的字典缓存(Dictionary Cache):把 key 列组装成 chunk,在内存字典缓存中做批量探测(probe),返回值字段聚合为一个 STRUCT 列。其参数封装在TDictionaryGetExpr(gensrc/thrift/Exprs.thrift):

struct TDictionaryGetExpr { 1: optional i64 dict_id // 字典缓存 ID 2: optional i64 txn_id // 事务版本号,用于定位具体版本的字典 3: optional i32 key_size // key 字段个数 4: optional bool null_if_not_exist // 未命中时是否置 NULL(否则走严格报错路径) }

6.2 prepare:绑定字典缓存与 schema

prepare(dictionary_get_expr.cpp)做静态初始化:

  1. 获取缓存管理器:经由state->exec_env()->compute_env()->dictionary_cache_manager()拿到DictionaryCacheManager(be/src/compute_env/dictionary_cache/dictionary_cache_manager.h);任一环节缺失即返回Status::InternalError("...missing compute dictionary cache manager"),体现 ExprDict 对ComputeEnv的依赖;
  2. 按 id 取 schemaget_dictionary_schema_by_id(dict_id),拿不到则报错 "there is no cache for dictionary: {dict_id}";
  3. 按版本取字典get_dictionary_by_version(dict_id, txn_id)拿到_dictionaryDictionaryCachePtr),保证读到的是txn_id对应的事务版本数据;
  4. 预建 Chunk 模板:用 schema 的前key_size个字段构造_key_chunk、其余字段构造_value_chunk,再基于 value 列模板构造一个nullable StructColumn_nullable_struct_column)——每个 value 子列先包一层 nullable,再作为 STRUCT 的 field,用于承载"某些行未命中"的情况。

6.3 evaluate_checked:缓存探测并输出 STRUCT

evaluate_checked(dictionary_get_expr.cpp):

  1. 求值 key 子表达式children()的第 0 项起是构造 key 的表达式;任一输入列含 NULL 即返回Status::InternalError("...get NULL paramenter");常数列展开、nullable 列剥壳;
  2. 批量探测:调用DictionaryCacheManager::probe_given_dictionary_cache(key_schema, value_schema, _dictionary, key_chunk, value_chunk, null_column),其中null_column仅在null_if_not_exist为 true 时传入,用于标记未命中行;
  3. 合并为 STRUCT:把value_chunk的每一列 append 到_nullable_struct_column对应 field 列中,并用 SIMD 指令(SIMD::contain_nonzero,见 be/src/base/simd/simd.h)扫描 null 标记列,设置 STRUCT 列的全局 null 标记,最后返回整个 nullable StructColumn。

DictQueryExpr的"落盘点查"不同,DictionaryGetExpr走的是内存缓存探测路径,返回值天然支持多列聚合为 STRUCT,更适合高频、低延迟的字典关联场景。

七、dict_encode:常量值预编码标量函数

除了三类表达式节点,exprs_ext/dict还提供DictFunctions静态类,其dict_encode是一个支持向量化的标量函数(DEFINE_VECTORIZED_FN)。函数签名与语义(dict_functions.h):

  • dict_encode(value, dict_slot_id):把常量 VARCHAR翻译为指定全局字典列(dict_slot_id标识)的字典码,供低基数重写后的array_contains(dict_array, dict_encode('foo', slot))这类比较直接在整型码上进行;
  • 值不在字典中时编码为std::numeric_limits<DictId>::max()哨兵值(保证"必定缺席",等值/成员判断语义正确,但不能用于大小比较);
  • NULL 输入编码为 NULL。

dict_encode_prepare(dict_functions.cpp)在FRAGMENT_LOCAL作用域初始化函数状态:

  1. 校验dict_slot_id必须是非空常量(否则Status::InternalError);
  2. 校验value必须是常量;若为 NULL 常量则直接记录is_null = true
  3. 通过runtime_state->fragment_dict_state()拿到FragmentDictState(be/src/compute_env/global_dict/fragment_dict_state.h),调用其mutable_dict_optimize_parser()->lookup_dict_code(runtime_state, slot_id, value)完成执行期常量查码

求值时(dict_functions.cpp)只需根据EncodeDictState返回常量列:NULL 输入返回create_const_null_column,否则返回TYPE_INT常量列code。该实现把昂贵的字符串查字典操作收敛在prepare阶段执行一次,运行时零字符串处理,是典型的"以预计算换执行速度"的设计。

八、模块边界的工程意义与扩展指南

8.1 依赖方向与可维护性

从 AGENTS.md 与 manifest 可以提炼出 ExprDict 的依赖铁律:

ExprDict(字典语义、工厂注册) ├── Expr(表达式生命周期契约:prepare/open/evaluate/close) ├── ComputeEnv(FragmentDictState、DictionaryCacheManager) └── Storage / StoragePrimitive(TableReader、StorageSchemaHelper) └── 更底层:column/ types/ base/ gutil/ platform/ gen_cpp/

同时禁止向上依赖service/http/agent/connector/。这样做的收益是:

  • 字典表达式可以被独立编译(ExprDict目标)与单测,不拖带整个 BE 服务层;
  • 共享的表达式抽象(如DictMappingExprInterface)沉淀在Expr模块,扩展与核心解耦;
  • 变更影响面可控:FE 只要把DICT_EXPR / DICT_QUERY_EXPR / DICTIONARY_GET_EXPR三类节点下发到 BE,BE 侧的注册挂钩会自动完成实例化,无需改动核心ExprFactory

8.2 如何新增一个字典表达式

遵循本模块的既有模式,新增字典表达式的推荐步骤是:

  1. be/src/exprs_ext/dict/下新建xxx_expr.h/.cpp,继承Expr,实现prepare/open/evaluate_checked/close
  2. 在 gensrc/thrift/Exprs.thrift 中补充节点类型与参数 struct(如TDictQueryExpr模式),FE 侧同步下发;
  3. 在 expr_factory_dict_exprs_extension.cpp 的switch中增加一个case分支完成创建;
  4. 保持 include 与 target 依赖不越界(对照 manifest 的 allow 列表),并可用仓库自带的边界校验脚本机械验证。

8.3 边界校验的工程化落地

AGENTS.md 明确指出本节内容由 be/module_boundary_manifest.json 自动生成,并给出了两条配套命令:

# 修改 manifest 后重新生成各模块 AGENTS.md python3 build-support/render_be_agents.py --write # 机械地校验模块边界规则是否被满足 python3 build-support/check_be_module_boundaries.py --mode full

其中check_be_module_boundaries.pyrender_be_agents.py均位于 build-support/ 目录。这套机制保证了"文档声明 → 代码约束 → CI 校验"三者一致:开发者阅读的 AGENTS.md 永远与 manifest 中的规则同步,任何越过边界的 include 或 target 依赖都会在构建前被脚本拦截。对于希望为 StarRocks 贡献字典类表达式(例如新的字典编码函数、新的字典查询策略)的开发者,这既是开发指南,也是提交前必须通过的自动检查。

九、小结

StarRocks 的 ExprDict 模块以一份 AGENTS.md 为核心契约,通过 module_boundary_manifest.json 精确划定了"字典表达式扩展"这一层级的职责与依赖:DictMappingExpr承载全局字典优化中的表达式重写(dictmapping_expr.h),DictQueryExpr通过TableReader直连字典表执行 multi_get 点查(dict_query_expr.cpp),DictionaryGetExpr依托DictionaryCacheManager做内存缓存批量探测并输出 STRUCT(dictionary_get_expr.cpp),dict_encode则在prepare期完成常量预编码(dict_functions.cpp),最后由 expr_factory_dict_exprs_extension.cpp 的 post-hook 统一接入ExprFactory。理解这一模块的边界与实现,是深入 StarRocks 全局字典优化与字典缓存执行链路的最佳切入点。

【免费下载链接】starrocksThe world's fastest open query engine for sub-second analytics both on and off the data lakehouse. With the flexibility to support nearly any scenario, StarRocks provides best-in-class performance for multi-dimensional analytics, real-time analytics, and ad-hoc queries. A Linux Foundation project.项目地址: https://gitcode.com/GitHub_Trending/st/starrocks

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询