【数据蒋堂】第 28 期:迭代聚合语法

sjjt-28

我们讨论过的常规聚合运算如 SUM/COUNT 和非常规聚合运算如 maxp/top,都是事先设计好的聚合函数。但如果我们想实现一个以前没有定义过的运算怎么办?是否可以用已有的语法和函数组合出来?比如想做连乘运算,显然这也算是一种聚合。

(题外话:连乘可以用 exp(SUM(ln(x))) 来做,不过这有点耍赖了,而且这还对付不了成员是负数的情况。)

要设计这样的语法方案,我们来看看这些聚合结果值是如何被程序计算出来的。

SUM:先设置一个初始值 0,然后遍历集合的每个成员,每次将成员值加到初始值上,直到成员被遍历完。

COUNT:设置初始值 0,遍历集合成员,每次碰到非空成员将初始值加 1,直到遍历完。

AVERAGE:这个不能边遍历边计算了,不过 AVERAGE=SUM/COUNT,算是个导出函数,不用考察了。

MAX:设置初始值为无穷小,遍历集合成员,每碰比初始值更大的成员值则替换初值值,直到遍历完。

MIN:和 MAX 一样,只是初始值和比较方向是反的。

我们发现,这些基本聚合运算的实现方案都有相同的过程:先设置一个初始值,然后遍历集合成员,让当前成员和初始值计算得到一个新的初始值,再进入下一轮循环,直到遍历完整个集成,那个初始值就变成我们需要聚合值了。

这样,我们可以设计一个实现迭代计算的聚合函数:

 A.iterate( x, a )

以 a 为初始值遍历集合 A,每次用当前初始值和当前遍历员计算表达式 x,得到的结果替换当前初始值,直到遍历完成,返回最后的 x 值。

这里有个问题,在 x 中用什么标识符或符号来表示当前初始值和当前成员。当前成员可以用我们以前讨论过的符号,当前初始值要再找个符号,我们用两个来表示。

这样,前面那些聚合运算就都可以用这个迭代函数来表示:

 A.SUM() = A.iterate( ~~+~, 0 )
 
 A.COUNT() = A.iterate( ~+if(~~,1,0),0 )
 
 A.MAX() = A.iterate( if(~>~~,~,~~), -inf )
 
 A.MIN() = A.iterate( if( ~<~~,~,~~), inf )

连乘当然也很简单了:A.iterate(~~*~, 1)

我们提到过的非常规聚合也可以,比如返回单值的 maxp 可以写成

 A.maxp(F) = A.iterate( if (~==null || ~.F>>~~.F,~,~~), null )       这是F表示字段名

但返回集合的情况要麻烦一些:

 A.maxp(F) = A.iterate( if(~==null || ~.F>~~.F,~,if(~.F==~~.F,~~|~,~~)), null )       |表示集合的并运算
 
 A.top( n ) = A.iterate( merge(~~,~).to(n), \[inf\]*n )        merge函数指归并排序运算,to(n)选出集合的前n个成员
 
 A.distinct() = A.iterate( if(~~.contain(~),~,~~|~), \[\] )         contain函数判断集合是否包含某成员

不过,不是所有的聚合运算都容易用 iterate 来描述,毕竟聚合运算的定义太宽泛了,比如中位数就不合适用 itertate 描述。另外,象 first/last 这种聚合运算不需要遍历,也没必要用 iterate 描述。

迭代聚合语法不仅能够帮助我们写出一些新的聚合运算,而且它本身就是一种遍历方法。能用迭代语法写出来的聚合运算,都可以一边遍历一边计算,遍历完了就得到聚合值,目标集合只需要遍历一次。

我们知道,计算机不能直接针对外存计算,当数据量很大而不能全部加载进内存时,迭代聚合算法可以只需要较小的内存(能够放下聚合值)就可以完成大数据量的聚合。这对于实现分组后的聚合运算很有意义。

分组子集的总体数据规模和原集是一样大的,基于拆分后的子集再做聚合运算就意味着要遍历两次,对于纯内存运算还不是大问题,但如果数据量大到内存放不下时,就会发生外存倒换的现象,这样效率是非常低的。iterate 作为一种聚合运算当然可以用在分组之后,分组后进行能够被 iterate 描述的聚合运算时,就不需要对原数据遍历两次了,可以一边分组一边聚合,只需要较小的内存(能保存下结果集)就可以完成。

这大概也是 SQL 没有提供分组子集的原因之一,在发明 SQL 的那个年代内存很小,绝大多数原始数据都不可能放入内存,先产生分组子集后再聚合的运算效率难以容忍,而分组后直接聚合,且都是可以用 iterate 描述的聚合,虽然仍然可能发生外存倒换(结果集也装不下内存时)的现象,但问题的严重程度要小很多。