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 column
expr. 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:函数注册与签名匹配
- 注册范围覆盖所有可排序类型。FunctionSet.java 中,
PERCENTILE_DISC遍历SORTABLE_TYPES逐一注册,第二个参数固定为DOUBLE;同一循环中还注册了低基数列优化用的LC_PERCENTILE_DISC(percentile_disc_lc)。这与文档中"任意可排序类型"的表述一致。 - 第二个参数强制归一为 DOUBLE。如前所述,FunctionAnalyzer.java 在语义分析阶段将
argumentTypes[1]改写为FloatType.DOUBLE后再查签名,因此写入 SQL 的0.5、0.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),仅供参考