针对去重Cube的优化实践思路分析

本文主要分享了作者在蚂蚁集团高管数据链路改造升级过程中,针对去重cube的优化实践。
引言
sql作为目前最通用的数据库查询语言,其功能和特性复杂度远不止大家常用的“select * from tbl”这样简单,一段好的sql和差的sql,其性能可能有几十乃至上千倍的差距。而写出一个好的能兼顾性能和易用性的sql,考验的不仅仅是了解到多少新特性新写法,而是要深入理解数据的处理过程,然后设计好数据的处理过程。
一、场景描述
在做数据汇总计算和统计分析时,最头疼的就是去重类指标计算(比如用户数、商家数等),尤其还要带多种维度的下钻分析,由于其不可累加的特性,几乎每换一种统计维度组合,都得重新计算。数据量小时可以暴力的用明细数据直接即时统计,但当数据量大时就不得不考虑提前进行计算了。
典型场景如下:省、市、区等维度下的支付宝客户端的日支付用户数(其中省、市、区为用户支付时所在的位置,表格中指标数据均为虚构的)。
存在一个情况,某用户早上在杭州市使用支付宝支付了一次,下午跑到绍兴市时又使用支付宝线下支付了一次。那么在统计省+市维度的日支付用户数,需要为杭州市、绍兴市各计1;但在省维度下,需要按用户去重,只能为浙江省计1。针对这种情况,通常就需要以cube的方式完成数据预计算,同时每个维度组合都需要进行去重操作,因为不可累加。本文将此种场景简称为去重cube。
二、常见的实现方法
直接计算,每个维度组合单独计算。比如单独生成省、省+市、省+市+区等维度组合的多张表。每个表只计算固定的维度。然后是数据膨胀再计算,如union all或者lateral view explode或者maxcompute的 cube计算功能,通过数据膨胀实现一行数据满足多种维度组合的数据计算方法,如下图所示。
这三种写法其实都类似,重点都在于对数据进行膨胀,再进行去重统计。其执行流程如下图所示,核心思路都是先把数据膨胀拆为多行,再按照“普通”的distinct去重统计,因此性能上本身无太大差异,主要在于代码可维护性上。
三、性能分析
上述方法核心都是先把数据膨胀拆为多行,再按照“普通”的distinct去重统计,本身性能无太大差异,主要在于代码可维护性上。这几种方案计算消耗会随着所需维度组合线性增加,同时还要叠加distinct本身的计算性能差的影响。
在实际实验中,我们发现,去重cube的计算过程中,80%+的计算成本消耗在数据膨胀和数据传输上。比如高管核心指标场景,需要计算各种组合维度下的支付用户数以支持分析。实际实验中,选取100亿数据x25种维度组合进行测试,实际执行任务如下图所示,其中r3_2为核心的数据膨胀过程,数据膨胀近10倍,中间结果数据大小由100gb膨胀至1tb、数据量由100亿膨胀至近1300亿,大部分计算资源和计算耗时都花在数据膨胀和传输上了。若实际的组合维度进一步增加的话,数据膨胀大小也将进一步增加。
四、一种新的思路
首先对问题进行拆解下,去重cube的计算过程核心分为两个部分,数据膨胀+数据去重。数据膨胀解决的是一行数据同时满足多种维度组合的计算,数据去重则是完成最终的去重统计,核心思路还是在于原始数据去匹配结果数据的需要。其中数据去重本身的计算量就较大,而数据膨胀会导致这一情况加剧,因为计算过程中需要拆解和在shuffle过程中传输大量的数据。数据计算过程中是先膨胀再聚合,加上本身数据内容的中英文字符串内容较大,所以才导致了大量的数据计算和传输成本。
而我们的核心想法是能否避免数据膨胀,同时进一步减少数据传输大小。因此我们联想到,是否可以采用类似于用户打标签的数据打标方案,先进行数据去重生成uid粒度的中间数据,同时让需要的结果维度组合反向附加到uid粒度的数据上,在此过程中并对结果维度进行编号,用更小的数据结构去存储,避免数据计算过程中的大量数据传输。整个数据计算过程中,数据量理论上是逐渐收敛的,不会因为统计维度组合的增加而增加。
4.1.核心思路
核心计算思路如上图,普通的数据膨胀计算cube的方法,中间需要对数据进行膨胀,再聚合,其中结果统计需要的组合维度数就是数据膨胀的倍数,比如上述的“省、省+市”共计两种维度组合,数据预计要膨胀2倍。
而新的数据聚合方法,通过一定的策略方法将维度组合拆解为维度小表并进行编号,然后将原本的订单明细数据聚合至用户粒度的中间过程数据,其中各类组合维度转换为数字标记录至用户维度的数据记录上,整个计算过程数据量是呈收敛聚合的,不会膨胀。
4.2.逻辑实现
明细数据准备:以用户线下支付数据为例,明细记录包含订单编号、用户id、支付日期、所在省、所在市、支付金额。最终指标统计需求为统计包含省、市组合维度+支付用户数的多维cube。
订单编号 用户id 支付日期 所在省 所在市 支付金额
2023111101 u001 2023-11-11 浙江省 杭州市 1.11
2023111102 u001 2023-11-11 浙江省 绍兴市 2.22
2023111103 u002 2023-11-11 浙江省 杭州市 3.33
2023111104 u003 2023-11-11 江苏省 南京市 4.44
2023111105 u003 2023-11-11 浙江省 温州市 5.55
2023111106 u004 2023-11-11 江苏省 南京市 6.66
整体方案流程如下图。
step1:对明细数据进行所需的维度提取(即group by对应字段),得到维度集合。
step2:对得到的维度集合生成cube,并对cube的行进行编码 (假设最终需要所在省、所在省+所在市 2种组合维度),可以用odps的cube功能实现,再根据生成的cube维度组合进行排序生成唯一编码。
原始维度:所在省 原始维度:所在省 cube 维度:所在省 cube 维度:所在市 cube行id(可通过排序生成)
浙江省 杭州市 浙江省 all 1
浙江省 杭州市 浙江省 杭州市 2
浙江省 绍兴市 浙江省 all 1
浙江省 绍兴市 浙江省 绍兴市 3
浙江省 温州市 浙江省 all 1
浙江省 温州市 浙江省 温州市 4
江苏省 南京市 江苏省 all 5
江苏省 南京市 江苏省 南京市 6
step3:将cube的行编码,根据映射关系回写到用户明细上,可用mapjoin的方式实现。
订单编号 用户id 支付日期 所在省 所在市 汇总cube id
2023111101 u001 2023-11-11 浙江省 杭州市 [1,2]
2023111102 u001 2023-11-11 浙江省 绍兴市 [1,3]
2023111103 u002 2023-11-11 浙江省 杭州市 [1,2]
2023111104 u003 2023-11-11 江苏省 南京市 [5,6]
2023111105 u003 2023-11-11 浙江省 温州市 [1,4]
2023111106 u004 2023-11-11 江苏省 南京市 [5,6]
step4:汇总到用户维度,并对 cube id集合字段进行去重 (可以用array 的distinct)
step5:按照cube id进行计数计算(由于step4已经去重啦,因此这里不需要再进行去重);然后按照映射关系进行维度还原。
cube id 下单用户数指标 cube 维度还原:所在省 cube 维度还原:所在市
1 3 浙江省 all
2 2 浙江省 杭州市
3 1 浙江省 绍兴市
4 1 浙江省 温州市
5 2 江苏省 all
6 2 江苏省 江苏省
over~
4.3.代码实现
with -- 基本的明细数据表准备base_dwd as ( select pay_no ,user_id ,gmt_pay ,pay_amt ,prov_name ,prov_code ,city_name ,city_code from tmp_user_pay_order_detail)-- 生成多维cube,并进行编码,dim_cube as ( -- step02:cube生成 select *,dense_rank() over(partition by 1 order by cube_prov_name,cube_city_name) as cube_id from ( select dim_key ,coalesce(if(grouping(prov_name) = 0,prov_name,'all'),'na') as cube_prov_name ,coalesce(if(grouping(city_name) = 0,city_name,'all'),'na') as cube_city_name from ( -- step01:维度统计 select concat('' ,coalesce(prov_name ,''),'#' ,coalesce(city_name ,''),'#' ) as dim_key ,prov_name ,city_name from base_dwd group by prov_name ,city_name ) base group by dim_key ,prov_name ,city_name grouping sets ( (dim_key,prov_name) ,(dim_key,prov_name,city_name) ) ))-- 将cubeid回写到明细记录上,并生成uid粒度的中间过程数据,detail_ext as ( -- step04:指标统计 select user_id ,array_distinct(split(wm_concat(';',cube_ids),';')) as cube_id_arry from ( -- step03:cubeid回写明细 select /*+ mapjoin(dim_cube) */ user_id ,cube_ids from ( select user_id ,concat('' ,coalesce(prov_name,''),'#' ,coalesce(city_name,''),'#' ) as dim_key from base_dwd ) dwd_detail join ( select dim_key,wm_concat(';',cube_id) as cube_ids from dim_cube group by dim_key ) dim_cube on dwd_detail.dim_key = dim_cube.dim_key ) base group by user_id)-- 指标汇总并将cubeid翻译回可理解的维度,base_dws as ( -- step05:cubeid翻译 select cube_id ,max(prov_name) as prov_name ,max(city_name ) as city_name ,max(uid_cnt ) as user_cnt from ( select cube_id as cube_id ,count(1) as uid_cnt ,cast(null as string) as prov_name ,cast(null as string) as city_name from detail_ext lateral view explode(cube_id_arry) arr as cube_id group by cube_id union all select cast(cube_id as string) as cube_id ,cast(null as bigint) as uid_cnt ,cube_prov_name as prov_name ,cube_city_name as city_name from dim_cube ) base group by cube_id)-- 大功告成,输出结果!!!select prov_name ,city_name ,user_cntfrom base_dws; 实际的执行过程(odps的logview)如下图。
4.4.实验效果
左边是基于cube打标方案的新链路。实验过程中将实验数据由100亿增加至200亿,组合维度数由原来的25个增加至50种组合维度,整体耗时在18分钟,若只计算和原始数据量、组合维度均相同的数据,整体计算耗时可控制在10分钟内。
右边是基于数据膨胀计算的老链路。实验数据设定为100亿,组合维度数为25种,中间过数据将膨胀至1300亿+,数据大小更是膨胀至1tb+,整体耗时47分钟。若此方案扩展至新方法的200亿数据x50种组合维度,中间过程数据将膨胀至4000亿+,数据大小增加将膨胀至3tb+,整体计算耗时预估将达到2.5小时+。
新方法目前已经在业务核心高管链路上线,在数据统计维度组合、数据计算量都大幅增加的情况下,整体核心指标产出相较于以往,进一步提前1小时以上,有效的保障了相关核心指标数据的稳定性。
4.5.方案总结
常见的基于数据膨胀的cube计算方法,数据计算大小和过程数据传输量将随着组合维度的数量呈线性增长,组合维度数越多,花费在数据膨胀与shuffle传输的资源和耗时占比越高。在实验过程中,100亿实验数据x25种维度组合场景,过程数据已经膨胀至1300亿+,数据大小由100gb膨胀至1tb,当数据量和维度组合数进一步增加时,整个计算过程基本上难以完成。
为了解决数据膨胀过程中产生的大量过程数据,我们基于数据打标的思路反向操作,先对数据聚合为uid粒度,过程中将需要的维度组合转化编码数字并赋予明细数据上,整个计算过程数据呈收敛聚合状,数据计算过程较为稳定,不会随着维度组合的进一步增加而大幅增加。在实验中,将实验数据由100亿增加至200亿+,组合维度数由原来的25个增加至50种组合维度,整体耗时控制在18分钟左右。若同等的数据量,采用老的数据膨胀方案,中间过程数据将膨胀至4000亿+,数据大小将增加至3tb+,整体计算耗时将达到2.5小时+。
综上,当前的方案整体性能相较于以往有大幅度的提升,并且不会随着维度组合的增加而有明显的增加。但当前的方案也有不足之处,即代码的可理解性和可维护性,过程中的打标计算过程虽然流程较为固定,但整体上需要有个初始化理解的过程,目前尚无法做到普通unionall/cube等方案的易读和易写。另外,当组合维度数较少(即数据膨胀倍数不高)时,两者的性能差异不大,此时建议还是用原始普通的cube计算方案;但当组合维度数达几十倍时,可以改用这种数据打标的思路进行压缩,毕竟此时的性能优势开始凸显,并且维度组合数越高,此方案的性能优势越大。
五、其他方案
bitmap方案。核心思路在于将不可累计的数据指标,通过可累加计算的数据结构,近似实现可累加指标的效果。具体实现过程方案是对用户id进行编码,存入bitmap结构中,比如一个二进制位表示一个用户是否存在,消耗1个bit。维度统计上卷时,再对bitmap的数据结构进行合并和计数统计。
hyperloglog方案。非精确数据去重,相对于distinct的精确去重,性能提升明显。
这两种方案,性能上相对于普通的cube计算有巨大的提升,但bitmap方案需要对去重统计用的uid进行编码存储,对一般用户的理解和实操成本较高,除非系统级集成此功能,不然通常需要额外的代码开发实现。而hyperloglog方案的一大弊端就是数据的非精确统计。


毫米波雷达在环境监测中的应用:气象学和气候研究的前沿技术
CH-4无人机激光甲烷遥测仪的使用范围
为什么损失函数可以是这个形式?机器学习的终极目标是什么?
三菱飞机公司计划在日本国内进行SpaceJet M90飞机的飞行测试
挑战机遇并存的5G时代
针对去重Cube的优化实践思路分析
阻抗匹配总结
高精度IT/IN系列电流传感器在锂电池电流电压测量中的应用解析
比特币采矿业哪些技巧有利可图
在氮化镓中利用光电化学蚀刻深层高纵横比沟槽的进展
九轴姿态传感器的介绍和应用
PLC应用系统的设计原则
MySQL索引下推知识分享
Microchip推出全球第一款可编程USB2控制器集线器
电子芯闻早报:暴风魔镜发布新款VR 支持手势识别
什么是3C认证?
二极管用作平衡型AFC
影碟机的同轴输出
NVIDIA发布Q2季度财报 净利润同比下滑50%
联发科天玑800处理器怎么样_联发科天玑800处理器相当于骁龙多少