Hologres针对大数据量(亿级)、高QPS的UV计算场景,提供近实时预聚合UV计算方案,支持您通过RoaringBitmap,结合周期性调度的方式对数据进行预聚合,实现任意长周期的UV灵活计算。
方案介绍
对于大数据量且对QPS、延迟有一定需求的业务,可以通过Hologres RoaringBitmap,结合周期性调度的方式对数据进行预聚合,实现任意长周期的UV灵活计算。该方案的优缺点及适用场景如下:
优缺点
优点:计算性能好,可以实现高QPS低延迟的UV计算(基数精确去重计算),且可以支持任意周期范围,满足业务多种灵活查询需求。
缺点:需要做一次预计算,并周期性更新聚合表数据,会增加维护任务。
适用场景:大数据量(亿级)的任意长周期高QPS的UV计算。
根据业务的实际场景以及数据类型等,RoaringBitmap方案有以下三种实现方法,您可根据业务情况选择合适的方法。
方法1:INT字段类型的长周期UV计算:适用于对INT类型字段进行UV精确去重计算场景,适用于对单标签过滤筛选后的基数(即不同值的数量)进行计算。
方法2:TEXT字段类型的长周期UV计算:适用于对TEXT类型字段进行UV精确去重计算场景(需要结合Mapping表使用),适用于对单标签过滤筛选后的基数进行计算。
方法3(高级方法):分Bucket的长周期UV计算:多用于对画像类场景中,多个标签或属性字段进行交并差计算,以确定人群基数,通过Bucket分桶并行处理查询,实现人群的高度压缩,减少对数据的IO操作,提升计算效率。
本文中的UV仅是RoaringBitmap(RB)场景的一种,RB方案也适用于其他基数精确去重的场景,例如直播电商大屏场景,计算商品数、品牌数等。
更多RoaringBitmap函数信息请参见RoaringBitmap函数。
方法1:INT字段类型的RB长周期UV计算
适用场景
适用于大数据量(亿级)的任意长周期高QPS的UV计算,且计算UV的字段类型(UID,即用户ID)是INT类型的场景。
方案流程
该方案的主要流程如下:
步骤一:创建一张用户明细表,用于存放业务所有维度的明细数据。
步骤二:根据业务逻辑,将明细表按照基础维度进行GROUP BY聚合,再将聚合出的UID结果转换为RoaringBitmap形式,存放在聚合结果表中。
步骤三:按照查询维度查询聚合结果表,对关键的RoaringBitmap字段进行OR运算,去重并统计基数,即可得出对应用户数UV,统计聚合结果表中的记录数即可得出PV,达到亚秒级查询。
步骤一:准备基础数据
创建RoaringBitmap Extension。
使用RoaringBitmap前需要创建RoaringBitmap Extension,Extension是DB级别的函数,一个DB只需执行一次即可。语法如下:
说明RoaringBitmap Extension只能加载在public Schema下。
CREATE EXTENSION IF NOT EXISTS roaringbitmap;
准备用户明细表。
准备一张用户明细表,记录用户完整的明细数据。通常来说,明细表会存储所有明细数据,数据量大,建议使用分区表,按天分区,按天更新,使用更方便。
BEGIN; CREATE TABLE IF NOT EXISTS ods_app_detail ( uid int, country text, prov text, city text, channel text, operator text, brand text, ip text, click_time text, year text, month text, day text, ymd text NOT NULL ) PARTITION BY LIST (ymd) ;--通常来说明细表数据量大,按天分区更方便。 CALL set_table_property('ods_app_detail', 'orientation', 'column'); CALL set_table_property('ods_app_detail', 'bitmap_columns', 'country,prov,city,channel,operator,brand,ip,click_time, year, month, day, ymd'); --distribution_key根据需求设置,根据该表的实时查询需求,从什么维度做分片能够取得较好效果即可 CALL set_table_property('ods_app_detail', 'distribution_key', 'uid'); --用于做where过滤条件,包含完整年月日时间字段推荐设为clustering_key和event_time_column CALL set_table_property('ods_app_detail', 'clustering_key', 'ymd'); CALL set_table_property('ods_app_detail', 'event_time_column', 'ymd'); COMMIT;
准备RB聚合结果表。
创建RB聚合结果表,用于存放RoaringBitmap聚合后的结果数据。
聚合后的数据量通常不多(聚合后单天数据量仅几百万条),建议聚合结果表使用非分区表,数据回刷时更方便。或者使用按月/季度分区(若按天分区,数据量小,分区多可能会产生较多小文件,影响内存水位)。
以country、prov、city为维度构建基础维表,并设置为Distribution Key,查询时可以根据维度查询。如果存在多个GROUP BY字段(超过3个),建议将使用最频繁的字段设置为Distribution Key。
将查询维度字段和日期分区键作为主键,防止重复插入数据。
将日期过滤字段作为Clustering Key和Event Time Column,加快过滤查询。
代码如下:
BEGIN; CREATE TABLE dws_app_rb( rb_uid roaringbitmap, -- UV计算 country text, prov text, city text, ymd text NOT NULL, --日期字段 pv integer, -- PV计算 PRIMARY key(country, prov, city, ymd)--查询维度和时间作为主键,防止重复插入数据 ); CALL set_table_property('dws_app_rb', 'orientation', 'column'); --clustering_key和event_time_column设为日期字段,便于过滤 CALL set_table_property('dws_app_rb', 'clustering_key', 'ymd'); CALL set_table_property('dws_app_rb', 'event_time_column', 'ymd'); --distribution_key设为group by字段 CALL set_table_property('dws_app_rb', 'distribution_key', 'country,prov,city'); END;
步骤二:构建RB聚合结果表
准备好明细表之后,即可进行RB构建,并将结果数据写入RB聚合表。示例如下:
--示例:查询半年的数据,构建rb并写入聚合结果表
INSERT INTO dws_app_rb
SELECT
RB_BUILD_AGG(uid),
country,
prov,
city,
ymd,
COUNT(1)
FROM ods_app_detail
WHERE ymd >= '20231201' AND ymd <='20240502'
GROUP BY country,prov,city,ymd;
当明细表更新后,根据明细表的更新粒度,聚合表的更新可以分为增量更新或者全量更新。二者的区别如下:
聚合表更新方式 | 说明 | 代码示例 |
增量更新聚合表 | 明细表的数据有规律地更新或新增,例如只更新昨天的分区,即可通过INSERT的方式将增量数据写入聚合表。 | 只需写入新增数据:
|
全量更新聚合表 | 明细表的数据无规律地更新,无法快速计算出增量数据,导致无法用新增数据更新聚合表,因此使用全量回刷方式写入聚合表。 | 使用INSERT OVERWRITE的方式将数据全量回刷至聚合表:
|
步骤三:任意长周期UV查询
查询时,从聚合结果表中按照查询维度进行聚合计算,可以通过聚合表查询基础维度任意组合任意时间段的UV、PV,查询效率相比传统的SQL更高,通常为毫秒级的查询响应,也能支持较高的QPS查询。示例如下:
查询某天的UV、PV。
SELECT RB_CARDINALITY(RB_OR_AGG(rb_uid)) AS uv, country, prov, city, SUM(pv) AS pv FROM dws_app_rb WHERE ymd = '20240329' GROUP BY country,prov,city;
查询某个月的UV、PV。
SELECT RB_CARDINALITY(RB_OR_AGG(rb_uid)) AS uv, country, prov, city, SUM(pv) AS pv FROM dws_app_rb WHERE ymd >= '20240301' AND ymd <= '20240331' GROUP BY country,prov,city;
方法2:TEXT字段类型结合Mapping表的RB长周期UV计算
在实际业务场景中,大多数表的ID字段会使用TEXT类型,但是RB不支持TEXT类型,因此需要使用Serial类型构建一张Mapping表,来实现基于RB的UV高效计算。
适用场景
适用于大数据量(亿级)的任意长周期高QPS的UV计算,且计算UV的字段是TEXT类型(需要精确去重的字段是TEXT类型)的场景。
方案流程
该方案的主要流程如下:
步骤一:创建一张用户明细表,用于存放业务所有维度的明细数据。创建一张历史用户映射Mapping表,用于存放历史中每个访问过的用户ID(UID)和对应的INT32数值。
步骤二:明细表和用户映射Mapping表进行JOIN,并按照最细粒度基础维度进行GROUP BY,将前一天的所有数据根据最大的查询维度聚合出的UID结果转换为RoaringBitmap形式,写入聚合结果表(每天百万条)。
步骤三:按照查询维度查询聚合结果表,对关键的RoaringBitmap字段进行OR运算,去重并统计基数,即可得出对应用户数UV,统计聚合结果表中的记录数即可得出PV,达到亚秒级查询。
步骤一:准备基础数据
创建RoaringBitmap Extension。
使用RoaringBitmap前需要创建RoaringBitmap Extension,Extension是DB级别的函数,一个DB只需执行一次即可。语法如下:
说明RoaringBitmap Extension只能加载在public Schema下。
CREATE EXTENSION IF NOT EXISTS roaringbitmap;
准备用户明细表。
准备一张用户明细表,记录用户完整的明细数据。通常来说,明细表会存储所有明细数据,数据量大,建议使用分区表,按天分区,按天更新,使用更方便。
BEGIN; CREATE TABLE IF NOT EXISTS ods_app_detail ( uid text, country text, prov text, city text, channel text, operator text, brand text, ip text, click_time text, year text, month text, day text, ymd text NOT NULL ) PARTITION BY LIST (ymd) ;--通常来说明细表数据量大,按天分区更方便。 CALL set_table_property('ods_app_detail', 'orientation', 'column'); CALL set_table_property('ods_app_detail', 'bitmap_columns', 'country,prov,city,channel,operator,brand,ip,click_time, year, month, day, ymd'); --distribution_key根据需求设置,根据该表的实时查询需求,从什么维度做分片能够取得较好效果即可 CALL set_table_property('ods_app_detail', 'distribution_key', 'uid'); --用于做where过滤条件,包含完整年月日时间字段推荐设为clustering_key和event_time_column CALL set_table_property('ods_app_detail', 'clustering_key', 'ymd'); CALL set_table_property('ods_app_detail', 'event_time_column', 'ymd'); COMMIT;
准备RB聚合结果表。
创建RB聚合结果表,用于存放RoaringBitmap聚合后的结果数据。
聚合后的数据量通常不多(聚合后单天数据量仅几百万条),建议聚合结果表使用非分区表,数据回刷时更方便。或者使用按月/季度分区(若按天分区,数据量小,分区多可能会产生较多小文件,影响内存水位)。
以country、prov、city为维度构建基础维表,并设置为Distribution Key,查询时可以根据维度查询。如果存在多个GROUP BY字段(超过3个),建议将使用最频繁的字段设置为Distribution Key。
将查询维度字段和日期分区键作为主键,防止重复插入数据。
将日期过滤字段作为Clustering Key和Event Time Column,加快过滤查询。
代码如下:
BEGIN; CREATE TABLE dws_app_rb( rb_uid roaringbitmap, -- UV计算 country text, prov text, city text, ymd text NOT NULL, --日期字段 pv integer, -- PV计算 PRIMARY key(country, prov, city, ymd)--查询维度和时间作为主键,防止重复插入数据 ); CALL set_table_property('dws_app_rb', 'orientation', 'column'); --clustering_key和event_time_column设为日期字段,便于过滤 CALL set_table_property('dws_app_rb', 'clustering_key', 'ymd'); CALL set_table_property('dws_app_rb', 'event_time_column', 'ymd'); --distribution_key设为group by字段 CALL set_table_property('dws_app_rb', 'distribution_key', 'country,prov,city'); END;
准备用户Mapping表。
RoaringBitmap计算的用户ID字段类型必须为32位INT类型,且越稠密越好,而常见的业务系统或者埋点中的用户ID很多是字符串类型,因此使用Serial类型(自增的32位INT)构建一张用户映射表,实现用户映射的自动管理和稳定映射。
BEGIN; CREATE TABLE uid_mapping ( uid text NOT NULL, uid_int32 serial, PRIMARY KEY (uid) ); --将uid设为clustering_key和distribution_key便于快速查找其对应的int32值 CALL set_table_property('uid_mapping', 'clustering_key', 'uid'); CALL set_table_property('uid_mapping', 'distribution_key', 'uid'); CALL set_table_property('uid_mapping', 'orientation', 'row'); COMMIT;
步骤二:构建RB导入至聚合表
导入与更新用户映射表。
用户映射表全量初始化。
首次将全量UID数据导入映射表进行数据初始化。您也可以根据业务的查询时间范围,选择时间按字段过滤,导入部分周期的UID数据至Mapping表。此处以导入半年的UID数据为例。
--根据业务的查询时间范围,可以选择时间字段过滤,导入部分周期的uid数据至mapping表,示例导入半年的数据。 INSERT INTO uid_mapping (uid) B SELECT distinct (uid) FROM ods_app_detail WHERE B.ymd >= '20231201' AND B.ymd <='20240502';
验证Mapping表的数据正确性。
Mapping表数据导入后,需要验证其数据是否与明细表的数据一致。
--验证数据正确性 SELECT COUNT(*) FROM uid_mapping;
同时也需要验证Serial的连续性。在某些场景中,会使用TRUNCATE+INSERT的方式多次全量初始化Mapping表,而执行TRUNCATE时,Serial类型的Sequence不会被重置,导致Serial值浪费,在多次导入之后会超过Serial的INT32位上限。
--验证serial的连续性是否正确 SELECT MAX(uid_int32),MIN(uid_int32) FROM uid_mapping;
更新用户映射表。
随着明细表的UID数据更新,也需要及时更新用户映射表的数据。若明细表中仅涉及某天的UID数据更新,可使用INSERT ON CONFLICT更新用户映射表,如果无法确定UID的更新范围,可以进行全量数据回刷,但数据量较大可能导致数据回刷时间变长。
示例:使用INSERT ON CONFLICT DO NOTHING更新前一天的用户映射表,DO NOTHING会保证只更新数据,不会重复写入。
--更新前一天的用户映射表数据 INSERT INTO uid_mapping (uid) SELECT distinct (uid) FROM ods_app_detail WHERE ymd = '20240503' ON conflict do nothing;
导入与更新聚合结果表。
更新完用户映射表后,将数据做聚合运算后插入聚合结果表,主要步骤如下:
通过明细表JOIN用户映射表,得到聚合条件和对应的
uid_int32
。按照聚合条件做聚合运算后,将数据插入RoaringBitmap聚合结果表,可以根据业务情况选择聚合周期,如天、月等。
只需进行一次聚合,存放一份数据,数据条数等于UV数量。
插入数据至聚合结果表,命令如下。此处以聚合半年的数据至结果表为例。
WITH aggregation_src AS ( SELECT B.uid_int32, A.country, A.prov, A.city, A.ymd FROM ods_app_detail A INNER JOIN uid_mapping B ON A.uid = B.uid WHERE A.ymd >= '20231201' AND A.ymd <='20240502') INSERT INTO dws_app_rb SELECT RB_BUILD_AGG (uid_int32), country, prov, city, ymd, COUNT(1) FROM aggregation_src GROUP BY country, prov, city, ymd;
更新聚合结果表。
明细表和Mapping表更新后,也需要更新聚合结果表。可以根据实际业务中明细表的更新情况选择适当的方式更新聚合结果表。
增量更新:明细表周期性写入和更新,如按天写入,按天回刷,可以使用INSERT的方式直接写入最新分区数据。
全量回刷:明细表无固定时间范围的写入与更新,即无法识别明细表的增量数据,则无法使用新增数据更新聚合结果表,建议使用INSERT OVERWRITE的方式全量回刷聚合结果表。
步骤三:任意长周期UV查询
查询时,从聚合结果表中按照查询维度做聚合计算,可以通过聚合表查询基础维度的任意组合和任意时间段的UV、PV,查询效率相比传统的SQL更高,通常为毫秒级的查询响应,并且能支持较高的QPS查询。示例如下:
查询某天的UV、PV。
SELECT country, prov, city, RB_CARDINALITY(RB_OR_AGG(rb_uid)) AS uv, SUM(pv) AS pv FROM dws_app_rb WHERE ymd = '20240329' GROUP BY country,prov,city;
查询某一个月的UV、PV。
--查一个月的pv、uv SELECT country ,prov ,RB_CARDINALITY(RB_OR_AGG(rb_uid)) AS uv ,SUM(pv) AS pv FROM public.dws_app_rb WHERE ymd >= '20240301' AND ymd <= '20240331' GROUP BY country,prov,city;
方法3(高级方法):分Bucket的RB长周期UV计算
在实际场景中,会存在多个大表关联计算基数的需求,例如标签表和属性表关联计算人群交并差的基数、行为表和属性表关联计算行为基数等的画像分析场景,通过Hologres RoaringBitmap结合Bucket分桶的方案来实现高效的基数计算。通过分桶,可以将Bitmap拆分成多段打散存储,充分利用并发计算的能力,实现人群的高度压缩,减少对数据的IO操作,提升计算效率。
适用场景
适用于大规模数据量(亿级以上)的任意长周期UV查询,通常为精确去重的字段位数长(可能会超过INT32位)或字段基数低(数据的重复度高,例如性别)的场景,更多是多标签关联计算人群基数的场景。
原理介绍
Bucket是将字段拆分成不同的桶,让数据可以打散分布到各个Shard上。如果要实现较好的查询性能,分桶方式及分桶数量是关键因素。结合大多数的实际业务使用情况:
分桶方式:常见的比较高效的分桶方式是通过位运算计算出字段的高位数和低位数,然后将低位存入Bitmap,高位设置为Bucket。
分桶数量:通常实例中的单个Table Group的Shard数不会超过256,对于INT32位的数据来说,int_value>>24(即高8位)存为桶号,低24位存成Bitmap,即可将数据平分到多个Shard上,并且每个Shard上的数据都会聚集好,利于并发。
推荐的分桶计算公式:
方案流程
分Bucket的RB周期UV计算的方案流程如下:
为了降低难度,本示例中的UID字段使用INT类型,如果UID字段是TEXT类型,可参考方法2创建Mapping表来构建RB,分桶的方式不变。
步骤一:准备基础数据
创建RoaringBitmap Extension。
使用RoaringBitmap前需要创建RoaringBitmap Extension,Extension是DB级别的函数,一个DB只需执行一次即可。语法如下:
说明RoaringBitmap Extension只能加载在public Schema下。
CREATE EXTENSION IF NOT EXISTS roaringbitmap;
准备用户的行为表和属性表。
鉴于Bucket的方案通常用在多表关联计算画像的场景,因此此处准备两张表,一张为用户行为表,记录完整的明细行为,另一张为属性表,记录用户的属性,例如性别、年龄等。通过行为和属性的关联分析,可以计算任意维度的人群基数。
用户行为表ods_user_behaviour_detail。
--行为明细数据 BEGIN; CREATE TABLE IF NOT EXISTS ods_user_behaviour_detail ( uid int, operator text, channel text, shop_id text, time text, ymd text NOT NULL ); CALL set_table_property('ods_user_behaviour_detail', 'orientation', 'column'); --distribution_key根据需求设置,根据该表的实时查询需求,从什么维度做分片能够取得较好效果即可 CALL set_table_property('ods_user_behaviour_detail', 'distribution_key', 'uid'); --用于做where过滤条件,包含完整年月日时间字段推荐设为clustering_key和event_time_column CALL set_table_property('ods_user_behaviour_detail', 'clustering_key', 'ymd'); CALL set_table_property('ods_user_behaviour_detail', 'event_time_column', 'ymd'); COMMIT;
用户属性表dim_userbase。
--用户属性数据 BEGIN; CREATE TABLE IF NOT EXISTS dim_userbase ( uid int, age text, gender text, country text, prov text, city text ); CALL set_table_property('dim_userbase', 'orientation', 'column'); CALL set_table_property('dim_userbase', 'distribution_key', 'uid'); COMMIT;
准备RB聚合结果表。
创建RB聚合结果表,用于存放RoaringBitmap聚合后的结果数据。
聚合后的数据量通常不多(聚合后单天数据量仅几百万条),建议聚合结果表使用非分区表,数据回刷时更方便。或者使用按月/季度分区(若按天分区,数据量小,分区多可能会产生较多小文件,影响内存水位)。
以country、prov、city为维度构建基础维表,并设置为Distribution Key,查询时可以根据维度查询。如果存在多个GROUP BY字段(超过3个),建议将使用最频繁的字段设置为Distribution Key。
将查询维度字段和日期分区键作为主键,防止重复插入数据。
将日期过滤字段作为Clustering Key和Event Time Column,加快过滤查询。
行为表聚合结果表dws_user_behaviour_rb。
--行为表聚合结果表 BEGIN; CREATE TABLE dws_user_behaviour_rb( rb_uid roaringbitmap, -- UV计算 bucket int NOT NULL, -- 分桶字段 operator text, channel text, shop_id text, time text, ymd text NOT NULL, PRIMARY key(operator,channel,shop_id,time, ymd,bucket)--查询维度和时间作为主键,防止重复插入数据 ); CALL set_table_property('dws_user_behaviour_rb', 'orientation', 'column'); --clustering key和event_time_column设为日期字段,便于过滤 CALL set_table_property('dws_user_behaviour_rb', 'clustering_key', 'ymd'); CALL set_table_property('dws_user_behaviour_rb', 'event_time_column', 'ymd'); --将分桶字段bucket设置成distribution key CALL set_table_property('dws_user_behaviour_rb', 'distribution_key', 'bucket'); END;
用户属性聚合结果表dim_userbase_rb。
--用户属性表的聚合结果表 BEGIN; CREATE TABLE IF NOT EXISTS dim_userbase_rb ( rb_uid roaringbitmap, -- UV计算 bucket int NOT NULL, -- 分桶字段 age text, gender text, country text, prov text, city text, PRIMARY key(age,gender,country, prov,city,bucket)--查询维度作为主键,防止重复插入数据 ); CALL set_table_property('dim_userbase_rb', 'orientation', 'column'); CALL set_table_property('dim_userbase_rb', 'distribution_key', 'bucket');--bucket设置成distribution key,可以利用local join的能力 COMMIT;
步骤二:构建RB导入至聚合表
准备好明细表后,需要将明细结果表数据写入聚合结果表,同时也需要将UID进行分桶存入Bucket字段中。分桶的划分需要根据业务的数据规模、数据分布特征综合考虑,本示例中将UID划分为256个桶,这样可以将UID充分打散到每个Shard,系统也会保证同一个桶的UID数据分布在相同的Shard,查询时并行计算,实现高性能查询。最终的导入命令如下:
行为表数据写入聚合表:
--明细表数据写入聚合表 INSERT INTO dws_user_behaviour_rb SELECT RB_BUILD_AGG(uid), uid >> 24 AS bucket,--右移24位,这样高8位作为桶,低24为作为bitmap operator, channel, shop_id, time ymd FROM ods_user_behaviour_detail WHERE ymd >= '20231201' AND ymd <='20240503'
属性表数据写入聚合结果表:
--属性表数据写入聚合结果表 INSERT INTO dim_userbase_rb SELECT RB_BUILD_AGG(uid), uid >> 24 AS bucket,--右移24位,这样高8位作为桶,低24为作为bitmap age, gender, country, prov, city FROM dim_userbase GROUP BY age,gender,country,prov,city,bucket;
当明细表更新后,根据明细表的更新粒度,聚合表的更新可以分为增量更新或者全量更新。二者区别如下:
增量更新聚合表:明细表数据有规律地更新或新增,例如只更新昨天的分区,可以通过INSERT的方式将增量数据写入聚合表。
全量更新聚合表:明细表的数据无规律地更新,无法快速计算出增量数据,导致无法用新增数据更新聚合表,因此使用全量回刷方式写入聚合表。
步骤三:任意长周期UV查询
查询时,可以通过聚合表查询基础维度任意组合任意时间段的UV、PV,查询效率相比传统SQL更高。示例如下:
--查询:gender = 'man'&country = '北京'&operator = '购买'&shop_id ='1'的客群数
SELECT
SUM(RB_CARDINALITY (rb_and (t1.rb_uid, t2.rb_uid)))
FROM (
SELECT
rb_or_agg (rb_uid) AS rb_uid,
bucket
FROM
dws_user_behaviour_rb
WHERE
OPERATOR = '购买'
AND shop_id = '1'
AND ymd = '20240501'
GROUP BY
bucket) t1
JOIN (
SELECT
rb_or_agg (rb_uid) AS rb_uid,
bucket
FROM
dim_userbase_rb
WHERE
gender = 'man'
AND country = '北京'
GROUP BY
bucket) t2 ON t1.bucket = t2.bucket;