用户行为分析系列实践 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