StarRocks array_append 函数详解:向数组末尾追加元素的实现原理与实战用法
【免费下载链接】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
array_append是 StarRocks 数组(ARRAY)系列函数中最基础也最常用的函数之一,其作用是在数组的末尾追加一个新元素并返回新的数组。本文以官方函数文档(docs/en/sql-reference/sql-functions/array-functions/array_append.md)为核心骨架,结合 FE(Frontend)解析与 BE(Backend)向量化执行层的源码实现,系统讲解array_append的语法、返回值、NULL 语义、类型匹配规则以及底层执行原理,帮助读者在 StarRocks 中正确、高效地使用该函数。
一、函数概述
array_append将一个新的元素追加到数组的末尾,返回一个包含原数组全部元素和新元素的新数组。该函数属于 StarRocks ARRAY 类函数家族,与 array_concat(拼接两个数组)、array_remove(按值移除元素)、array_length(返回数组长度)等函数共同构成数组处理工具集。
函数语法
array_append(any_array, any_element)| 参数 | 说明 |
|---|---|
any_array | 任意类型的数组(ARRAY 类型),例如[1, 2]、['A', 'B']、[[1, 2], [3]](嵌套数组) |
any_element | 要追加到数组末尾的元素,其类型需要与数组元素类型一致或可隐式转换 |
返回值为一个新的数组,其类型与第一个参数any_array保持一致。
基本示例
在 mysql 客户端中执行:
mysql> select array_append([1, 2], 3); +------------------------+ | array_append([1,2], 3) | +------------------------+ | [1,2,3] | +------------------------+ 1 row in set (0.00 sec)可以看到,元素3被追加到[1, 2]的末尾,得到[1, 2, 3]。原数组本身不会被修改,array_append返回的是包含原数组全部元素和新元素的全新数组,这一语义在下面的源码分析中会得到印证。
二、NULL 元素的追加语义
与原文档明确说明的内容一致,array_append允许向数组中追加NULL值:
mysql> select array_append([1, 2], NULL); +---------------------------+ | array_append([1,2], NULL) | +---------------------------+ | [1,2,NULL] | +---------------------------+ 1 row in set (0.01 sec)此时结果数组为[1, 2, NULL],NULL会被作为一个真实的数组元素追加到末尾,而不是让整个表达式结果变成NULL。这一点与很多初学者直觉上的"任何运算遇到 NULL 结果都是 NULL"的三值逻辑不同:在 StarRocks 的array_append语义中,NULL是合法的数组元素内容。
需要区分两种NULL场景:
- 追加值为 NULL:如
array_append([1, 2], NULL),结果是把 NULL 作为新元素追加,得到[1, 2, NULL]; - 数组本身为 NULL:如
array_append(NULL, 'A'),由于被操作的数组是 NULL,整个表达式结果为NULL。
上述两种场景在 FE 的常量折叠测试中都有覆盖,见 ConstArrayFunctionFoldingTest.java 中的用例:
{"array_append(['A', 'B'], 'C')", "['A','B','C']"}, {"array_append(['A', 'B'], NULL)", "['A','B',NULL]"}, {"array_append(NULL, NULL)", "<slot 2> : NULL"}, {"array_append(NULL, 'A')", "<slot 2> : NULL"},从测试断言可以看出:追加NULL时结果数组包含 NULL 元素;而数组本身是 NULL 时(无论追加什么),整个表达式都被常量折叠为NULL。
三、字符串与空数组等边界场景
array_append对字符串数组同样适用。在 FE 的优化器常量折叠测试中,字符串场景被折叠为字面量数组:
{"array_append(['A', 'B'], 'C')", "['A','B','C']"},对于空数组[],array_append([], element)会返回仅包含该元素的新数组,这在 ArrayTypeTest.java 中通过select array_append([], null)、select array_append([], [])等用例进行了验证。
该测试还覆盖了嵌套数组场景(见 ArrayTypeTest.java):
select array_append([[1,2,3]], []) -- 嵌套数组:向数组追加一个空数组元素 select array_append([[1,2,3]], [null]) -- 追加包含 NULL 的子数组这说明array_append支持 ARRAY 嵌套类型:当数组元素本身也是数组时,any_element可以是一个数组字面量,如[]、[null]等。
四、元素类型匹配与隐式转换
array_append对第二参数的类型有约束:追加的元素类型必须与数组元素类型一致,或者可以隐式转换到数组元素类型。StarRocks 的优化器会在生成执行计划时自动插入类型转换(CAST),这一行为在 ArrayTypeTest.java 中有明确的计划级断言。
以DECIMAL64数组为例:
-- 假设表 adec 中 d_2 列为 ARRAY<DECIMAL64(4,3)> 类型 select array_append(d_2, 1.0) from adec;优化器生成的计划中包含array_append[([5: d_2, ARRAY<DECIMAL64(4,3)>, true], 1.0);,此时字面量1.0会按DECIMAL64(4,3)的精度语义参与计算。
对于VARCHAR数组:
-- 假设表 adec 中 s_1 列为 ARRAY<VARCHAR(65533)> 类型 select array_append(s_1, 1.0) from adec;计划中可以看到array_append[([3: s_1, ARRAY<VARCHAR(65533)>, true], '1.0');,即数字字面量1.0被隐式转换为字符串'1.0'后再追加。
类型匹配的实践建议
- 保持类型一致:追加的元素类型与数组元素类型一致时性能与语义最直观,如
array_append(['A', 'B'], 'C'); - 依赖隐式转换:StarRocks 会自动进行数值与字符串、不同精度的 DECIMAL 等之间的隐式转换,但建议显式使用
CAST以避免精度损失歧义; - 嵌套数组:当数组元素是 ARRAY 时,第二参数应传入数组字面量(如
[]、[null]),而不是标量值。
五、底层实现原理(BE 向量化执行)
在 BE 端,array_append的声明位于 be/src/exprs/array_functions.h,通过DEFINE_VECTORIZED_FN(array_append)注册为向量化函数;其核心实现位于 be/src/exprs/array_functions.cpp。
5.1 主流程:ArrayFunctions::array_append
实现主流程可以概括为以下几个步骤:
- NULL 数组短路:如果第一个参数(数组列)只有 NULL 值(
only_null()),直接返回该列,即整个表达式结果为 NULL(对应前文"数组本身为 NULL"的语义); - 解包常量列:通过
ColumnHelper::unpack_and_duplicate_const_column将常量数组列解包为普通数据列,便于统一处理; - 剥离可空包装:如果数组列是
NullableColumn,先剥离 null 标志,取出内部的ArrayColumn数据列; - 获取元素列与偏移列:从
ArrayColumn中取出elements()(元素列)与offsets()(偏移列)——StarRocks 的 ARRAY 列底层采用"元素数组 + 偏移数组"的紧凑存储结构,每个数组通过[offsets[i], offsets[i+1])区间定位; - 按第二参数形态分派:
- 第二参数
only_null()(即追加 NULL)→do_array_append<true, true> - 第二参数是常量列 →
do_array_append<false, true> - 第二参数是普通列 →
do_array_append<false, false>
- 第二参数
- 恢复可空包装:若原数组列可空,将原数组的 null 标志列复用到结果上,返回
NullableColumn。
5.2 核心循环:do_array_append
模板函数do_array_append(be/src/exprs/array_functions.cpp)实现了真正的追加逻辑,其核心是逐数组遍历的循环:
for (size_t i = 0; i < num_array; i++) { uint32_t next_offset = offsets_data[i + 1]; uint32_t array_size = next_offset - curr_offset; // 1. 把第 i 个数组的原有元素整体拷贝到结果元素列 result_elements->append(elements, curr_offset, array_size); // 2. 根据参数形态追加一个元素:NULL / 常量 / 逐行取值 if constexpr (OnlyNullData) { result_elements->append_nulls(1); } else if constexpr (ConstData) { result_elements->append(*const_data, 0, 1); } else { result_elements->append(data, i, 1); } // 3. 更新偏移数组:每个结果数组长度 = 原数组长度 + 1 result_offset += array_size + 1; result_offsets.push_back(result_offset); curr_offset = next_offset; }从实现可以确认两个关键事实:
- 不修改原数组:函数创建全新的
ArrayColumn作为结果(result_array),将原数组元素整体拷贝过去,因此原数组列不受影响,符合函数式语义; - 批量处理:该实现面向"一列多行"的向量化执行,一次调用即可处理整批数组(如某个表中某一列的所有数组值),并通过
reserve预分配容量、append批量拷贝,避免逐元素分散写入; - NULL 元素本质:追加 NULL 时调用
append_nulls(1),在结果元素列中追加一个 NULL 标记,这正是[1, 2, NULL]结果的列式存储形态。
需要注意的是,源码注释中标注了FIXME: A proof-of-concept implementation with poor performance(be/src/exprs/array_functions.cpp),即当前实现被作者标注为"概念验证、性能欠佳",后续版本有进一步向量化优化的空间。读者在实际使用中如果对超大批量数组追加的性能有较高要求,可以留意新版本的实现变更。
六、FE 侧注册与常量折叠优化
array_append在 FE 侧通过内置函数注册表FunctionSet注册,常量标识符定义在 FunctionSet.java:
public static final String ARRAY_APPEND = "array_append";StarRocks 优化器还针对数组常量表达式实现了**常量折叠(Constant Folding)**优化:当array_append的两个参数都是常量时,不需要下推到 BE 执行,而是在 FE 端直接计算出结果。该逻辑位于 FoldConstantsRule.java:
.put("array_append", this::constArrayAppend)constArrayAppend处理函数定义在 FoldConstantsRule.java。这就是为什么前文测试用例中array_append(['A', 'B'], 'C')会被直接折叠成字面量['A','B','C']、array_append(NULL, 'A')会被折叠为NULL——这些常量表达式在查询计划生成阶段就被求值完毕,几乎不消耗执行期资源。
七、与相邻数组函数的分工
array_append在数组处理函数家族中承担"末尾追加单元素"的职责,与相近函数的使用场景对比如下:
| 函数 | 用途 | 与 array_append 的区别 |
|---|---|---|
array_append(arr, e) | 向数组末尾追加一个元素 | 本函数主题 |
| array_concat | 拼接两个数组 | 第二参数是数组而非标量元素 |
| array_remove | 删除数组中所有等于指定值的元素 | 方向相反,返回删除后的数组 |
| array_length | 返回数组长度 | 常与 append 配合验证结果 |
实际开发中,array_append常用于:在聚合或窗口计算的中间结果上追加最新值、为数组补位后再做其他数组运算、配合unnest(unnest.md)展开后继续处理等场景。
八、使用注意事项小结
- 函数名不区分大小写:
array_append、ARRAY_APPEND均可用,文档 keyword 为ARRAY_APPEND, ARRAY; - 返回新数组:原数组不会被修改,需要将结果赋给新列或用于后续表达式;
- NULL 语义:追加 NULL 得到含 NULL 元素的数组;数组本身为 NULL 时整体结果为 NULL;
- 类型对齐:追加元素应尽量与数组元素类型一致,依赖隐式转换时注意精度与字符串化规则;
- 空数组:
array_append([], e)返回单元素数组[e]; - 常量优化:全常量参数在 FE 端即被折叠,无需关心执行期开销;涉及真实表列的批量场景才会走 BE 端向量化实现。
九、参考与延伸阅读
- 函数官方文档:docs/en/sql-reference/sql-functions/array-functions/array_append.md
- BE 向量化实现:be/src/exprs/array_functions.cpp 与 be/src/exprs/array_functions.h
- FE 函数注册:fe/fe-core/src/main/java/com/starrocks/catalog/FunctionSet.java
- FE 常量折叠:fe/fe-core/src/main/java/com/starrocks/sql/optimizer/rewrite/scalar/FoldConstantsRule.java
- 相关测试:ConstArrayFunctionFoldingTest.java、ArrayTypeTest.java
- 数组函数目录:docs/en/sql-reference/sql-functions/array-functions/array-functions.mdx
【免费下载链接】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),仅供参考