【程序设计】11.2 [大数据] 游标上的函数
11.2 游标上的函数
仅是数数量,SPL 还有个 skip 函数。
A |
|
1 |
=file("data.txt").cursor@t() |
2 |
=A1.skip() |
skip 本意是跳过若干记录不读取,同时返回跳过的记录数,没有参数时就会走到最后,返回的跳过记录数也就是总记录数了。
看来直接使用游标上的函数会更简单,那么求和这些也有相应的游标函数吗?毕竟自己写循环还是比较麻烦,像 sum、max 这些都要先设定一个初值才行。
SPL 没有针对游标的 sum 函数,但是有 groups,上一节的平均订单金额可以这么写:
A |
|
1 |
=file("data.txt").cursor@t(amount) |
2 |
=A1.groups(;sum(amount):S,count(amount):C) |
3 |
=A2.S/A2.C |
我们学过,groups 函数的第一段参数填空时,就表示将全数据表分成一个组,相当于对全表做汇总了,所以也就可以记录出所有记录的订单额和数量。A2 的结果集是一个只有一行记录的数据表,A3 再计算就得到了平均值(排列取字段时会缺省取其第一个成员的字段)。
为啥不像序列那样,给游标提供一个 sum,count 这种函数?
因为游标遍历一次很慢,在遍历过程中要尽量多计算出一些结果。如果提供 sum 和 count 等函数,做一次 sum 要遍历一次游标,再做一次 count 又要遍历一次,性能就会非常差了。而且代码中也需要再次创建一个新游标,也很麻烦,所以 SPL 干脆不提供了。直接在 groups 里一次遍历中计算出多个结果。
不过,如果总是用 groups 针对全表汇总,那也总是会返回一个一行的序表,虽然使用上没啥问题,但感觉还是不太好,还得起字段名,也略有麻烦。所以 SPL 提供了一个 total 函数可以稍简化一点:
A |
|
1 |
=file("data.txt").cursor@t(amount) |
2 |
=A1.total(sum(amount),count(amount)) |
3 |
=A2(1)/A2(2) |
total 函数也是要求一次遍历计算出多个值,但它返回成序列了,不用起字段名了。
即然是 groups 函数,那么应该也可以用来分组汇总吧?
是的,比如我们可以按地区分组计算每个地区的订单金额合计和订单数量:
A |
|
1 |
=file("data.txt").cursor@t(area,amount) |
2 |
=A1.groups(area;sum(amount):S,count(amount):C) |
注意这次要把 area 字段也取出来了。
看起来,针对游标和之前针对序表排列做分组汇总在代码上完全没有区别。
确实是这样的,但游标还是有特殊之处,比如我们想再按月份再做个分组,如果是排列,那再写一句 groups 就行了,但如果是游标就不能再次使用了,它已经遍历结束而取不出记录了。这时候需要重新创建一个新游标再做 groups。
A |
|
1 |
=file("data.txt").cursor@t(area,amount) |
2 |
=A1.groups(area;sum(amount):S,count(amount):C) |
3 |
=file("data.txt").cursor@t(dt,amount) |
4 |
=A3.groups(month@y(dt);sum(amount):S,count(amount):C) |
但这样一来,游标数据被遍历了两次,速度就比较慢了。
确实是有这个问题。SPL 也提供一次游标遍历过程中计算出多套分组汇总值的方法,称为遍历复用,但这些内容属于性能优化的范畴,需要增加新的概念才能解释清楚,超出了本书的设计大纲,这里就不讲了,有兴趣的读者可以去参考 SPL 的其它资料。
如果使用 SQL,那就只能遍历多次,它不支持遍历复用。
游标上有 groups 函数,但并没有对应的 group 函数(其实 SPL 也有这个函数,但使用条件有些限制,和 groups 并不对应,我们下一节会提到)。也就是说,游标的 groups 函数并不是先用 group 拆分出分组子集之后再做汇总来做的。这是为什么?
因为没有必要,其实序表和排列的 groups 函数也不是先拆出分组子集再做汇总计算的。有很多汇总计算,类似 sum,count,max 等,都可以用循环依次访问集合成员的方法来实现,每个成员只是使用一次就行了,不需要我们前面说过的那种随机访问,这样的运算效率更高。如果仔细阅读过第五章的迭代函数,就会对这个原理理解得更透彻。
那么,为什么游标没有对应的 group 函数呢?
引入游标是为了处理内存装不下的大数据,原数据表无法装入内存,那么所有的分组子集显然也不可能装入。如果提供 group 函数,那还要把这些分组子集保持在外存中,这又是一件性能很差且很麻烦的事,而一定要用分组子集才能实现的运算,我们会想出其它更好的办法来做,所以就没有提供这种运算了。
我们理解了在游标上的聚合和分组运算,现在继续来了解其它运算。
选出函数 select 在游标上会是什么样子呢?比如我们想找出订单金额超出 5000 的订单。
这时候,就和序列和排列的运算有很大不同了。针对排列的选出函数,直接就把结果排列计算出来了,我们可以查看其中的成员。但是游标却不行,因为游标对应的是大数据,选出后的结果仍然可能是大数据,我们没办法把它算出来成为一个内存中的序表,它还得是个游标,所以,游标上的 select 函数仍然返回一个游标。
然而,我们怎么查看选出的结果呢?
还是 fetch,被 select 过的游标仍然是游标,那就也可以使用 fetch 函数来取数据。只不过,因为它是游标,我们不能把数据全部取出来,又是只能一次取一部分来看。
A |
|
1 |
=file("data.txt").cursor@t() |
2 |
=A1.select(amount>=5000) |
3 |
=A2.fetch(100) |
A2 对游标做了条件过滤,仍然返回一个游标,然后 A3 可以从中取出 100 条记录构成的序表来查看。
执行这段代码,可以看一下 A2 的返回值,会发现是个奇怪的东西。游标在没有 fetch 时是看不到数据的,反正也没法显示,SPL 就随便把它显示成内部对象的名称了。
然而,对着游标执行 select 函数到底是什么意思?它会把游标中的数据都遍历一次吗?
应该不会,不然,遍历过程中碰到符合条件的记录会放哪里呢?我们说过,内存是放不下的,难道再放进硬盘吗?
SPL 没有这么做,也没有必要这么做。游标的 select 函数其实没有做任何实质性的选出动作,它只是在游标上记着有个选出动作将要做。等到 fetch 数据时,SPL 才会来看,这个游标上还记着有个 select 动作,这时候才会真地来执行条件判断以选出记录。把 fetch 要求的记录(比如这里是 100 条)选出来之后,它又停下不再计算了,等待下次 fetch 再计算。
select 函数返回的这种游标,我们称为延迟游标,它只有在真下取数据时才会执行实质性的计算。这和刚才讲过的 groups 不同,groups 会立即触发游标遍历动作,而且返回的也不再是游标。
学习游标函数时,要了解其返回值,知道它是不是延迟游标。
延迟游标技术能让游标相关的代码写出来和排列上的计算很像。比如我们想针对金额在 5000 以上的订单按地区分组汇总,如果是排列,我们知道只要 select 再 groups 就可以了;而对于游标,其实也一样:
A |
|
1 |
=file("data.txt").cursor@t() |
2 |
=A1.select(amount>=5000).groups(area;sum(amount):S) |
和排列上运算的写法完全一样,非常简单。
所以,我们通常也可以不太关注这些函数返回的游标是不是延迟的,简单地理解为这些函数就在执行相应的计算,一般也不会有问题。但还是要知道这个机制,在细致分析某些代码运行结果时还是必要的。
不过,游标还是有个和排列的关键不同点。
我们对某个排列执行 select 后,会得到一个新排列,但原排列是不会变的,仍然可以继续做各种其它的计算。而对游标执行 select 后得到的新游标则是和原游标相关的,这个新游标在遍历的过程中,原游标也会同时被遍历,新游标遍历结束而不可再用时,原游标也会遍历结束而不可再用。反过来,去遍历原游标也会影响新游标。在某个游标上用执行了函数得到了新的延迟游标后,原则上原来那个游标就不要再用了,不然会出现非常混乱的后果。
游标和排列本质上还是不同的,毕竟内存和外存确实有较大的差异。SPL 只是尽量让它们很相像,这样学习和编写代码都会更方便一点。
因为游标没法随机访问,定位就没什么意义了,所以游标上没有 pselect 这些函数。另外,align 和 enum 这些函数常常只会和小数据相关,也就没有对应的游标版本。
但 new,run,derive 这些函数却很有意义,这些也都是延迟游标。
比如我们为订单金额超过 5000 的订单上增加一个平均单价的字段:
A |
|
1 |
=file("data.txt").cursor@t() |
2 |
=A1.select(amount>=5000) |
3 |
=A2.derive(amount/quantity:price) |
4 |
=A3.fetch(100) |
可以基于延迟游标上再创建一个延迟游标。SPL 仍然不会立即计算,而是记下来这个游标上有个 select 动作、还有个 derive 动作,等到 fetch 的时候再来一一执行。
new 和 run 是类似的,事实上,用于扩展的 news 也可以在游标上执行。这里就不再举例了。
外键相关的 switch 和 join 对游标是有意义的。
假定,每个地区会有各自的税率,现在我们想按月份统计税金超过 100 的订单数量和金额合计。
A |
|
1 |
[East,West,North,South,Center] |
2 |
[0.05,0.06,0.03,0.04,0.08] |
3 |
=A1.new(~:area,A2(#):taxrate).keys(area) |
4 |
=file("data.txt").cursor@t() |
5 |
=A4.switch(area,A3).select(area.taxrate*amount>100) |
6 |
=A5.groups(month@y(dt):ym;sum(amount):S,count(amount):C) |
switch 返回的也是延迟游标。groups 计算过程中要读取数据时才会去实际处理 switch 将外键字段 area 转换成维表的记录。
类似地,我们想把金额在 5000 以上订单上增加一个税金字段后写入另一个文件。
A |
|
… |
… |
5 |
=A4.select(amount>5000) |
6 |
=A5.join(area,A3,taxrate:tax).run(tax=tax*amount) |
7 |
>file("data.csv").export@ct(A6) |
export 函数可以把游标直接写入文件,@c 表示写成逗号分隔的 csv 文件,@t 还是将字段名写成第一行作为标题的意思。在 A6 中,我们先用 join 函数把税率加到游标数据,然后再用 run 把税率改算成税金。延迟游标的函数可以多级使用,写法和内存中的排列完全一样。