关于数据库:技术内幕|StarRocks-标量函数与聚合函数

52次阅读

共计 16521 个字符,预计需要花费 42 分钟才能阅读完成。

作者:徐嘉 StarRocks Active ContributorStarRocks

函数就像预设于数据库中的公式,容许用户调用现有的函数以实现特定性能。函数能够很不便地实现业务逻辑的重用,因而正确应用函数会让读者在编写 SQL 语句时起到事倍功半的成果。
StarRocks 提供了多种内置函数,包含标量函数、聚合函数、窗口函数、Table 函数和 Lambda 函数等,可帮忙用户更加便捷地处理表中的数据。此外,StarRocks 还容许用户自定义函数以适应理论的业务操作。本文将以标量函数和聚合函数为例,介绍 StarRocks 常见的两种函数实现原理,心愿读者可能借鉴其设计思路,并按需实现所需的函数。同时,咱们也欢送社区小伙伴一起贡献力量,独特欠缺 StarRocks 的性能,具体的函数工作认领形式请见文末。

如何为 StarRocks 增加标量函数

标量函数介绍

标量函数用于解决单行数据,承受一个或多个参数作为输出,并返回一个值作为后果。StarRocks 常见的标量函数有 abs、floor、ceil 等。

标量函数的实现原理

首先,咱们来理解函数签名,函数签名用来惟一标识函数,形容函数的 ID、名字、返回类型、输出参数的类型等根本信息。标量函数的函数签名定义在 gensrc/script/functions.py,在编译阶段咱们会依据 Python 文件中的内容生成对应的 Java 和 C++ 代码,供 FE 和 BE 应用。每个函数签名在 Python 文件中通过一个特定的数组来形容,数组的内容有如下两种格局:

[<function_id>, <function_name>, <return_type>, [<arg_type>...], <be_scalar_function>]
or
[<function_id>, <function_name>, <return_type>, [<arg_type>...], <be_scalar_function>, <be_prepare_function>, <be_close_function>]

其中根本信息如下:

  • function_id:函数惟一标识,是惟一一串数字,function_id 遵循如下约定,前两位示意 function_type,两头两位示意 function_group,余下的示意具体的 sub_function,前面咱们会举例说明
  • function_name:函数名称
  • return_type:返回值类型
  • arg_type:入参类型,如果有多个入参,须要在数组中形容每个入参的类型
  • be_scalar_function:BE 中负责实现该函数计算逻辑的函数
  • be_prepare_function/be_close_function:可选参数,有些函数在执行的过程中可能会传递一些状态,be_prepare_function 和 be_close_function 就是 BE 中负责实现创立状态和回收状态的函数

为了反对多种数据类型作为输出,须要为每种类型独自创立函数签名。以下以 abs 函数为例,该函数用于计算绝对值,须要形容以下五个信息:

  • function_id:10 代表它们都属于 math function,04 代表它们都属于 abs 这个 function group,余下的数字用来辨别具体的 sub-function
  • function_name:函数名称都是 abs
  • return_type:返回值类型,同入参类型统一
  • arg_type:该函数只承受一个入参,所以第四项的数组中只有一个元素。
  • be_eval_function:BE 中实现计算逻辑的函数,StarRocks 针对每种数据类型做了非凡解决,所以每个签名中的函数名也不一样
    对于 abs 函数而言,因为不须要传递状态,因而不须要 be_prepare_function 和 be_close_function 这两个选项。请留神,这两个选项在某些状况下可能会用到,具体用法将在前面的示例中介绍。
    [10040, "abs", "DOUBLE", ["DOUBLE"], "MathFunctions::abs_double"],
    [10041, "abs", "FLOAT", ["FLOAT"], "MathFunctions::abs_float"],
    [10042, "abs", "LARGEINT", ["LARGEINT"], "MathFunctions::abs_largeint"],
    [10043, "abs", "LARGEINT", ["BIGINT"], "MathFunctions::abs_bigint"],
    [10044, "abs", "BIGINT", ["INT"], "MathFunctions::abs_int"],
    [10045, "abs", "INT", ["SMALLINT"], "MathFunctions::abs_smallint"],
    [10046, "abs", "SMALLINT", ["TINYINT"], "MathFunctions::abs_tinyint"],
    [10047, "abs", "DECIMALV2", ["DECIMALV2"], "MathFunctions::abs_decimalv2val"],
    [100470, "abs", "DECIMAL32", ["DECIMAL32"], "MathFunctions::abs_decimal32"],
    [100471, "abs", "DECIMAL64", ["DECIMAL64"], "MathFunctions::abs_decimal64"],
    [100472, "abs", "DECIMAL128", ["DECIMAL128"], "MathFunctions::abs_decimal128"],

在 StarRocks 的编译和执行阶段,都会应用函数签名来确定函数的输入输出和执行逻辑。具体流程如下:

  1. 在编译阶段,依据 gensrc/script/functions.py 中的内容生成代码供 FE 和 BE 应用。
  2. Java 代码在 fe/fe-core/target/generated-sources/build/com/starrocks/builtins/VectorizedBuiltinFunctions.java,FunctionSet[1] 保留了所有的函数签名,初始化阶段会调用 VectorizedBuiltinFunctions::initBuiltins 来增加标量函数的函数签名。SQL analyze 阶段,会利用 FunctionSet 提供的信息进行校验,如果找不到函数签名会间接返回谬误,这部分实现在 ExpressionAnalyzer.Visitor [2]的 visitFunctionCall[3] 办法中。
  3. C++ 代码在./gensrc/build/gen_C++/opcode/builtin_functions.cpp,BE 标量函数的函数签名保留在 BuiltinFunctions::_fn_tables[4],生成的代码用于初始化_fn_tables。在 SQL 执行阶段,VectorizedFunctionCallExpr 会依据 fid(函数惟一标识)从 _fn_tables 中找到执行该函数所须要的信息,包含输出参数的个数,执行函数的函数指针(ScalarFunction),以及执行前后的 PrepareFunction 和 CloseFunction,这部分定义在 FunctionDescriptor[5]。

在 BE 实现函数的计算逻辑

这部分此处不做赘述,依据函数的性能实现相干的逻辑即可。

增加标量函数示例

接下来咱们以 sha2 函数为例,介绍引入新函数的具体流程。sha2 函数的性能如下图,其详细信息能够参考官网文档 [6] 中的介绍。

生成函数签名

首先,须要在 gensrc/script/functions.py 中新增签名。

[120160, "sha2", "VARCHAR", ["VARCHAR", "INT"], "EncryptionFunctions::sha2", "EncryptionFunctions::sha2_prepare", "EncryptionFunctions::sha2_close"],

如上述代码所示,sha2 函数输出须要两个参数,依据第二个参数来决定应用哪种加密算法,如果第二个参数自身是个常数,那么不须要每次执行的时候都去判断。咱们能够把这部分“状态”保存起来,所以函数签名中除了前文所述的五个根本信息之外,还减少了 EncryptionFunctions::sha2_prepare 和 EncryptionFunctions::sha2_close,用来实现状态的创立和回收。

实现函数的计算逻辑

sha2 属于加密函数的一种,所以咱们间接在 EncryptionFunctions [7]中减少相应的办法即可。具体代码如下:

    /*
     * Called by sha2 to the corresponding part
     */
    DEFINE_VECTORIZED_FN(sha224);
    DEFINE_VECTORIZED_FN(sha256);
    DEFINE_VECTORIZED_FN(sha384);
    DEFINE_VECTORIZED_FN(sha512);
    DEFINE_VECTORIZED_FN(invalid_sha);
    /**
     * @param: [json_string, tagged_value]
     * @paramType: [BinaryColumn, BinaryColumn]
     * @return: Int32Column
     */
    DEFINE_VECTORIZED_FN(sha2);
    static Status sha2_prepare(FunctionContext* context, FunctionContext::FunctionStateScope scope);
    static Status sha2_close(FunctionContext* context, FunctionContext::FunctionStateScope scope);

其中,实现标量函数的计算逻辑次要散布在 PrepareFuntion、ScalarFunction、CloseFunction 三个函数中。
PrepareFunction
Prepare 阶段次要是针对第二个参数进行非凡解决,如果是常数,能够把实现对应加密算法的函数指针保存起来,前面的 ScalarFunction 中能够间接调用。加密算法的函数指针保留在 EncryptionFunctions::SHA2Ctx 中,通过 FunctionContext::set_function_state 保留在上下文中。具体代码如下:

Status EncryptionFunctions::sha2_prepare(FunctionContext* context, FunctionContext::FunctionStateScope scope) {if (scope != FunctionContext::FRAGMENT_LOCAL) {return Status::OK();
    }

    if (!context->is_notnull_constant_column(1)) {return Status::OK();
    }

    ColumnPtr column = context->get_constant_column(1);
    auto hash_length = ColumnHelper::get_const_value<TYPE_INT>(column);

    ScalarFunction function;
    if (hash_length == 224) {function = &EncryptionFunctions::sha224;} else if (hash_length == 256 || hash_length == 0) {function = &EncryptionFunctions::sha256;} else if (hash_length == 384) {function = &EncryptionFunctions::sha384;} else if (hash_length == 512) {function = &EncryptionFunctions::sha512;} else {function = EncryptionFunctions::invalid_sha;}

    auto fc = new EncryptionFunctions::SHA2Ctx();
    fc->function = function;
    context->set_function_state(scope, fc);
    return Status::OK();}

ScalarFunction
ScalarFunction 次要实现 sha2 的计算逻辑,如果第二个参数是常数,那么 PrepareFunction 中保留的 function_state 就能够派上用场了。具体代码如下:

StatusOr<ColumnPtr> EncryptionFunctions::sha2(FunctionContext* ctx, const Columns& columns) {if (!ctx->is_notnull_constant_column(1)) {auto src_viewer = ColumnViewer<TYPE_VARCHAR>(columns[0]);
        auto length_viewer = ColumnViewer<TYPE_INT>(columns[1]);

        auto size = columns[0]->size();
        ColumnBuilder<TYPE_VARCHAR> result(size);

        for (int row = 0; row < size; row++) {if (src_viewer.is_null(row) || length_viewer.is_null(row)) {result.append_null();
                continue;
            }

            auto src_value = src_viewer.value(row);
            auto length = length_viewer.value(row);

            if (length == 224) {
                SHA224Digest digest;
                digest.update(src_value.data, src_value.size);
                digest.digest();
                result.append(Slice(digest.hex().c_str(), digest.hex().size()));
            } else if (length == 0 || length == 256) {
                SHA256Digest digest;
                digest.update(src_value.data, src_value.size);
                digest.digest();
                result.append(Slice(digest.hex().c_str(), digest.hex().size()));
            } else if (length == 384) {
                SHA384Digest digest;
                digest.update(src_value.data, src_value.size);
                digest.digest();
                result.append(Slice(digest.hex().c_str(), digest.hex().size()));
            } else if (length == 512) {
                SHA512Digest digest;
                digest.update(src_value.data, src_value.size);
                digest.digest();
                result.append(Slice(digest.hex().c_str(), digest.hex().size()));
            } else {result.append_null();
            }
        }

        return result.build(ColumnHelper::is_all_const(columns));
    }

    auto ctc = reinterpret_cast<SHA2Ctx*>(ctx->get_function_state(FunctionContext::FRAGMENT_LOCAL));
    return ctc->function(ctx, columns);
}

CloseFunction
CloseFunction 次要用来回收资源。函数执行中所依赖的 function state,在执行完结之后不再被须要,那么能够在这个阶段开释内存。具体代码如下:

Status EncryptionFunctions::sha2_close(FunctionContext* context, FunctionContext::FunctionStateScope scope) {if (scope == FunctionContext::FRAGMENT_LOCAL) {auto fc = reinterpret_cast<SHA2Ctx*>(context->get_function_state(scope));
        delete fc;
    }

    return Status::OK();}

减少对应的单元测试

具体细节可参考 EntryptionFunctionTest[8] 即可。代码示例如下:

TEST_P(ShaTestFixture, test_sha2) {auto [str, len, expected] = GetParam();

    std::unique_ptr<FunctionContext> ctx(FunctionContext::create_test_context());
    Columns columns;

    auto plain = BinaryColumn::create();
    plain->append(str);

    ColumnPtr hash_length =
            len == -1 ? ColumnHelper::create_const_null_column(1) : ColumnHelper::create_const_column<TYPE_INT>(len, 1);

    if (str == "NULL") {columns.emplace_back(ColumnHelper::create_const_null_column(1));
    } else {columns.emplace_back(plain);
    }
    columns.emplace_back(hash_length);

    ctx->set_constant_columns(columns);
    ASSERT_TRUE(EncryptionFunctions::sha2_prepare(ctx.get(), FunctionContext::FunctionStateScope::FRAGMENT_LOCAL).ok());

    if (len != -1) {ASSERT_NE(nullptr, ctx->get_function_state(FunctionContext::FRAGMENT_LOCAL));
    } else {ASSERT_EQ(nullptr, ctx->get_function_state(FunctionContext::FRAGMENT_LOCAL));
    }

    ColumnPtr result = EncryptionFunctions::sha2(ctx.get(), columns).value();
    if (expected == "NULL") {std::cerr << result->debug_string() << std::endl;
        EXPECT_TRUE(result->is_null(0));
    } else {auto v = ColumnHelper::cast_to<TYPE_VARCHAR>(result);
        EXPECT_EQ(expected, v->get_data()[0].to_string());
    }

    ASSERT_TRUE(EncryptionFunctions::sha2_close(ctx.get(),
                                                FunctionContext::FunctionContext::FunctionStateScope::FRAGMENT_LOCAL)
                        .ok());
}

残缺的改变能够参考 PR:https://github.com/StarRocks/starrocks/pull/1264/files。

如何为 StarRocks 增加聚合函数

聚合函数介绍

聚合函数用于解决多行数据,承受多行数据作为输出,通过计算后返回一行后果。StarRocks 常见的聚合函数有 count、sum、avg、min、max 等。

聚合函数的实现原理

在查问执行阶段,Pipeline 引擎的聚合算子通过 Aggregator 实现聚合计算,聚合算子的实现原理可参见文末《StarRocks 聚合算子源码解析》[9],本文次要关注聚合函数的实现原理。
Aggregator 在 prepare 阶段会依据函数名找到对应的 AggregateFunction 并保留下来,AggregateFunction 是最重要的形象,封装了聚合计算过程中须要的各个接口,每个聚合函数都须要继承 AggregateFunction 实现本人的逻辑。计算的两头后果保留在 AggDataPtr 中,AggDataPrt 是一个指针,指向形容两头后果的数据结构。每种聚合函数的两头后果都不雷同,比方求和函数,只须要保留 sum 即可,而平均值函数,除了保留 sum 之外,还须要记录 count。
在 AggregateFunction 提供的接口中,咱们须要重点关注以下几个:


// 逐行读取数据,不断更新 state 中保留的两头后果。void update(FunctionContext* ctx, const Column** columns, AggDataPtr __restrict state, size_t row_num)

// 通常用在多阶段聚合中,读取曾经算好的局部两头后果,合并计算,更新 state 中的数据。void merge(FunctionContext* ctx, const Column* column, AggDataPtr __restrict state, size_t row_num)

// 多阶段的聚合可能会通过多个节点执行,计算的两头后果须要跨网络传输,这个办法用来实现序列化的逻辑。void serialize_to_column(FunctionContext* ctx, ConstAggDataPtr __restrict state, Column* to) 

// 把两头后果转成最终对用户返回的后果。比方求和函数,间接返回两头后果保留的 sum 即可,而平均值函数,须要返回 sum/count。void finalize_to_column(FunctionContext* ctx, ConstAggDataPtr __restrict state, Column* to)

// 重置 state 的状态,比方在 window aggregate 中,咱们会用一个的 state 保留两头后果,每次遇到新的 group 时,须要通过 reset 重置,而后能力进行接下来的计算。void reset(FunctionContext* ctx, const Columns& args, AggDataPtr __restrict state)

除了上述内容之外,为了缩小函数调用的开销,AggregateFunction 还封装了批量操作的接口,具体的细节这里就不开展解说了,能够参考 be/src/exprs/agg/aggregate.h。

增加聚合函数示例

接下来咱们以 ANY_VALUE 为例,介绍增加聚合函数的流程,这个函数实现的性能比较简单,能够参考官网文档 [10] 阐明:

在 FE 创立函数签名
FE 通过 AggregateFunction[11] 来形容聚合函数,所有的聚合函数都会注册在 FunctionSet 中,初始化阶段在 FunctionSet 的 initAggregateBuiltins [12] 办法内减少对应的函数即可。具体代码如下:

   // ANY_VALUE
    addBuiltin(AggregateFunction.createBuiltin(ANY_VALUE,
            Lists.newArrayList(t), t, t, true, false, false));

在 BE 实现函数的计算逻辑
此处重点是如何形容两头后果,以及如何实现 AggregateFunction 的外围接口。
ANY_VALUE 的语义很简略,在每个 group 中抉择一行返回。两头后果通过 AnyValueAggregateData 形容,只须要记录以后是否曾经有后果以及对应的数据是什么即可,AnyValueAggregateData 为每种数据类型进行了特化,实现上简直统一。具体代码如下:

template <LogicalType LT>
struct AnyValueAggregateData {
    using T = AggDataValueType<LT>;

    T result;
    bool has_value = false;

    void reset() {result = T{};
        has_value = false;
    }
};

具体的计算逻辑非常简单,这部分通过 AnyValueElement 实现。具体代码如下:

template <LogicalType LT, typename State>
struct AnyValueElement {
    using RefType = AggDataRefType<LT>;

    void operator()(State& state, RefType right) const {if (UNLIKELY(!state.has_value)) {AggDataTypeTraits<LT>::assign_value(state.result, right);
            state.has_value = true;
        }
    }
};

最初利用 AnyValueElement 实现 AggregateFunction 所须要的接口即可,具体代码如下:

template <LogicalType LT, typename State, class OP, typename T = RunTimeC++Type<LT>, typename = guard::Guard>
class AnyValueAggregateFunction final
        : public AggregateFunctionBatchHelper<State, AnyValueAggregateFunction<LT, State, OP, T>> {
public:
    using InputColumnType = RunTimeColumnType<LT>;

    void reset(FunctionContext* ctx, const Columns& args, AggDataPtr state) const override {this->data(state).reset();}

    void update(FunctionContext* ctx, const Column** columns, AggDataPtr __restrict state,
                size_t row_num) const override {DCHECK(!columns[0]->is_nullable());
        const auto& column = down_cast<const InputColumnType&>(*columns[0]);
        OP()(this->data(state), AggDataTypeTraits<LT>::get_row_ref(column, row_num));
    }

    void update_batch_single_state(FunctionContext* ctx, size_t chunk_size, const Column** columns,
                                   AggDataPtr __restrict state) const override {update(ctx, columns, state, 0);
    }

    void merge(FunctionContext* ctx, const Column* column, AggDataPtr __restrict state, size_t row_num) const override {DCHECK(!column->is_nullable());
        const auto& input_column = down_cast<const InputColumnType&>(*column);
        OP()(this->data(state), AggDataTypeTraits<LT>::get_row_ref(input_column, row_num));
    }

    void serialize_to_column(FunctionContext* ctx, ConstAggDataPtr __restrict state, Column* to) const override {DCHECK(!to->is_nullable());
        AggDataTypeTraits<LT>::append_value(down_cast<InputColumnType*>(to), this->data(state).result);
    }

    void convert_to_serialize_format(FunctionContext* ctx, const Columns& src, size_t chunk_size,
                                     ColumnPtr* dst) const override {*dst = src[0];
    }

    void finalize_to_column(FunctionContext* ctx, ConstAggDataPtr __restrict state, Column* to) const override {DCHECK(!to->is_nullable());
        AggDataTypeTraits<LT>::append_value(down_cast<InputColumnType*>(to), this->data(state).result);
    }

    void get_values(FunctionContext* ctx, ConstAggDataPtr __restrict state, Column* dst, size_t start,
                    size_t end) const override {DCHECK_GT(end, start);
        InputColumnType* column = down_cast<InputColumnType*>(dst);
        for (size_t i = start; i < end; ++i) {AggDataTypeTraits<LT>::append_value(column, this->data(state).result);
        }
    }

    std::string get_name() const override { return "any_value";}
};

残缺的实现细节参见:be/src/exprs/agg/any_value.h

在 AggregateFactory 中注册

这一步是为了让 AggregateFactory 能够依据函数名找到对应的函数,函数的创立通过 MakeAnyValueAggregateFunction 实现,相干的改变能够在 aggregate_factory.hpp[13] 中 grep MakeAnyValueAggregateFunction 看到,比较简单,这里不再过多赘述,具体示例如下:

template <LogicalType LT>
AggregateFunctionPtr AggregateFactory::MakeAnyValueAggregateFunction() {
    return std::make_shared<
            AnyValueAggregateFunction<LT, AnyValueAggregateData<LT>, AnyValueElement<LT, AnyValueAggregateData<LT>>>>();}

增加单元测试

能够参见 test/exprs/agg/aggregate_test.cpp[14]增加单测,比方:

TEST_F(AggregateTest, test_any_value) {const AggregateFunction* func = get_aggregate_function("any_value", TYPE_SMALLINT, TYPE_SMALLINT, false);
    test_non_deterministic_agg_function<int16_t, int16_t>(ctx, func);

    func = get_aggregate_function("any_value", TYPE_INT, TYPE_INT, false);
    test_non_deterministic_agg_function<int32_t, int32_t>(ctx, func);

    func = get_aggregate_function("any_value", TYPE_BIGINT, TYPE_BIGINT, false);
    test_non_deterministic_agg_function<int64_t, int64_t>(ctx, func);

    func = get_aggregate_function("any_value", TYPE_LARGEINT, TYPE_LARGEINT, false);
    test_non_deterministic_agg_function<int128_t, int128_t>(ctx, func);

    func = get_aggregate_function("any_value", TYPE_FLOAT, TYPE_FLOAT, false);
    test_non_deterministic_agg_function<float, float>(ctx, func);

    func = get_aggregate_function("any_value", TYPE_DOUBLE, TYPE_DOUBLE, false);
    test_non_deterministic_agg_function<double, double>(ctx, func);

    func = get_aggregate_function("any_value", TYPE_VARCHAR, TYPE_VARCHAR, false);
    test_non_deterministic_agg_function<Slice, Slice>(ctx, func);

    func = get_aggregate_function("any_value", TYPE_DECIMALV2, TYPE_DECIMALV2, false);
    test_non_deterministic_agg_function<DecimalV2Value, DecimalV2Value>(ctx, func);

    func = get_aggregate_function("any_value", TYPE_DATETIME, TYPE_DATETIME, false);
    test_non_deterministic_agg_function<TimestampValue, TimestampValue>(ctx, func);

    func = get_aggregate_function("any_value", TYPE_DATE, TYPE_DATE, false);
    test_non_deterministic_agg_function<DateValue, DateValue>(ctx, func);
}

残缺的改变见 PR:https://github.com/StarRocks/starrocks/pull/2073

总结

本文介绍了 StarRocks 中标量函数和聚合函数的实现原理,并以 sha2 标量函数和 ANY_VALUE 聚合函数为例,阐明了如何增加标量函数和新增聚合函数。
标量函数定义在 be/src/exprs/ 目录下。若想查看某个函数的实现,能够在函数签名中找到对应的 be function,而后在该目录下应用 grep 进行查找。
此外,StarRocks 还实现了多种聚合函数,具体实现可在 be/src/exprs/agg 目录下查找。

最初,如果你在浏览完本文后对 StarRocks 函数的实现原理以及如何增加新的函数还有很多疑难,欢送报名加入 4/6(星期四)的 <StarRocks 源码实验室直播 >,以进一步学习。同时,咱们也欢送你支付函数工作,并通过实际学习如何为 StarRocks 增加新的函数!👇

相干链接:

[1]FunctionSet:https://github.com/StarRocks/starrocks/blob/main/fe/fe-core/src/main/java/com/starrocks/catalog/FunctionSet.java
[2] ExpressionAnalyzer.Visitor:https://github.com/StarRocks/starrocks/blob/main/fe/fe-core/src/main/java/com/starrocks/sql/analyzer/ExpressionAnalyzer.java#L303

[3]visitFunctionCall:https://github.com/StarRocks/starrocks/blob/main/fe/fe-core/src/main/java/com/starrocks/sql/analyzer/ExpressionAnalyzer.java#L893
[4]BuiltinFunctions::_fn_tables:https://github.com/StarRocks/starrocks/blob/main/be/src/exprs/builtin_functions.h#L75
[5]FunctionDescriptor:https://github.com/StarRocks/starrocks/blob/main/be/src/exprs/builtin_functions.h#L32
[6]sha2 函数:https://docs.starrocks.io/zh-cn/latest/sql-reference/sql-func…
[7]EncryptionFunctions:
https://github.com/StarRocks/starrocks/blob/main/be/src/exprs/encryption_functions.h
[8]EntryptionFunctionTest:
https://github.com/StarRocks/starrocks/blob/main/be/test/exprs/encryption_functions_test.cpp
[9]《StarRocks 聚合算子源码解析》:https://zhuanlan.zhihu.com/p/592058276

[10]ANY_VALUE 性能:https://docs.starrocks.io/zh-cn/latest/sql-reference/sql-func…
[11]AggregateFunction:https://github.com/StarRocks/starrocks/blob/main/fe/fe-core/src/main/java/com/starrocks/catalog/AggregateFunction.java#L61
[12]initAggregateBuiltins:https://github.com/StarRocks/starrocks/blob/main/fe/fe-core/src/main/java/com/starrocks/catalog/FunctionSet.java#L742
[13]aggregate_factory.cpp:https://github.com/StarRocks/starrocks/blob/main/be/src/exprs/agg/factory/aggregate_factory.hpp
[14]aggregate_test:https://github.com/StarRocks/starrocks/blob/main/be/test/exprs/agg/aggregate_test.cpp#L1667

正文完
 0