【性能优化】5.7 [有序遍历] 序号分组与可控分段
5.7 序号分组与可控分段
SPL 有个内存的序号分组汇总函数 groups@n,对于外存游标也有同样的函数。
如果能够把分组键值简单地计算成序号,那么分组时就不必再计算哈希值并比对了,可以直接用序号找到分组(去做汇总累计),性能就会更好。
注意一定要是简单地计算,如果计算过于复杂(比如查个表才知道序号),那还不如计算哈希值了。
A |
|
1 |
=file("trades.ctx").open().cursor(dt,amonut) |
2 |
=A1.groups@n(month(dt);sum(amount)) |
这样就能高效地按月份分组汇总交易额了,对于年月这类比较容易计算成序号的都合适,可以采用我们在第二章讲过的数据类型转换方案,从整数化的日期中迅速计算出年月序号。
groups@n 也支持多路游标。
groups 将返回小分组,那么对于大分组是不是也可以利用序号来提高性能呢?
理论上当然可以,这其实就是个简化的哈希大分组,分组键本身就是哈希值,省去了计算和比对的时间,而且返回的游标也会天然对分组键有序了。
A |
|
1 |
=file("trades2.btx").cursor(id,amount) |
2 |
=A1.groupx@n(id;sum(amount)) |
假设帐户 id 就是连续的自然数,这样可以计算每个帐户的交易额。
但是,与小分组时序号分组会比哈希分组有明显性能优势不同,大分组时序号分组并不会比哈希大分组明显更快。因为大分组的主要耗时在于缓存文件的写和读,内存中定位分组记录是用序号来还是哈希值,其时间差异相对于缓存文件的读写时间而言可以忽略不计。
groupx@n 对比常规哈希分组的意义在于它不会出现哈希分组有可能出现的“运气不好”而需要第二轮哈希的情况。
哈希分组时知道哈希值的范围(可由哈希函数决定),也就能相对合理地控制分段数量(回顾一下哈希大分组要将哈希值分段后将相应的分组写到不同缓存文件中)。但序号分组时却不知道最大序号是多少,系统自己估算时可能会较为保定,导致分出的段过多,也会影响性能。所以 group@n 增加了一个参数,允许人为控制分段规则:
A |
|
1 |
=file("trades2.btx").cursor(id,amount) |
2 |
=A1.groupx@n(id;sum(amount);1000000) |
groupx@n 的第三个参数表示每一段包含多少个序号(也就是分组记录),这样程序员可以根据数据特征和内存大小来控制分段数量了。
对于非序号分组时,人为控制分段也是有意义的。SPL 提供了另一个选项:
A |
|
1 |
=file("trades2.btx").cursor(id,amount) |
2 |
=A1.groupx@g(id;sum(amount);id\1000000) |
使用 groupx@g 将采用可由人为控制分段方案的哈希大分组算法,第三个参数是基于当前记录的表达式,计算结果是整数,决定把当前记录要分到哪一段。每段数据量都不大,可以再读入进内存使用哈希分组运算。这个例子中 id 不必是连续的自然数序号,但仍然是整数,可用来计算出分段序号。
序号的方法还可以用来排序。如果用排序的字段(或表达式)就是自然数,那只要简单按序号列出来就可以,不需要再比较,复杂度会比快速排序还低很多。
对于内存排序,SPL 提供了 sort@n 函数用于序号排序。对应地,对于大排序也可以使用 sortx@n,这时候会用类似哈希分组的办法来做排序,每次读入一批数据后分段写到多个缓存文件中,最后顺序读入每个缓存文件的数据再做序号排序。
A |
|
1 |
=file("trades2.btx").cursor(id,amount) |
2 |
=A1.sortx@n(id) |
2 |
=A1.sortx@n(id;1000000) |
sortx@n 也有与 groupx@n 类似的参数用来控制分段大小。
同样地,还有 sortx@g 可人为设置数据的分段方案,上一节用程序游标节实现的大排序就是这种策略,相当于如下代码:
A |
|
1 |
=file("orders.btx").cursor@b() |
2 |
=A1.sortx@g(amount;int(amount\100)) |
排序不能像分组那样用哈希值分段,因为哈希值和排序字段可能不同序,要基于排序字段计算出一个单调的分段序号。本质上不会有哈希排序,但会有 sortx@g 这种类似的方法。