【性能优化】4.1 [遍历技术] 游标过滤
4.1 游标过滤
使用索引或利用有序可以高速查找记录,但建立和维护索引以及保持数据有序的成本并不低,我们不可能预先为所有查询条件建立索引,必须时还要使用顺序查找,也就是遍历。
遍历查找外存数据表,容易想到的办法就是一条条读出数据产生记录,再针对记录计算查找条件是否成立,以决定保留或丢弃这条记录。这种简单的办法会产生一些浪费动作,比如我们要查找 18 岁以上的人员的姓名、性别、年龄、住址等信息,如果先将这些字段都读出并产生了人员记录之后,再判断该人员的年龄是否是 18 岁以上,碰到 18 岁以下的人员记录时将要被丢弃,这时读取字段和产生记录的动作都是无效的。如果我们可以先读出年龄字段就立即判断条件是否成立,成立时才读取其它字段并产生记录,不成立则直接跳过,不再读取其它字段并产生这条记录,这样就能省去很多动作,特别是不满足条件的记录较多时会更明显。
SPL 为组表实现了这种机制,创建组表上的游标时可以附加一个过滤条件,SPL 会先只读出用于计算条件的字段值,如果条件不成立就放弃到下一步,条件成立才再继续读出其它需要的字段并创建这条记录。
在已创建过的游标上使用 select 函数起不到这个效果,SPL 不知道这个游标的数据来源,不能把过滤条件施加到游标创建记录之前,只能 fetch 出记录再去判断了。
A |
|
1 |
=file("persons.ctx").open() |
2 |
=A1.cursor(sex,age;age>=18).groups(sex;avg(age)) |
3 |
=A1.cursor(sex,age).select(age>=18).groups(sex;avg(age)) |
A2 和 A3 的计算结果相同,但 A2 使用了上述游标前过滤技术,性能会好得多。
SPL 没有为文本文件和集文件实现这种优化。
有时过滤条件可能会有多个,比如要查出 18 岁以上的女性人员,即过滤条件为
age>=18 && sex==“Female”
因为 && 运算有交换律,这两个条件的书写次序并不影响运算结果,但却会影响运算性能。我们要考虑哪个条件更容易为 false,把它放在前面。因为,如果前半条件计算出 false,后面的条件就不用再算了,整个逻辑表达式一定为 false,这条记录就可以直接跳过了,用于计算后半条件的字段也不必读出了。
比如我们大体知道这份数据中不到一半人是女性,而大部分人都超过 18 岁,那么女性的条件就更容易算出 false,这时就要把它写成
sex==“Female” && age>=18
就会比刚才的写法计算性能更好。
对于 || 相关的条件则相反,要把容易算出 true 的部分写到前面,已经算出 true 了,后半部分就不再计算了。
遍历时的数据量常常很大,过滤条件也就可能会计算很多次,那么它的计算速度就非常重要了,在写代码时要尽量选择让它计算速度快的语法。
比如刚才这个例子,结合我们前面讲过的知识,sex=="Female" 是个字符串运算,它的运算速度赶不上 age>=18。如果这两个条件算出 false 的可能性差不多或者无法判断,那么我们应该把计算速度的部分放在前面,尽早地让条件能计算出确定的结果。
当然,更好的办法是使用前面说过的数据类型转换方法,把 sex 字段改造成小整数,用类似 sex==1 这种整数运算来判断。
另外,尽量不要把一些不必要的计算式放到过滤条件,而应该尽量事先计算好。
A |
|
1 |
=file("persons.ctx").open() |
2 |
=A1.cursor(;year(now())-year(birthday)>=18) |
A2 中的过滤条件就写得不好,now()是个即时信息,不能事先计算好,year(now()) 就要每次都临时计算,性能就会比较差。如果写成:
A |
|
1 |
=file("persons.ctx").open() |
2 |
=year(now()) |
3 |
=A1.cursor(;A2-year(birthday)>=18) |
就会好得多,year(now()) 只要计算一次了,更好的写法是:
A |
|
1 |
=file("persons.ctx").open() |
2 |
=year(now())-18 |
3 |
=A1.cursor(;year(birthday)<=A2) |
减法也可以省去了。再使用上一章讲过的转换数据类型,把 birthday 改成小整数,还会有更好的性能提升。
有时候要判断某字段值是否在某个集合中,比如指定一个名字列表,找出这些姓名的人。
A |
|
1 |
=file("persons.ctx").open() |
2 |
[John,Alice,Mary,Steven,…] |
3 |
=A1.cursor(;A2.contain(name)) |
contain 通常会用顺序查找方法,性能不太好,如果这个列表有点长(10 个以及上),则可以使用二分法来提高速度。
A |
|
1 |
=file("persons.ctx").open() |
2 |
[John,Alice,Mary,Steven,…] |
3 |
=A2.sort() |
4 |
=A1.cursor(;A3.contain@b(name)) |
这样的遍历速度就会快得多了。
在第八章我们还会再讲一种更高效的方法用于在遍历中进行这种集合从属判断。