SPL 高性能实践的套路

SPL 超越 SQL 性能的关键点

在性能优化案例中,SPL 常常能跑出比 SQL 高出数量级的性能。对此,我们的解释是 SPL 能写出 SQL 写不出来的低复杂度算法,计算量更小,性能自然会好;而 SQL 则只能指望数据库的优化器,这在复杂场景时并不管用。
抽象地说这个逻辑没有问题,但我们还想知道更具体的技术原因,

根本原因在于我们说过多次的离散性
业务需求并不只有针对整体集合的运算。本来,离散的个体和整体的集合可以任意组合拆分并混合运算,很多计算过程也不需要特别设计,按需求自然写出就可以了。但 SQL 缺乏离散性,难以直接描述业务逻辑,只能转换思路写出既复杂又非常“绕”的代码。从字面上很难理解其真实目标,这也是把优化器搞“晕”的关键原因。
我们在讨论离散性时已经举过很多例了,涉及有序运算、定位运算及序号利用、分组和聚合等等,这里就不再赘述了(离散性讨论)。
从这个意义上讲,SQL 描述的算法逻辑其实是“错”的,所以 SPL 也没办法把 SQL 翻译过来还能保证高性能,高性能是因为算法而不是语法。同样的算法逻辑用 SPL 写和 SQL 写在性能上并不会有什么本质区别,而 SPL 的优化能力要弱于传统数据库,结果只会跑得更慢。

有了离散性后,SPL 可以重新定义 JOIN,特别强调主键的参与,并把 JOIN 分成外键关联和主键关联两大类,分别适应不同的应用场景。分情况讨论相当于加强条件,从而获得更多可利用的特征来优化性能。
而 SQL 对 JOIN 的定义过于简单宽泛,看起来能覆盖一切,但也几乎没什么可以利用的特性,结果成为 SQL 性能优化的老大难问题。
这些我们也在 JOIN 的讨论中提到过( JOIN 系列 )。

强化离散性后,SPL 再提供有序存储机制,在保证大数据在存储和读取时的次序的基础上实现了有序遍历方法,能够有效地避免 SQL 中最头痛的大表关联和大结果集分组问题。
而 SQL 基于无序集合设计,不能保证存储的有序性,即使刻意有序存储了也不会利用。
这在讨论 基于对象 - 事件模式的数据计算问题 时也仔细分析过。

SQL 这些问题与其语法风格无关,而是其理论基础关系代数的问题,只要语法上改进增加一些关键字或函数并不能解决问题,要从关系代数上革新。但真要进行这个层面的改进后,那很难说是不是应该称为关系代数和 SQL 了?
SPL 的本质优势也不是它的语法,而是其代数基础,即离散数据集。事实上我们完全可以把 SPL 语法设计成像 SQL 这种类英语的样子。
高性能不只是靠优秀的代码,更关键的是创新的代数。

SPL 高性能实践的套路

SPL 提供的性能优化手段很多,可以足够出一本书的,这会给人一种非常复杂的感觉,碰到问题也不知道从哪里入手。
其实,就像我们之前讨论 SPL 比 SQL 更难了还是更容易? 时的说法,SPL 的高性能还是有套路的,常用的招术也没有太多。事实上,上面关键点分析中就已经指出了大致范围。

1 有序存储

如果数据量比较大,那么首先要做的事是设计存储,高性能和存储方案强相关。
选择行存或列存比较容易(查找用行存、遍历用列存),分文件方案(相当于数据库分表)在了解数据规模和产生频率后也不难设计,这里更关键的事情是想清楚数据应该对哪些字段有序。
有序存储及其上的有序遍历技术是 SPL 的一大特色,它可以有效地避免高复杂度的大表关联和大结果集分组,注意这里说的是避免,不是解决,采用有序存储和有序遍历后,业务逻辑更容易用自然思维写出来,没必要再“绕”,原来在 SQL 中的大结果集分组(以及退化的 COUNT DISTINCT)和很多大表关联(特别是自关联)就没有了。
很多计算困难的大数据都是与某个对象 ID 相关的事件记录,数据要按这个 ID 有序,而不是保持原始的时间序。熟悉算法的程序员都不难理解,数据如果针对 ID 有序,原本头痛的 COUNT DISTINCT ID 会变得多么容易。
当然排序是个慢动作,但这是一次性的。SPL 还提供了增量排序机制,可以把排序的巨大计算量分散到每一次数据维护过程中,基本感觉不到有性能损失。而一旦数据有序后,之后的运算就能获得数量级的性能提升。保持数据有序相当于某种适应面非常广的预计算,而预计算的成本又被分散到不起眼的日常数据维护中
阅读 基于对象 - 事件模式的数据计算问题 可以更清晰地感受有序对这类计算任务的意义,以及该如何确定有序字段。

2 区分关联

SPL 和 SQL 对 JOIN 的理解相差很大。SPL 中,等值 JOIN 总有主键参与,分为外键关联和主键关联两大类,被认为是两种完全不同的运算,会分别采用不同的优化手段。外键关联的本质是查找,而主键关联的本质是归并,确实不是一回事。
在用 SPL 实现关联时,一定先要准确的区分类型,以前其中涉及的主键(可能是逻辑的),然后再来运用相应的优化算法。
比如大表和小表 JOIN,如果是大事实表和小维表, 那就是小表内存化后来遍历大表做关联(也是 SQL 常用的手段),而如果是大维表和小事实表(经常是过滤后变小的事实表),就要采用查找技术来做,以避免遍历大维表。这两者是完全不同的方法。
再比如,多个表关联时,通常是一个事实表和多个小维表(可能多层)的关系,就可以把小维表内存化并做好预关联再来遍历大事实表,一次遍历解析掉所有关联。
有些外键关联并不是那么明显,但实际上还用被遍历集合(事实表)的键值在某个集合(维表)查找,甚至更广泛一点,找出多条数据也算。事实上,非等值 JOIN 也可以分为分别对应查找和归并技术的两种类型,只是不再涉及主键了,查找和归并的手段会有所有不同。

3 整数化和序号化

数据库常常会把可枚举的字符串优化成整数来处理,比如地区、性别这些都用整数表示,无论存储和运算都能获得更好的性能。这也是 SPL 提倡的方法。
日期也可以整数化。日期类型中的计算中会有较多的判断,效率不高。如果转化成某种能用加减乘除计算的整数就会获得更好的性能,在性能优化中都有介绍。
除了常规的整数化,SPL 还特别强调序号化,就是在整数化的时候尽量使用从 1 开始的自然数(或很容易计算成这样的数)。SPL 提供了大量和序号有相关的运算方法,比如基本的序号取值、以及更高级的序号分组和序号关联、以及更进一步的对位序列等,这些都是 SQL 中没有的。使用序号访问数据的计算复杂度是最低的,充分利用序号特征能减少相当多的计算量。

4 集合思维

在离散性的支持下,SPL 有更强大的集合运算能力。
SPL 写出的运算过程中经常出现多层集合(集合的成员也是集合),比如分组的结果就是个两层集合,分组上的 TopN 计算也是集合的集合。使用多层集合对于描述业务逻辑非常方便,但其它程序实践过程中一般不常有这种情况(虽然语言本身可能都支持),程序员刚接触时会有些不习惯,道理都能明白,但不熟悉时很可能用错,需要一些时间来练习。
SPL 中不仅有 SQL 中的数据表对象(序表),还特别提倡使用无结构的单列数据(序列, 也就是数组),序列对象更简单,运算也就更快,实现业务逻辑时要习惯于考虑是不是序列。
SPL 的 Lambda 语法非常强大,绝大多数集合计算都可以使用循环函数实现,而不要使用 for 语句(除非层次太多看起来太乱的情况),循环函数的代码简短性和运算性能都好于 for 语句。
这些集合思维习惯一旦建立,代码可以写得既优雅又高效。

5 调试与优化

剩下主要是一些细碎的性能优化技巧,在开始写算法时未必能也不需要都想到,可以编码测试后再来优化。
SPL 调试时会自动记录每个代码格的执行时间,也容易在主体代码旁边写上辅助代码记录一段代码的执行时间,通过这些信息就可以定位性能差的环节。
比如一些常见的技巧:
* 对大数据的过滤尽量放在游标上,有些外键关联的目标就是过滤,fjoin 函数提供了丰富的参数对付这种问题;主键关联的表中很可能其中一个被过滤后,另一个就可以跟随定位,pjoin 函数可以处理这种情况;
* 避免复杂的字符串运算,有些 like 能用高速的 pos 替代;
* json 风格数据类型比较复杂,处理速度慢,必要时可以冗余数据列;
* 针对同一个集合的多次或批量查找(非常规的外键关联就是这种情况),可以先将集合排序后用二分法或者建内存索引;但只查找一次时排序和索引都没有意义;
* 游标数据尽量不要落地(存出临时文件),尝试采用程序游标;
如前所述,程序员并不需要都掌握这些技巧(事实上这里列举的也不全面),可以在测试后针对性能不好的环节再来查阅相应资料(性能优化相关专题)来研究对策,过程中也就熟悉了这些技巧。
需要指出的是,SPL 经常为同一功能提供了多种方法以适应不同的场景,同样目标的代码可以尝试不同写法,比如 pjoin 可以用 T.new/news 替代,有时还使用附表存储会更有性能优势。
大数据的运算常常是针对游标的,但很多游标上的计算是延迟的,记录出来的执行时间会不准,而且游标数据也不容易查看,不方便调试。这时候可以使用相应的内存数据表来调试,SPL 刻意把针对游标的运算和针对序表的运算的语法设计得基本一致,这样只要把开始产生游标的代码改成产生序表就可以调试了,完成完成后再改回去。
另外,SPL 现在工作于 JVM 下,Java 在内存不足时会频繁启动 GC 动作,会导致测试出来的性能非常不稳定,所以在调试时还要刻意观察 GC 的情况。

最后还要再强调两点。
这写了 5 步,优化工作大体也是按这个次序进行,但并不一定能一轮做完,在调试时发现新问题后有可能会回头去修改存储设计,整个过程可能有多次局部反复才能完成性能优化,特别是对于新手。
套路不能解决一切问题,就像解数学题,虽然我们在学校都会学习很多方法,但并不存在一种能解决一切数学问题的方法论,不过学会套路还是能解决相当多问题的,仍然是很有意义的。