用户行为分析系列实践 10 – 帐户有序存储

目标任务

用户事件表T结构和部分数据示例如下:

Time

UserID

EventType

2022/6/1 10:20

1072755

Search

2022/6/1 12:12

1078030

Browse

2022/6/1 12:36

1005093

Submit

2022/6/1 13:21

1048655

Login

2022/6/1 14:46

1037824

Logout

2022/6/1 15:19

1049626

AddtoCart

2022/6/1 16:00

1009296

Submit

2022/6/1 16:39

1070713

Browse

2022/6/1 17:40

1090884

Search

T表字段说明:

字段名

数据类型

字段含义

Time

日期时间

事件发生的时间戳,精确到毫秒

UserID

字符串

用户ID

EventType

字符串

事件类型

计算任务:

统计指定时间段内,每种事件类型下的发生次数,以及去重用户数。

需要考虑的问题是,去重用户数非常多,内存放不下。

实践技能

关于有序去重/有序增量的相关知识可参考:

SQL 提速:大数据 DISTINCT 和 COUNT(DISTINCT)

SPL 的有序存储

1. 将数据按帐户排序后实施去重运算

按用户ID排序后另存为有序数据,可以实现快速去重。对有序字段去重计数,不需要在内存中保留结果集,遍历过程中只需要和上一条记录对比即可,速度快且不会有内存溢出问题。

2. 外部排序

数据量太大超出内存可容纳范围时,数据库的排序往往很慢,此时可以把数据先读出到集文件,在集算器中排序,再把结果存为组表文件

3. 有序增量

新增数据通常并不会按帐户字段继续有序,所以不能直接追加到有序数据的末尾,而直接将有序数据和新增数据一起重新做常规大排序,会非常耗时。

采用组表的补表机制,另外保持一个小规模的有序数据 (即补表)。新增数据排序后和补表归并,原组表不变。经过适当的时间后,补表积累到合适大小时,再和原组表归并。做去重计算时,要从原组表和补表中分别读取,归并后遍历,会比只有一份有序数据时性能下降一些,但仍能利用有序快速去重。

这个适当时间的确定,与新增数据的周期有关。比如每天都有新增数据,则每个月做一次原组表和补表的归并。补表不会超过一个月的数据量,原组表存储一个月之前的所有数据。也就是说补表可能会比原组表小很多,所以每天归并的数据量相对较小,很快就能完成数据追加。每个月才需要完成一次全量有序归并,耗时长一些也可以接受。

4. 并行计算

生成组表时指定按UserID字段分段,保证不把UserID相同的记录分到不同段里,之后统计时对组表产生多路游标,实现并行计算,进一步提高性能。

示例代码

1、 按用户ID排序后转储T表的基本方法


A

1

=connect("demo").cursor@x("select * from T order by UserID,Time")

2

=file("T.ctx").create@p(#UserID,#Time,EventType,…)

3

=A2.append(A1)

4

>A2.close()

A1 读事实表,按UserIDTime排序

A2 产生组表文件的结构,@p选项表示组表将按第一个字段(这里是UserID)为单位分段,在并行计算时将不会把UserID相同的记录分配给不同的线程,确保并行计算的正确性。无此选项时,将简单地按记录为单位分段,可能把同一个UserID的数据分配给两个线程,在有序去重统计时会出错。前面章节中没有用到有序去重算法,也就没有特别的分段要求,不用@p也可以正确地并行计算。

A3 把数据输出到A2组表文件中

2、 外部排序

如果数据量很大,超出了内存可容纳范围,需要利用硬盘缓存才能排序的话,数据库的排序可能会非常慢,此时可以先把数据导出到集文件,再利用集算器排序,结果存为组表文件:


A

1

=connect("demo").cursor@x("select * from T")

2

=file("T.btx").export@b(A1)

3

=file("T.ctx").create@p(#UserID,#Time,EventType,…)

4

=file("T.btx").cursor@b(UserID,Time,EventType,…).sortx(UserID,Time;1000000)

5

=A3.append(A4)

6

>A3.close()

A1 读事实表不排序

A2 把数据输出到集文件

A3 产生组表数据结构

A4 读集文件,产生游标,按UserID,Time排序。此时可以在sortx(…;n)n参数中指定缓冲区的行数,如果无法确定缓冲区的行数,可以不用此参数,sortx函数会自动算出一个相对合适的缓冲区行数。一般建议缓冲区占用的内存不超过可用内存的一半,占用多了会导致java虚拟机变慢,在内存足够大的情况下,缓冲区越大排序越快

A5 把排好序的数据输出到组表文件

3、 有序增量

增量数据:用时间戳来识别增量数据,每天0点以后,把前一天的增量数据追加到组表文件里:


A

B

1

=connect("demo").cursor@x("select * from T where Time>=? && Time<? order by UserID,Time",date(now()-1), date(now()))

2

=file("T.ctx").open()


3

=A2.append@a(A1)


4

if day(now())==1

>A2.reset()

5

>A2.close()


A1 读事实表的增量部分数据,因为增量比较小,直接按UserID,Time排序

A2 打开组表文件

A3 把增量数据以归并的方式输出到组表文件的补表中,@a选项表示以归并的方式输出到补表文件中

A4-B4 判断当前是否为每月1日,如果是,对组表和补表进行合并,补表清空,这个功能由reset()函数来完成

产生组表时使用了@p选项,增量数据追加到补表时,SPL也会自动按正确的方式处理,保证后续的并行计算正确。

4、 对按用户有序的组表计算去重用户数


A

1

>start=date("2022-03-15","yyyy-MM-dd"),end=date("2022-06-16","yyyy-MM-dd")

2

=file("T.ctx").open().cursor@m(UserID, EventType;Time>=start && Time<=end;2).groups(EventType; count(1):Num, icount@o(UserID):iNum)

A2 在游标中对T表进行过滤,然后再分组汇总。icount@o()选项表示数据按照去重字段有序。数据准备时使用过@p,在这里只要简单使用@m选项就可以实现并行计算了。

遗留问题:数据进行去重计数前,还需要按Time字段过滤,由于为了解决去重计数的问题,数据先按UserID排序了,Time不再是第一有序字段,Time上的过滤条件就不能快速执行,只能在每个UserID下再利用有序来过滤,但每个UserID下的数据量很少,一般不会超过一个数据块,所以几乎不会发生直接跳块的现象,结果会相当于遍历所有数据。对于总数据量的时间跨度较大而待查询区间的时间跨度较小的场景,这种方法的性能会较差。如何既解决UserID的去重计数问题,又解决Time字段的过滤问题,会在后续的文章(双维有序)中介绍。

运行结果:

EventType

Num

iNum

AddtoCart

1845674

175476

Browse

3578901

348791

Login

4033000

393400

Logout

4033000

393400

Search

2947931

257539

Submit

867345

83375


以下是广告时间

对润乾产品感兴趣的小伙伴,一定要知道软件还能这样卖哟性价比还不过瘾? 欢迎加入好多乾计划。
这里可以低价购买软件产品,让已经亲民的价格更加便宜!
这里可以销售产品获取佣金,赚满钱包成为土豪不再是梦!
这里还可以推荐分享抢红包,每次都是好几块钱的巨款哟!
来吧,现在就加入,拿起手机扫码,开始乾包之旅



嗯,还不太了解好多乾?
猛戳这里
玩转好多乾