StarRocks percentile_disc 分位数函数详解:从 SQL 语法、实战示例到 C++ 聚合内核实现
2026/9/17 19:08:58 网站建设 项目流程

StarRocks percentile_disc 分位数函数详解:从 SQL 语法、实战示例到 C++ 聚合内核实现

【免费下载链接】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

PERCENTILE_DISC是 StarRocks 提供的精确分位数聚合函数,它基于输入列的离散分布返回真实的百分位值;当精确的分位点落在两个相邻值之间时,它返回两者中较大的那个值。本文完整继承官方文档对语法、参数、返回值与示例的说明,并结合当前仓库中前端函数注册(Java)与后端聚合执行(C++)的源码实现,解释"取较大值"这一行为在引擎内部的落地方式,帮助读者既能直接上手使用该函数,也能理解其类型检查、常量处理与排序求值等底层机制。该函数自 v2.5 起受支持。

函数定义与语义

官方文档(percentile_disc.md)对该函数的核心定义是:

Returns a percentile value based on a discrete distribution of the input columnexpr. If the exact percentile value cannot be found, this function returns the larger value between the two closest values.

即:

  • 输入列expr的取值集合被视为一个离散分布;
  • 函数返回该分布中对应percentile位置上的分位值;
  • 若目标分位点无法精确命中某个数据值(例如中位点落在两个值中间),则返回两个最接近候选值中较大的那个(即统计学中的"上分位数",upper percentile)。

这一"取较大值"的语义在后端实现中被逐字实现,下文源码部分会给出对应代码行。

语法

PERCENTILE_DISC (expr, percentile)

参数说明

  • expr:需要计算分位数值的列。该列可以是任意可排序的数据类型(任意 sortable 类型均可)。
  • percentile:想要查找的分位点。必须是一个0 到 1 之间的常数量化数。例如,查找中位数时设置为0.5,查找第 70 百分位时指定为0.7

关于percentile必须是常量这一点,在 FE 分析器源码中有明确保障:FunctionAnalyzer.java 中对PERCENTILE_DISC(及其低基数变体LC_PERCENTILE_DISC)做了专门处理——将第二个参数的类型强制归一为DOUBLE,并使用IS_IDENTICAL比较模式精确匹配函数签名;若没有匹配到任何注册签名,则直接抛出SemanticException。这与文档中"It must be a constant floating-point number between 0 and 1"的约束相互印证。

返回值

返回值的数据类型与expr完全相同。FE 侧的分析器源码同样支持这一结论:FunctionSet.java 在为每个可排序类型(SORTABLE_TYPES)注册PERCENTILE_DISC时,入参签名为(type, DOUBLE)、返回类型直接就是type本身:

for (Type type : SORTABLE_TYPES) { addBuiltin(AggregateFunction.createBuiltin(FunctionSet.PERCENTILE_DISC, Lists.newArrayList(type, FloatType.DOUBLE), type, VarbinaryType.VARBINARY, false, false, false)); }

值得注意的是,分析器对 DECIMAL 类型还有一段额外的精度/标度修正逻辑(FunctionAnalyzer.java):当第一个参数为 DecimalV3 时,会基于原始参数类型重新构造函数元数据,确保返回值携带与输入列一致的精度和标度,而不会因签名匹配过程发生隐式提升。

使用注意事项

  • NULL 值不参与计算("NULL values are ignored in the calculation")。下文的实战示例中,chemistry科目存在一条score为 NULL 的记录,但中位数结果仅由非 NULL 的 80 与 100 两个值决定,正好可以验证该规则。

实战示例

以下示例完整来自官方文档。

建表并写入数据

CREATE TABLE exam ( subject STRING, score INT ) DISTRIBUTED BY HASH(`subject`); INSERT INTO exam VALUES ('chemistry',80), ('chemistry',100), ('chemistry',null), ('math',60), ('math',70), ('math',85), ('physics',75), ('physics',80), ('physics',85), ('physics',99);

查看数据:

select * from exam order by subject; +-----------+-------+ | subject | score | +-----------+-------+ | chemistry | 80 | | chemistry | 100 | | chemistry | NULL | | math | 60 | | math | 70 | | math | 85 | | physics | 75 | | physics | 80 | | physics | 85 | | physics | 99 | +-----------+-------+

计算每个科目的中位数

select subject, percentile_disc(score, 0.5) from exam group by subject;

输出:

+-----------+-----------------------------+ | subject | percentile_disc(score, 0.5) | +-----------+-----------------------------+ | chemistry | 100 | | math | 70 | | physics | 85 | +-----------+-----------------------------+

用源码公式逐步验证示例结果

后端在最终阶段对每个分组收集到的值排序后,按下式选取结果值(详见下文 percentile_cont.h 中的finalize_to_column):

index = ceil((n - 1) * rate) // n 为去 NULL 后的元素个数,rate 为 percentile result = sorted[index]

逐组验证文档示例:

  • chemistry:非 NULL 值排序后为[80, 100],n=2,index = ceil(1 × 0.5) = 1→ 取sorted[1] = 100。两个值之间无法精确命中 50 分位,按"取较大值"规则返回 100;
  • math[60, 70, 85],n=3,index = ceil(2 × 0.5) = 1→ 取sorted[1] = 70
  • physics[75, 80, 85, 99],n=4,index = ceil(3 × 0.5) = ceil(1.5) = 2→ 取sorted[2] = 85

三个结果与文档给出的输出完全一致,同时也直观展示了"精确分位点不可达时返回较大值"的离散语义(chemistry 组是典型例子)。

源码级实现原理

FE:函数注册与签名匹配

  1. 注册范围覆盖所有可排序类型。FunctionSet.java 中,PERCENTILE_DISC遍历SORTABLE_TYPES逐一注册,第二个参数固定为DOUBLE;同一循环中还注册了低基数列优化用的LC_PERCENTILE_DISCpercentile_disc_lc)。这与文档中"任意可排序类型"的表述一致。
  2. 第二个参数强制归一为 DOUBLE。如前所述,FunctionAnalyzer.java 在语义分析阶段将argumentTypes[1]改写为FloatType.DOUBLE后再查签名,因此写入 SQL 的0.50.7等字面量无需手动转 DOUBLE 即可匹配。

BE:聚合状态与最终求值

percentile_disc的具体实现位于 percentile_cont.h(与percentile_cont共用文件与状态结构),核心类为PercentileDiscAggregateFunction

聚合状态PercentileState定义(percentile_cont.h):

struct PercentileState { void update(CppType item) { items.emplace_back(item); } void update_batch(const ImmBuffer<CppType> vec) { /* memcpy 批量追加 */ } ItemType items; // 该分组收集到的所有非 NULL 值 GridType grid; // 部分合并/归并路径使用的分格数据 double rate = 0.0; // 第二个常量参数 percentile };

最终求值finalize_to_column)的关键逻辑:

// percentil_cont.h,PercentileDiscAggregateFunction::finalize_to_column typename PercentileStateTypes<LT>::ItemType new_vector = std::move(this->data(state).items); // ...合并 grid 中的分格数据后... pdqsort(new_vector.begin(), new_vector.end()); // ① 对分组内全部值排序 const double& rate = this->data(state).rate; if (new_vector.empty()) { column->append_default(); // ② 全组 NULL → 返回 NULL return; } if (new_vector.size() == 1 || rate == 1) { column->append(new_vector.back()); // ③ 单元素或 percentile=1 → 最大值 return; } // choose the uppper one // ④ 源码注释即文档语义 int index = ceil((new_vector.size() - 1) * rate); // ⑤ 上分位点公式

其中:

  • 步骤②对应文档中"NULL 被忽略"的极端情形——分组内全部为 NULL 时返回 NULL 默认值;
  • 步骤④⑤即"larger value between the two closest values"的实现:ceil((n-1) × rate)保证在分位点落在两个元素之间时索引向上取整,从而选中较大的那个候选值;
  • 结果类型按输入类型分支处理:DATE、DATETIME 与数值/字符串/DECIMAL 分别走对应赋值路径;若遇到未注册的不可排序类型则抛出runtime_error("Invalid PrimitiveTypes for percentile_disc function"),这也从执行侧解释了为什么文档强调expr必须是可排序类型(percentile_cont.h)。

常量参数的传输路径:percentile 为何必须写常量

文档要求percentile是常量,BE 侧执行路径也体现了对这一约定的专门处理。在 aggregator.cpp 中,聚合输入列的求值逻辑对第二参数做了区别对待:

// if function has at least two argument, unpack const column selectively // for function like percentile_disc, the second args is const, do not unpack it if (agg_expr_ctxs[j]->root()->is_constant()) { _agg_input_columns[i][j] = std::move(col); // 常量列原样保留,不展开 } else { _agg_input_columns[i][j] = ColumnHelper::unpack_and_duplicate_const_column(chunk->num_rows(), std::move(col)); }

注释明确点名percentile_disc:第二个参数作为常量列直接传入聚合状态(state.rate),而无需像第一参数那样展开为与 chunk 行数对齐的普通列。这说明 FE 保证第二个参数是常量、BE 依赖该常量一次性写入聚合状态,二者共同构成"percentile 必须是 0~1 常量"这一使用约束的实现基础。

函数在 BE 侧的入口注册位于 aggregate_resolver_others.cpp,按输入类型将percentile_disc解析为对应的PercentileDiscAggregateFunction<LT>模板实例。

小结

  • PERCENTILE_DISC(expr, percentile)对可排序列做精确(非近似)离散分位计算,percentile为 0~1 的常量;
  • 精确分位点不可达时返回相邻两值中较大者,源码以ceil((n-1) × rate)的取整方向直接落实该语义;
  • 返回值类型与输入列完全一致,NULL 不参与计算;
  • 该实现是精确算法,需要对每个分组的全部值排序(pdqsort),在分组基数极大、且只需估算分位的场景中,应结合场景权衡是否改用近似分位类函数;
  • 主要源码位置:FE 注册见 FunctionSet.java,参数分析见 FunctionAnalyzer.java,BE 求值见 percentile_cont.h,常量参数处理见 aggregator.cpp。

【免费下载链接】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),仅供参考

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

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

立即咨询