外键预关联(JOIN 简化和提速系列 6)

我们再来研究如何利用 JOIN 的特征实现性能优化,这些内容的细节较多,我们挑一些易于理解的情况来举例,更完善的连接提速算法可以参考乾学院上的《性能优化》图书及课程。

六、外键预关联

先看全内存下外键关联的情况。
设有两个表:

customer	客户信息表
    id		编号
    name	名称
    city	城市
    ...
orders		订单表
    seq		序号
    date	日期
    cid		客户编号
    amount	金额
    ...

其中 customer 表的主键是 id,orders 表中的 cid 是指向 customer 表的外键。
现在我们想统计各个城市的订单总额(为简化讨论,就不再设定条件了),用 SQL 写出来:

SELECT customer.city, SUM(orders.amount) FROM orders
JOIN customer ON orders.cid=customer.id
GROUP BY customer.city

数据库一般会使用 HASH JOIN 算法,需要分别两个表中关联键的 HASH 值并比对。

我们用前述的简化的 JOIN 语法写出这个运算:

SELECT cid.city, SUM(amount) FROM orders GROUP BY cid.city

这个写法其实也就预示了它还可以有更好的优化方案,下面来看看怎样实现。

如果所有数据都能够装入内存,我们可以实现外键地址化。
将事实表 orders 中的外键字段 cid,转换成维表 customer 中关联记录的地址,即 orders 表的 cid 的取值已经是某个 customer 表中的记录,那么就可以直接引用记录的字段进行计算了。
用 SQL 无法描述这个运算的细节过程,我们使用 SPL 来描述、并用文件作为数据源来说明计算过程:


A

1

=file("customer.btx").import@b()

2

>A1.keys@i(id)

3

=file("orders.btx").import@b()

4

>A3.switch(cid,A1)

5

=A3.groups(cid.city;sum(amount))

A1 读出客户表,A2 为客户表设置主键并建立索引。A3 读出订单表,A4 的动作是将 A3 的外键字段 cid 转换成对应的 A1 的记录,执行完后,订单表字段 cid 将变成客户表的某条记录。A2 建立索引能让 switch 更快,因为通常事实表远大于维表,这个索引能被复用很多次。A5 就可以执行分组汇总了,遍历订单表时,由于 cid 字段取值现在已经是一条记录,那么可以直接用. 操作符引用其字段了,cid.city 就可以正常执行。
完成 A4 中的 switch 动作之后,内存中事实表 A3 的 cid 字段存储内容已经是维表 A1 的某条记录的地址,这个动作即称为外键地址化。这时候引用维表字段时,可以直接取出,而不需要再用外键值在 A1 中查找,相当于在常数时间内就能取到维表的字段,避免了 HASH 值计算和比对。

A2 建索引一般也会用 HASH 办法,对 id 计算 HASH 值,A4 转换地址时也是计算 cid 的 HASH 值与 A2 的 HASH 索引表对比。如果只做一次关联运算,地址化的方案和传统 HASH JOIN 方案的计算量基本上一样,没有根本优势。
但不同的是,如果数据能在内存中放下,这个地址一旦转换之后可以复用,也就是说 A1 到 A4 只要做一次,下次再做关于这两个字段的关联运算时就不必再计算 HASH 值和比对了,性能就能大幅提高。
能够这样做,正是利用了前面说过的外键关联在维表这一方具有的唯一性,一个外键字段值只会唯一对应一条维表记录,可以把每个 cid 转换成它唯一对应的那条 A1 的记录。
而延用 SQL 中对 JOIN 的定义,就不能假定外键指向记录的唯一性,无法使用这种表示法。而且 SQL 也没有记录地址这种数据类型,结果会导致每次关联时都要计算 HASH 值并比对。

而且,如果事实表中有多个外键分别指向多个维表,传统的 HASH JOIN 方案每次只能解析掉一个,有 N 个 JOIN 要执行 N 遍动作,每次关联后都需要保持中间结果供下一轮使用,计算过程复杂得多,数据也会被遍历多次。而外键地址化方案在面对多个外键时,只要对事实表遍历一次, 没有中间结果,计算过程要清晰很多。
还有一点,内存本来是很适合并行计算的,但 HASH JOIN 算法却不容易并行。即使把数据分段并行计算 HASH 值,但要把相同 HASH 值的记录归聚到一起供下一轮比对,还会发生共享资源抢占的事情,这将牺牲很多并行计算的优势。而外键式 JOIN 模型下,关联两表的地位不对等,明确区分出维表和事实表后,只要简单地将事实表分段就可以并行计算。

将 HASH JOIN 算法参照外键地址化方案进行改造后,也能一定程度地改善多外键一次解析和并行能力,有些数据库能在工程层面上实施这种优化,不过通常只在只有两个表 JOIN 的情况时有效,在有很多表及各种 JOIN 混在一起时,数据库并不容易识别出应当把哪个表当作事实表去并行遍历、而把其它表当作维表建立 HASH 索引,优化并不总是有效的。所以我们经常会发现当 JOIN 的表变多时性能会急剧下降的现象(常常到四五个表时就会发生,数据量并无显著增大)。而从 JOIN 模型上引入外键概念后,将这种 JOIN 专门处理时,就总能分清事实表和维表,更多的 JOIN 表只会导致性能的线性下降。
内存数据库是当前比较火热的技术,但上述分析表明,采用 SQL 模型的内存数据库在 JOIN 运算上是并不容易跑得快!