StarRocks array_append 函数详解:向数组末尾追加元素的实现原理与实战用法
2026/9/17 14:45:17 网站建设 项目流程

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

实现主流程可以概括为以下几个步骤:

  1. NULL 数组短路:如果第一个参数(数组列)只有 NULL 值(only_null()),直接返回该列,即整个表达式结果为 NULL(对应前文"数组本身为 NULL"的语义);
  2. 解包常量列:通过ColumnHelper::unpack_and_duplicate_const_column将常量数组列解包为普通数据列,便于统一处理;
  3. 剥离可空包装:如果数组列是NullableColumn,先剥离 null 标志,取出内部的ArrayColumn数据列;
  4. 获取元素列与偏移列:从ArrayColumn中取出elements()(元素列)与offsets()(偏移列)——StarRocks 的 ARRAY 列底层采用"元素数组 + 偏移数组"的紧凑存储结构,每个数组通过[offsets[i], offsets[i+1])区间定位;
  5. 按第二参数形态分派
    • 第二参数only_null()(即追加 NULL)→do_array_append<true, true>
    • 第二参数是常量列 →do_array_append<false, true>
    • 第二参数是普通列 →do_array_append<false, false>
  6. 恢复可空包装:若原数组列可空,将原数组的 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)展开后继续处理等场景。

八、使用注意事项小结

  1. 函数名不区分大小写array_appendARRAY_APPEND均可用,文档 keyword 为ARRAY_APPEND, ARRAY
  2. 返回新数组:原数组不会被修改,需要将结果赋给新列或用于后续表达式;
  3. NULL 语义:追加 NULL 得到含 NULL 元素的数组;数组本身为 NULL 时整体结果为 NULL;
  4. 类型对齐:追加元素应尽量与数组元素类型一致,依赖隐式转换时注意精度与字符串化规则;
  5. 空数组array_append([], e)返回单元素数组[e]
  6. 常量优化:全常量参数在 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),仅供参考

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

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

立即咨询