遍历复用 | 润乾 -九游会登陆

发布时间:2018-07-17 分类:数据蒋堂 tag:

sjjt-217

减少外存(硬盘)访问量一直是提高大数据计算性能的永恒话题,我们也讨论过列存、压缩等直接减少访问量甚至存储量的手段。除了这些存储层面的方法外,在算法和计算实现环节,也可以想办法减少外存的访问量。

遍历是大数据计算中必不可少的环节。有时候,我们会发现在一个计算任务中,会有两次(或更多)涉及针对同一批数据的遍历动作。如果我们能有办法让两次遍历合并成一次,那么总的计算量(cput的动作)并没有差别,但硬盘的访问量会减少了一半,这样计算性能还是能得到提升,对于数据密集型计算的提升效果还相当明显。


设有简化的帐目表t的数据结构中如下字段:账号a、日期d、发生地p,金额m

现在我们想统计账号a1和a2的余额,用sql写出来是这样:

select sum(m) from t where a=a1

select sum(m) from t where a=a2

这样两句计算就会导致遍历两次表t,如果表t非常大,计算效率就很低了。

如果我们把这句sql写成这样:

select sum(case when a=a1 then m else 0 end),

sum(case when a=a2 then m else 0 end) from t

一个语句把这两个统计值都计算出来,句子复杂了不少,数据库的总计算量也反而略有变大(判断次数相同,累计次数变多,要多加很多次0),但是表t却只要遍历一次就可以了,最后获得的运算效率却要高很多。

作为数据库程序员,要学会这种技巧。


不过,并不是所有运算都可以用case when来对付。

我们想分别统计每天的金额合计和每个发生地的金额合计,写出sql是:

select d,sum(m) from t group by d

select p,sum(m) from t group by p

sql没有直接提供遍历复用的语法,不同的where还可以用case when去绕,但不同的group by就无法再合并起来了,只能遍历两次表t。

理论上,使用数据库游标可以做到这一点,定义一个基于select d,p,m from t的游标,一行行取数,然后分别针对d和p去做group by运算。这个运算用sql写起来实在太麻烦了,而且游标遍历的性能很差,结果不仅繁琐而且更慢了。


sql的体系下解决不了这个问题了,我们需要设计新的概念和语法来实现遍历复用。

在游标机制中引入管道的概念。游标遍历数据实施某个运算的同时,将数据压入到一个管道中,而管道上可以再定义另一个运算,这样,数据在一次遍历时可以同时获得游标本身以及附加的管道上的两个运算结果。上面的的运算写出来的大体代码结构如下:

cs = t.cursor()
ch = channel(cs).groups( p; sum(m) )
dg = cs.groups( d; sum(m) )
pg = ch.result()

channel(cs)在游标cs上绑定一个管道ch,并且定义一个针对p的分组运算,然后游标cs照常遍历并实施针对d的分组运算,遍历完毕后,从管道ch中取了相关结果就可以了。


前面那个不同条件汇总的问题当然也可以用游标和管道机制写出来

cs = t.cursor()
ch = channel(cs).select( a==a2 ).sum(m))
m1 = cs.select( a==a1 ).sum(m)
m2 = ch.result()

代码结构都是一样的。


当然,一个游标上还可以附加多个管道,比如刚才这两件事(条件汇总和不同分组)也可以一次遍历做完:

cs = t.cursor()
ch1 = channel(cs).select( a==a2 ).sum(m))
ch2 = channel(cs).groups( p; sum(m) )
ch3 = channel(cs).groups( d; sum(m) )
m1 = cs.select( a==a1 ).sum(m)
m2 = ch1.result()
dg = ch2.result
pg = ch3.result()

再举一个计算中位数的例子。

计算中位数时需要排序,但一般情况下排序运算只管排序本身,并不管计数,排序完成了甚至还不知道总共有多少数据, 这时候要找中位数,就还得再做一次count遍历数据,浪费时间。如果有管道机制,我们就可以在排序的同时把计数也做完了。

cs = t.cursor()
ch = channel(cs).count()
s = cs.sortx(m) //遍历排序过程中把管道上的计数也完成
k = ch.result()
m = s.skip( (k-1)\2 ).fetch@x(2-k%2).avg(m) //找出中间一个或两个数
网站地图