3. 聚合模型(Aggregate Key Model)
聚合模型,说白了就是 StarRocks 里专门为「预聚合」场景设计的利器。我最早接触它的时候,第一反应是——这不就是物化视图的变种吗?后来用深了才发现,它比物化视图要底层得多,也高效得多。
它的核心思想很简单:在数据导入时就完成部分聚合计算,把多行明细数据合并成一行聚合结果。你想想看,如果一张表每天产生几亿条订单明细,但你的报表只需要看每天的总销售额,那存那么多明细干嘛?直接在入库时算好总和,查询时秒出结果,多痛快。
核心原理: 聚合模型通过定义一组 AGGREGATE KEY(聚合键),在数据导入时对相同 Key 的多行记录,按照指定的聚合函数进行合并。最终存储的是聚合后的结果,而非原始明细。
3.1 模型原理
聚合模型的工作机制,我习惯把它拆成三步来理解:
- 定义聚合键: 建表时指定哪些列是
AGGREGATE KEY,这些列就是分组依据。剩下的列都是指标列,需要声明聚合函数类型。 - 数据导入时合并: 当数据写入时,StarRocks 会按照
AGGREGATE KEY对数据进行排序和分组。如果多条记录的 Key 完全相同,就会触发聚合函数计算,合并成一条。 - 查询时再聚合: 虽然存储层已经做了预聚合,但查询时如果涉及更高维度的聚合(比如按天聚合的数据再按月聚合),StarRocks 仍然会再做一次聚合。不过因为数据量已经大幅减少,性能依然很好。
我举个例子你就明白了。假设有一张订单表,按「日期 + 商品ID」做聚合键,指标是「销售额」用 SUM 聚合。导入两条记录:(2024-01-01, A001, 100) 和 (2024-01-01, A001, 200)。最终存储的只有一条:(2024-01-01, A001, 300)。数据量直接减半,查询时连 GROUP BY 都省了。
我的经验: 聚合模型最适合「写少读多」的场景。写入时多做一点计算,查询时就能快十倍甚至百倍。我在做电商 GMV 报表时,用聚合模型把原始 10 亿条订单数据压缩到 2000 万条,查询从 30 秒降到了 0.5 秒以内。
3.2 聚合函数类型
StarRocks 聚合模型支持多种聚合函数。我按使用频率排个序,你心里有个数:
| 聚合函数 | 说明 | 典型场景 |
|---|---|---|
SUM |
求和,数值累加 | 销售额、流量、时长 |
MAX |
取最大值 | 最高温度、最大并发 |
MIN |
取最小值 | 最低价格、最早时间 |
REPLACE |
新值覆盖旧值 | 用户最新状态、最后登录 IP |
REPLACE_IF_NOT_NULL |
非空时覆盖 | 增量更新字段,保留已有值 |
HLL_UNION |
HyperLogLog 近似去重 | UV 统计、去重计数 |
BITMAP_UNION |
Bitmap 精确去重 | 精确 UV、留存分析 |
PERCENTILE_UNION |
百分位近似计算 | P50/P95 延迟 |
这里我要特别提醒一下 REPLACE 和 REPLACE_IF_NOT_NULL 的区别。我曾经在做一个用户画像表时,用了 REPLACE,结果导入了一条只有部分字段更新的数据,把之前已有的字段值全覆盖成了 NULL。后来改用 REPLACE_IF_NOT_NULL 才解决问题。嗯,这个坑我踩过,你记住就好。
3.3 适用场景
聚合模型不是万能的。我总结了三个最适合的场景:
- 固定维度的汇总报表: 比如按天、按门店、按品类的销售汇总。维度组合是固定的,不需要随意下钻到明细。
- 去重计数场景: 用
BITMAP_UNION或HLL_UNION做 UV 统计。我做过一个日活报表,用 Bitmap 聚合后,查询 30 天去重 UV 只需要 200ms。 - 状态快照场景: 比如用户的最新资料、订单的最新状态。用
REPLACE聚合,每次导入新数据就覆盖旧数据,查询时直接拿最新值。
那什么时候不适合用?如果你需要频繁查询原始明细数据,或者聚合键维度组合太多导致几乎每条记录都唯一,那聚合模型就没什么优势了。数据压缩不了,反而多了一层聚合开销。这时候我建议你用明细模型。
注意: 聚合模型一旦建好,AGGREGATE KEY 和聚合函数类型就不能修改了。所以建表前一定要想清楚业务需要的聚合维度。我见过有人建完表才发现少了一个维度列,最后只能重建表重新导数据,非常痛苦。
3.4 建表语法
建表语法其实不复杂。我直接给你一个完整的例子:
CREATE TABLE IF NOT EXISTS agg_order_daily (
event_date DATE NOT NULL COMMENT '日期',
shop_id BIGINT NOT NULL COMMENT '门店ID',
product_id BIGINT NOT NULL COMMENT '商品ID',
order_count BIGINT SUM DEFAULT '0' COMMENT '订单数',
total_amount DECIMAL(18,2) SUM DEFAULT '0.00' COMMENT '总金额',
max_amount DECIMAL(18,2) MAX DEFAULT '0.00' COMMENT '最大单笔金额',
first_order_id VARCHAR(64) REPLACE DEFAULT '' COMMENT '首单ID',
uv_hll HLL HLL_UNION COMMENT 'UV近似去重'
)
AGGREGATE KEY (event_date, shop_id, product_id)
DISTRIBUTED BY HASH(shop_id) BUCKETS 16
PROPERTIES (
"replication_num" = "3",
"storage_medium" = "SSD"
);
几个关键点我拎出来说一下:
AGGREGATE KEY必须包含所有维度列,且定义顺序决定了数据的排序方式。我习惯把基数小的列放前面,比如日期在前,门店在后。- 指标列必须声明聚合函数类型,写在数据类型后面。
DEFAULT值建议都写上,避免导入时出现 NULL 导致聚合异常。HLL和BITMAP类型需要特殊处理,建表时声明为HLL_UNION或BITMAP_UNION。
3.5 数据写入与查询示例
写入数据时,你不需要做任何特殊处理。StarRocks 会自动完成聚合。我演示一下:
-- 写入三条明细数据
INSERT INTO agg_order_daily VALUES
('2024-01-01', 1001, 2001, 1, 50.00, 50.00, 'ORD001', hll_hash('user_001')),
('2024-01-01', 1001, 2001, 2, 120.00, 80.00, 'ORD002', hll_hash('user_002')),
('2024-01-01', 1001, 2001, 1, 30.00, 30.00, 'ORD003', hll_hash('user_001'));
-- 实际存储的只有一条聚合结果
-- (2024-01-01, 1001, 2001, 4, 200.00, 80.00, 'ORD003', HLL合并后的值)
查询时,你可以直接查,也可以再做更高维度的聚合:
-- 直接查询,结果已经是按天+门店+商品聚合好的
SELECT * FROM agg_order_daily
WHERE event_date = '2024-01-01';
-- 按门店汇总,查询时再做一次SUM聚合
SELECT shop_id,
SUM(order_count) AS total_orders,
SUM(total_amount) AS total_amount
FROM agg_order_daily
WHERE event_date = '2024-01-01'
GROUP BY shop_id;
-- 使用HLL计算UV
SELECT shop_id,
hll_union_agg(uv_hll) AS uv
FROM agg_order_daily
WHERE event_date = '2024-01-01'
GROUP BY shop_id;
你看,查询语法和普通表几乎一样。唯一要注意的是,HLL 和 BITMAP 类型需要用对应的聚合函数来查询,比如 hll_union_agg、bitmap_union_count。
我的建议: 如果你用聚合模型做去重统计,优先选 BITMAP_UNION。虽然它比 HLL_UNION 占更多存储,但结果是精确的。我在做用户留存分析时,就因为用了 HLL 导致数据对不上,排查了半天才发现是近似误差。后来全改成 Bitmap 了。
最后,我画了一张聚合模型的工作流程图,帮你把整个流程串起来:
这张图把聚合模型的完整链路画出来了。从原始明细数据开始,经过导入时的自动聚合,到最终存储和查询。你重点关注中间那个「按 AGGREGATE KEY 分组聚合」的步骤,这是整个模型的核心。
好了,聚合模型的内容就这些。记住一句话:用空间换时间,用预聚合换查询速度。选对场景,它就是你的性能利器;选错场景,它就是你的维护负担。