StarRocks percentile_hash 函数详解:将 DOUBLE 值构造为 PERCENTILE 近似分位数
【免费下载链接】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_hash是 StarRocks 中用于把 DOUBLE 类型的原始数值构造成PERCENTILE聚合类型的核心构造函数,是 PERCENTILE 类型在写入、聚合与物化视图等场景中的入口。本文以官方文档 percentile_hash.md 为主体,结合 BE 端源码与配套函数文档,完整讲解其语法、参数、返回类型、实战用法与底层 T-Digest 实现原理,帮助你在聚合表、Stream Load 和物化视图中正确使用 PERCENTILE 做近似分位数统计。
函数定位:PERCENTILE 类型的唯一入口
在 StarRocks 中,PERCENTILE不是普通标量类型,而是一种用于近似分位数(quantile/percentile)统计的聚合载体类型。原始 DOUBLE 数值不能直接存入 PERCENTILE 列,必须先经过percentile_hash将其"哈希"进一个分位数数据结构中。
提示:这里的 "hash" 并不是密码学意义上的哈希摘要,而是把单个 DOUBLE 值装载进基于 T-Digest 的
PercentileValue结构(见 percentile_value.h),后续所有分位数计算都基于该结构进行。因此文档描述将其定义为 "Constructs DOUBLE values as PERCENTILE values"(把 DOUBLE 值构造成 PERCENTILE 值)。
语法与参数说明
PERCENTILE_HASH(x);参数
| 参数 | 说明 |
|---|---|
x | 需要构造成 PERCENTILE 的输入值,仅支持 DOUBLE 类型 |
返回值
返回一个PERCENTILE类型的值,其中承载了输入值及其权重信息,供后续percentile_union、percentile_approx_raw等函数使用。
空值处理
从 BE 端实现(percentile_functions.cpp)可以看到,函数逐行遍历输入列:当某一行值为 NULL 时,会写入一个空的PercentileValue(等价于不包含任何数据),而不会报错或跳过整行:
ColumnViewer<TYPE_DOUBLE> viewer(columns[0]); auto percentile_column = PercentileColumn::create(); size_t size = columns[0]->size(); for (int row = 0; row < size; ++row) { PercentileValue value; if (!viewer.is_null(row)) { value.add(viewer.value(row)); } percentile_column->append(&value); }同时,实现中还做了常量列优化:当整列输入都是常量时,函数返回ConstColumn包装的结果,避免不必要的逐行计算开销。
官方示例与结果解读
官方文档给出的最小示例是直接在percentile_hash的结果上调用percentile_approx_raw求 0.99 分位数:
mysql> select percentile_approx_raw(percentile_hash(234.234), 0.99); +-------------------------------------------------------+ | percentile_approx_raw(percentile_hash(234.234), 0.99) | +-------------------------------------------------------+ | 234.23399353027344 | +-------------------------------------------------------+ 1 row in set (0.00 sec)注意输出值234.23399353027344与输入234.234存在极微小的偏差,这正是 T-Digest 近似算法的特征:PercentileValue内部以浮点型(float)存储质心均值(见 tdigest.h 中using Value = float;),分位数查询quantile(q)也是基于压缩后的摘要结构估算而非精确排序。因此percentile_hash适用于海量数据的近似分位数统计,对单值精度要求苛刻的场景(如财务精确计算)应改用精确类型。
实战场景一:聚合表中的 PERCENTILE 列
PERCENTILE 类型最典型的用法是配合聚合模型(Aggregate Key)表,以PERCENTILE_UNION作为聚合方式。参考配套文档 percentile_approx_raw.md,建表与写入流程如下:
CREATE TABLE `aggregate_tbl` ( `site_id` largeint(40) NOT NULL COMMENT "id of site", `date` date NOT NULL COMMENT "time of event", `city_code` varchar(20) NULL COMMENT "city_code of user", `pv` bigint(20) SUM NULL DEFAULT "0" COMMENT "total page views", `percent` PERCENTILE PERCENTILE_UNION COMMENT "others" ) ENGINE=OLAP AGGREGATE KEY(`site_id`, `date`, `city_code`) COMMENT "OLAP" DISTRIBUTED BY HASH(`site_id`) PROPERTIES ("replication_num" = "1");写入时,每条记录的原始数值都必须用percentile_hash包装:
insert into aggregate_tbl values (5, '2020-02-23', 'city_code', 555, percentile_hash(1)); insert into aggregate_tbl values (5, '2020-02-23', 'city_code', 555, percentile_hash(2)); insert into aggregate_tbl values (5, '2020-02-23', 'city_code', 555, percentile_hash(3)); insert into aggregate_tbl values (5, '2020-02-23', 'city_code', 555, percentile_hash(4));随后即可查询任意分位数:
mysql> select percentile_approx_raw(percent, 0.5) from aggregate_tbl; +-------------------------------------+ | percentile_approx_raw(percent, 0.5) | +-------------------------------------+ | 2.5 | +-------------------------------------+ 1 row in set (0.03 sec)多条记录在导入落盘时按PERCENTILE_UNION聚合,底层通过PercentileValue::merge将多个 T-Digest 合并为一份摘要(见 percentile_value.h),因此无论聚合多少数据,存储的始终是压缩后的分位数摘要而非全量明细,这就是 PERCENTILE 类型能支撑超大规模分位数统计的原因。
实战场景二:Stream Load 导入时转换
在通过 Stream Load 导入包含 PERCENTILE 列的数据时,同样需要借助percentile_hash做列转换。参考 percentile_union.md 的示例,建一张包含 PERCENTILE 列的聚合表:
CREATE TABLE sales_records( record_id int, seller_id int, store_id int, sale_amt_per percentile percentile_union ) ENGINE=OLAP AGGREGATE KEY(`record_id`, `seller_id`, `store_id`) COMMENT "OLAP" DISTRIBUTED BY HASH(`record_id`) PROPERTIES ( "replication_num" = "3", "storage_format" = "DEFAULT" );导入时,先用临时列tmp接收原始 DOUBLE 值,再通过sale_amt_per = percentile_hash(tmp)完成类型转换:
curl --location-trusted -u root -H "columns: record_id, seller_id, store_id,tmp, sale_amt_per =percentile_hash(tmp)" -H "column_separator:," -T a http://<ip:port>/api/test/sales_records/_stream_load查询时配合percentile_union与percentile_approx_raw还原分位数结果:
select percentile_approx_raw(percentile_union(sale_amt_per), 0.99) from sales_records;实战场景三:物化视图中的预聚合
percentile_hash也可以出现在物化视图的建视图语句中,实现"明细表 + 分位数物化视图"的组合。仍参考 percentile_union.md:
CREATE TABLE sales_records( record_id int, seller_id int, store_id int, sale_date date, sale_amt bigint ) distributed BY hash(record_id) PROPERTIES ("replication_num" = "3");create materialized view mv as select store_id, percentile_union(percentile_hash(sale_amt)) from sales_records group by store_id;这里percentile_hash先把每个sale_amt明细值转换为 PERCENTILE,percentile_union再按store_id分组聚合成压缩摘要,从而让分位数查询命中物化视图加速。
底层原理:从 T-Digest 到 PERCENTILE 列
数据类型链
PERCENTILE 相关函数在 BE 端的声明位于 percentile_functions.h,三者构成了完整的数据流转闭环:
| 函数 | 输入 | 输出 | 职责 |
|---|---|---|---|
percentile_hash | DOUBLE | PERCENTILE | 构造:单值 → 摘要 |
percentile_union | PERCENTILE | PERCENTILE | 聚合:多摘要 → 一份摘要 |
percentile_approx_raw | PERCENTILE, DOUBLE | DOUBLE | 查询:摘要 → 指定分位数估值 |
此外还有 percentile_empty,用于在 Stream Load / INSERT INTO 导入时填充 NULL 占位。
PercentileValue 与序列化
PercentileValue是 PERCENTILE 值的内存表示(percentile_value.h):
- 内部持有
TDigest _tdigest与 1 字节的_type标记(当前仅支持TDIGEST一种实现); - 序列化格式为
1 字节类型标记 + T-Digest 序列化数据,落盘与网络传输都基于该格式; add(value)等价于_tdigest.add(value),quantile(q)委托给_tdigest.quantile(q)。
T-Digest 近似算法
percentile_hash之所以能支撑海量数据的实时分位数统计,根源在于 T-Digest 数据结构。StarRocks 的实现(tdigest.h)基于 Ted Dunning 与 Otmar Ertl 的论文《Computing extremely accurate quantiles using t-digest》及 derrickburns/tdigest 开源实现,其核心思路是:
- 用一组带权重的质心(Centroid)描述数据分布,靠近尾部分布(低分位/高分位)的质心更密集,从而在压缩存储的同时保留对极端分位数的高精度;
- 通过
kHighWater = 40000等阈值控制质心数量与压缩策略,平衡内存占用与精度; merge操作天然支持分片/副本间摘要合并,与percentile_union的聚合语义完全吻合。
因此percentile_hash得到的 PERCENTILE 值并非原始数据快照,而是一份可合并、可序列化、可查询的近似分布摘要——这正是示例中234.234被估算为234.23399353027344的原因,也是官方文档将其归类为近似分位数函数族的定位所在。
注意事项
- 仅支持 DOUBLE 输入:
percentile_hash的参数类型限定为 DOUBLE,其他数值类型需要先显式转换为 DOUBLE 再传入。 - 近似而非精确:结果基于 T-Digest 估算,存在微小误差;需要精确分位数(如精确中位数)时应使用
percentile_cont/percentile_disc等精确函数。 - 与聚合类型绑定:PERCENTILE 列只能定义在聚合模型(Aggregate Key)表中,且聚合方式必须为
PERCENTILE_UNION。 - 分位数取值区间:配合
percentile_approx_raw查询时,分位数参数y的取值范围为[0.0, 1.0],例如 0.99 表示 99 分位。 - NULL 输入:对 NULL 值
percentile_hash会写入空摘要,查询时该行不参与分位数统计。
参考文档与源码
- 函数官方文档:percentile_hash.md
- 配套查询函数:percentile_approx_raw.md
- 配套聚合函数:percentile_union.md
- 空值占位函数:percentile_empty.md
- BE 端函数实现:percentile_functions.cpp
- PERCENTILE 值类型:percentile_value.h
- T-Digest 实现:tdigest.h
- 聚合函数实现:percentile_union.h
【免费下载链接】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),仅供参考