irpas技术客

hiveSql完成递归计算2.0_@nanami_hive怎么写递归sql

irpas 6660

hiveSql完成递归计算 基础数据分析数据初步思路最终解决方案代码优缺点总结 最近遇到一个业务数据的坑,涉及到递归的计算思想,但是需要用hivesql完成计算。经过同事的提点完成了,这里记录一下过程。

基础数据

先说下基础数据: 有用户ID,下单时间,卡类型。想要按照有效期分别为年卡:365天,月卡:31天,两年卡:730天计算出每个用户每一单的会员开始结束时间。PS:表中没有一个用户在同一天买多单的情况

用户ID下单时间卡类型A2022-01-24 21:47:00月卡A2022-02-03 23:08:45月卡A2022-02-11 07:22:43月卡A2022-02-15 22:31:32年卡B2022-02-14 16:03:12月卡

有会员数据,只记录了购买会员的下单时间,和会员卡的类型,下单用户。不知道会员的开始结束时间(会员有效时间),需要通过上述已知的条件计算出来。一开始以为只要算一下下单时间作为当前单的开始时间,再通过卡类型的有效期计算下结束时间就好(不同类型的卡有不同的有效期,例如月卡31天,年卡365天)。但是后来开始写才发现没那么简单~ 用户可以在自己还是上一单会员的有效期内提前购买会员。如果这种情况还是按照上述逻辑计算的话。上一单还没结束,当前单的下单时间就不是当前单的有效期开始时间了。例如:

用户第N次买会员卡类型下单时间计算开始时间计算结束时间A1月卡2022-01-24 21:47:002022-01-24 21:47:002022-02-23 21:47:00A2月卡2022-02-03 23:08:452022-02-03 23:08:452022-03-05 23:08:45

上表中用户A买了两单,但是第二单是在第一单还没结束买的。所以第二单的开始结束时间计算的就有问题了! 真实的第二单开始时间应该是第一单的结束时间,即2022-02-23号。结束日期是这个日期加上卡类型对应的有效期,即2022-03-25号。

分析数据

准对这种情况可以分析数据。 用户可以在自己还是会员有效期内再次购买,且可以不止购买一次。类似上述基础数据,用户A购买了4单,其中后三单都是在相对前一单没结束的情况下买的。按照上述的逻辑,这种情况,用户A的后3单都应该是相对前一单的结束时间。类似递归计算,每一单都需要先计算出前一单。

第N单用户ID下单时间卡类型计算开始时间计算结束时间1A2022-01-24 21:47:00月卡当前单的下单时间当前单的下单时间+月卡有效天数312A2022-02-03 23:08:45月卡第1单的结束时间第1单的结束时间+月卡效天数313A2022-02-11 07:22:43月卡第2单的结束时间第2单的结束时间+月卡效天数314A2022-02-15 22:31:32年卡第3单的结束时间第3单的结束时间+年卡效天数365

利用hiveSQL实现,到这里可以先思考下怎么做


初步思路

将上述表按照用户id分组,下单时间升序row_number()开窗后进行自关联。左表序号 = 右表序号 + 1,会得到将每个用户的前后单拉平的数据。判断前一单会员的结束时间和当前单下单时间的大小,知道下一单购买时,是否在上一单的有效期内。从而判断当前单的开始时间用当前单的下单时间还是上一单的结束时间。见表:

a.用户IDa.下单时间a.结束时间a.卡类型a.序号b.下单时间b.卡类型b.序号A2022-01-242022-02-24月卡1\N\N\NA2022-02-032022-03-06月卡22022-01-24月卡1A2022-02-112022-03-14月卡32022-02-03月卡2A2022-02-152022-03-15年卡42022-02-11月卡3

可以看到上表中第二行数据是A用户的第二单(a.字段名)和第一单(b.字段名)拉平。其中a.结束时间是根据a.下单时间加上卡的对应有点天数计算的临时结束日期(目的是为了得到用户的第一单的结束日期),此时比较b.下单时间 和 a.结束时间即可知道用户的第二单是否是在第一单的有效期内下单。从而判断第二单的开始时间是第一单的结束时间 还是 第二单的下单时间。

仔细看发现此初步思路并不可行。因为是错一位关联的,可以看到上表中的第三行,应该需要用b.下单时间 和 用户第二单的真正会员结束时间做比较才对。此逻辑行不通。

最终解决方案

理一下思路,其实从一个用户角度出发,如果该用户购买了多单。每单可能是在上一单的有效期内购买(即续费),也有可能是在上一单有效期外购买(即复购)。 给用户的所有单按照时间升序排序后,如果是续费则为1,是复购则为0。有表如下:

第N单复购0/续费1第一单-第二单0第三单1第四单1……第N单0

即只需要找到除了第一单外每个连续续费前的第一个复购即可。按照这单复购连续递归计算出升序后的连续续费有效期。知道遇到下一个复购。即上表中的每个0管下面的连续一串的1。递归思路就在这里了。但是想了挺久没有想到怎么在已知用户的现有的所有订单数据的情况下计算出来(我感觉算不出来,如果有能算出来的思路课在下面的评论区 或者私信告诉我,我学习学习)。后来请教了大神同事。给了我一个思路。

先查询该表的所有数据最小下单时间作为第一天。因为表中没有一个用户在同一天买多单的情况。所以第一天出现的一定是用户的第一单。以此算下这些第一单的开始时间(即下单时间) 和 结束时间(下单时间 + 卡对应的有效天数) 写到表中,得结果表tableA,原表为tableB。再限制tableB中的下一天(即最小天的下一天,记为表tmp),tmp这些下单用户作为左表 左关联 tableA中每个用户的最大下单时间的那一单。判断tmp中的每个用户每一单的下单时间 和 该用户在今天之前的最后一单的结束时间。从而模拟出时间的流逝,每天用户下单 都和今天之前的最后一单做比较,判断是复购还是续费,从而对应的计算开始结束时间,再union all 今天之前的所有已经计算好的会员数据。就说这个思路真的挺牛,每单都能和自己的上一单已经算好的结束时间做关联!没明白的可以见下表,简单画一下一个用户的:

例如现在有用户A,下了很多单,有复购有续费的,最小下单时间取出来,最小下单则为该用户的第一单(时间是第一天):

用户第N次买会员卡类型下单时间计算开始时间计算结束时间A1月卡2022-01-24 21:47:002022-01-24 21:47:002022-02-23 21:47:00

此时时间流逝,来到该用户第二单的那天,取出该数据 作为左表A 左关联这个用户在今天之前的最后一单表b,即上表中的第一单:

a.用户a.第N次买会员a.卡类型a.下单时间b.用户b.第N次买会员b.卡类型b.下单时间b.开始时间b.结束时间A2月卡2022-02-03 23:08:45A1月卡2022-01-24 21:47:002022-01-24 21:47:002022-02-23 21:47:00

此时第二单(当前单)就和已经算好了正确的开始 、 结束时间的前一单关联到一起了。判断当前单的下单时间和上一单的正确的结束时间的大小即可判断出当前单是续费还是复购。从而计算出当前单正确的开始时间结束时间。再和表b做union all操作,将该用户的两单写到结果表中。

用户第N次买会员卡类型下单时间计算开始时间计算结束时间A1月卡2022-01-24 21:47:002022-01-24 21:47:002022-02-23 21:47:00A2月卡2022-02-03 23:08:452022-02-23 21:47:002022-03-26 21:47:00

此时,时间继续流逝,来到该用户的第三单,同第二单一样。将第三单取出,左关联 前两单的最后一单(即第二单)。这样第三单右可以和已经计算好正确的开始结束时间的第二单关联上了。再计算第三单的开始结束时间,再union all以为第四单做准备。

代码 insert overwrite table 结果表 select a.order_sn, -- 订单号 a.member_id, -- 用户ID cast(a.pay_amount as double) as pay_amount, -- 支付金额 a.create_time, -- 订单创建时间 case when a.total_amount = 98 then '月卡' when a.total_amount = 298 then '年卡' else '两年卡' end as card_type, -- 卡类型 -- 会员开始时间 case when b.member_id is null or cast(a.payment_time as string) > cast(b.end_time as string) then a.payment_time else b.end_time end as start_time, -- 会员结束时间 case when b.member_id is null or cast(a.payment_time as string) > cast(b.end_time as string) then case when a.total_amount = 98 then dateadd(a.payment_time,31,'dd') when a.total_amount = 298 then dateadd(a.payment_time,365,'dd') else dateadd(a.payment_time,730,'dd') end else case when a.total_amount = 98 then dateadd(b.end_time,31,'dd') when a.total_amount = 298 then dateadd(b.end_time,365,'dd') else dateadd(b.end_time,730,'dd') end end as end_time from (select * from 原表 where substr(create_time,1,10) = '${tdate}') a left join (select * from (select *,ROW_NUMBER() OVER(PARTITION BY member_id ORDER BY create_time desc) AS irank from 结果表 where substr(create_time,1,10) < '${tdate}' ) t where irank = 1 ) b on a.member_id = b.member_id union all select * from 结果表 where substr(create_time,1,10) < '${tdate}'; 优缺点

优点 能准确的计算出每单的开始结束时间,思路很棒。

缺点 当数据量很大时,即会员表开始时间在很久以前,这种思路只能是单线程跑下来,不能并行计算的,所以跑的会很慢。

总结

涛神(同事),牛x!!!


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #hive怎么写递归sql