【性能优化】5.3 [有序遍历] 程序游标

 

【性能优化】5.2 [有序遍历] 有序分组子集

5.3 程序游标

继续帐户交易表,现在我们希望找出每月内连续 n 天都有交易的那些交易记录,然后按发生日期的星期几统计交易额度。

后半任务很简单,就是个普通的分组汇总。但找出连续 n 天都发生的交易却有些问题,即使数据表已经按帐户和日期排序,完成这种复杂运算也还是需要把分组子集取出来再写几句代码才能过滤出来,然后这些记录就在内存中,怎么让它们继续进行下一步的分组汇总运算呢?

一个容易想到的办法是,把这些算出来数据逐步写入一个缓存文件,然后再对这个缓存文件来做分组汇总:


A

B

1

=file("trades.ctx").open().cursor(id,dt,amount)

2

for A1;id

=A2.align@a(31,day(dt)).group@o(~==[])

3


=B2.select(~.len()>=n ).conj().conj()

4


=file("temp.btx").export@ab(B3,dt,amount)

5

=file("temp.btx").cursor@b().groups(day@w(dt);sum(amount))

A2 取出每个分组子集,B2 将其按交易日期对齐到 31 天,再用有序分组拆成连续空和不空的一些子集,在 B3 找出子集长度超过 n 的,即有连续 n 天都有交易或都没有交易,再合并起来就得到了有连续 n 天内发生的交易(连续 n 天没有交易的是空集,不会改变合并的结果)。注意这里要 conj 两次,因为 align@a 的结果是序列的序列。

计算完之后把结果写入临时文件,只要写入两个字段即可,最后再做一轮分组汇总。

这个计算过程显然会很慢,因为要把中间数据写到缓存文件中,有一次写和读的动作。其实这些数据只要直接拿去做分组汇总就可以了,没有必要写入外存。但分组函数只能基于序表或游标,如果自己对着每一批数据硬编码实现分组汇总,那就太麻烦了。

SPL 提供了程序游标可以完成这个机制,即把循环过程中产生的数据模拟成一个游标。


A

B

C

1

func

=file("trades.ctx").open().cursor(id,dt,amount)

2


for A1;id

=A2.align@a(31,day(dt)).group@o(~==[])

3



return B2.select(~.len()>=n ).conj().conj()

4

=cursor@c(A1).groups(day@w(dt);sum(amount))

定义一个子程序,在其中的循环里计算出需要的记录返回,cursor@c 函数将收集这个子程序的返回值拼成一个游标。当要向这个游标 fetch 请求数据时(比如这里的 groups),cursor 函数就会去执行子程序,收集到返回值,当收集到足够返回本次 fetch 的请求时,暂停子程序的执行并返回本次 fetch,但并不关闭子程序。下一次需要再要 fetch 数据时则再继续执行这个子程序,直到子程序彻底循环完毕后结束执行,cursor 函数也会返回游标结束。

这种过程可以把循环中不断计算出来的数据拼成一个游标,中间数据不必落地到文件,让这种复杂过程的运算也获得更高性能。这种游标称为程序游标

我们在前面讲过,SPL 有哈希大分组,但没有提供类似的排序算法。我们可以自己用程序游标的机制来实现一个粗略的,比如将订单表按订单额排序:


A

B

C

1

func

=file("orders.btx").cursor@b()

=100.(file(~))

2


=B1.groupn(int(amount/100)+1;C1)

>B1.skip()

3


for C1

return   B3.import@b().sort(amount)

4

return cursor@c(A1)

在 B2 中把订单按金额分成 100 份(这里我们假定订单金额范围在 0-10000 区间内基本平均分布,可以根据实际情况调整拆分方式,要保证这个拆分表达式和待排序字段值是单调不降或不增的,且每个拆分取值对应记录数较少,可以装入内存)。然后,只要按次序依次返回每一份的排序结果就可以了。cursor@c 函数会把这些返回值收集起来组织成游标。

【性能优化】5.4 [有序遍历] 前半序分组

【性能优化】 前言及目录