【性能优化】4.2 [遍历技术] 遍历复用

 

【性能优化】4.1 [遍历技术] 游标过滤

4.2 遍历复用

我们知道,外存数据表的遍历运算中读取时间占比非常大,但读取又不可以避免,所以我们会希望一次读取能做尽量多的事情,也就是尽量做到能复用遍历过程中读出来的数据。

比如我们对订单做分组统计,希望按产品统计销售额,再找出每个地区的最大订单额。这样两个分组键不同的任务是不能写到一个分组运算中的。简单的写法会是这样:


A

1

=file("orders.ctx").open()

2

=A1.cursor(product,amount).groups(product;sum(amount))

3

=A1.cursor(area,amount).groups(area;max(amount))

这样计算会把数据表遍历两次,amount 字段被重复读取了(假定是列存,行存则重复读取的内容更多)。

其实我们使用一点技巧就可以在一次遍历中把两个分组结果都计算出来。


A

1

=file("orders.ctx").open()

2

=A1.cursor(area,product,amount).groups(area,product;sum(amount):samount,max(amount):mamount)

3

=A2.groups(product;sum(samount))

4

=A2.groups(area;max(mamount))

这样做,CPU 的计算量会多一些,占用内存也会更大,因为要计算和保持一个更细致的分组结果集,但是遍历量少很多,通常还是能获得更好的运算性能。不过,如果我们还要对更多字段做不同的分组统计,代码就会麻烦得多;要是再对这个游标做一些不是简单分组的运算,比如先过滤后再分组,那几乎就不可能用这种技巧写出来了。

在数据库中使用 SQL 就会面临这种窘境。

SPL 提供了遍历复用技术来解决这类问题,上面的运算可以这样写:


A

1

=file("orders.ctx").open()

2

=A1.cursor(product,area,amount)

3

=channel(A2).groups(area;max(amount))

4

=A2.groups(product;sum(amount))

5

=A3.result()

A3 用 channel 函数定义一个与游标 A2 同步的管道,并在其上设置了某种运算(也是一个分组)。当 A4 中遍历游标计算分组时,读出的数据会同时送给这个管道 A3 去设置的运算(那个分组),遍历结束后,管道中会保持相应的计算结果,在 A5 取出来即可。

这段代码的 CPU 运算量和前面那个两次遍历的代码是一样的,都只要做两个小分组。但是,它的硬盘读取量要小很多,amount 字段只被读了一次。

一个游标可以定义多个同步管道,同时附加多套运算。而且这种运算可以随意写,不仅限于分组,可以是任意的,也可以多个步骤。比如 A3 可以写成:

=channel(A2).select(amount>=50).groups(area;max(amount))

就会对金额在 50 以上的订单进行统计。

这种机制对于所有游标都有效,并不只限于组表。

SPL 还提供语句式的管道写法,这样代码显得更整齐一点:


A

B

1

=file("orders.ctx").open()

2

=A1.cursor(product,area,amount)

3

cursor A2

=A3.groups(area;max(amount))

4

cursor

=A4.groups(product;sum(amount))

5

cursor

=A5.select(amount>=50).total(count(1))

6



游标创建后,用 cursor 语句为其创建管道,然后在其上设置运算。可以创建多个管道,后面语句不再写游标参数则表示会复用同一个游标,所有的 cursor 语句(的代码块)写完后,SPL 即认为整套管道定义结束,就会开始遍历游标,将每个管道的运算结果计算出来存放在 cursor 语句所在的格中。

这里 B3 和 B4 分别定义前面的运算目标,B5 中增加了计算金额在 50 以上的订单数量,这些计算结果会分别放在 A3、A4、A5 中(注意不是 B3、B4、B5)。

遍历复用思路还可以应用在数据拆分上。比如有个大文本文件,我们希望从中挑出合规的数据(满足给定条件)进一步做分析,这对游标使用 select 函数就可以了。同时,我们可能也想知道有哪些不合规的数据(不满足条件)以便防止这种情况的再发生。但一次过滤原则不能同时把满足和不满足条件的记录分开,这时候就可以使用遍历复用技术了。


A

1

=file("data.txt").cursor@t()

2

3

=channel(A1).select(!(${A2})).fetch()

4

=A1.select(${A2})

5

>file("result.btx").export@b(A4

6

=A3.result()

在 A2 中填入过滤条件,A3 的管道将过滤出不满足条件的记录并取出,A4 的游标过滤出满足条件的记录,A5 遍历游标把满足条件的记录写入新文件,A6 则可以管道结果取出来了,这里我们假定不满足条件的记录很少,能够在内存中放下。

不过,这样做还是需要把这个条件计算两次(满足和不满足要分别计算)。SPL 在 select 函数直接提供了这种方法,可以把不满足条件的记录同时取出来,不过只能写入另一个文件,也只能集文件的格式。


A

1

=file("data.txt").cursor@t()

2

3

=A1.select(${A2};file("error.btx"))

4

>file("result.btx").export@b(A3)

类似地,还有可能把大数据表拆分成多个组,比如把订单记录按地区拆分成多个文件以便分发处理,也可以使用管道。不过,管道的数量是在代码中事先确定的,要事先知道有分成多少份,而不能在分组过程中临时产生新的管道。

对于这种情况,SPL 在游标的序号分组同时附加了拆分写入文件的功能,比如下面的代码将把订单分到 12 个月(月份容易序号化方便举例,其它情况读者可以自己做序号化)。


A

1

=file("orders.txt").cursor@t()

2

=12.(file("order"/~/".btx"))

3

=A1.groupn(month(dt);A2)

4

=A1.skip()

这里的 groupn 函数是个延迟游标,只是记录一下动作,在游标遍历过程中才会真地计算。事先要准备好相应数量的文件对象(A2)。和 select 类似,groupn 也只能写出集文件。

【性能优化】4.3 [遍历技术] 并行遍历

【性能优化】 前言及目录