【数据蒋堂】第 33 期:JOIN 提速 – 外键指针化

sjjt-33

我们再来看重新定义 JOIN 后如何能够提高运算性能,先看外键式 JOIN 的情况。

设有两个表:

products 商品信息表

id 商品编号
name 商品名称
price 单价

sales 商品销售记录

seq 序号
date 日期
productid 商品编号
quantity 销售数量

其中 sales 表中的 productid 是指向 products 表中 id 字段的外键,id 是 products 表的主键。

现在我们想计算销售额有多少(为简化讨论,就不再设定条件了),用 SQL 写出来:

 SELECT SUM(sales.quantity*products.price) FROM sales JOIN products ON sales.productid=products.id

基于笛卡尔积定义的 JOIN,原则上只能两层循环全遍历来计算,不过这个计算量实在太大,关系数据库一般采用 HASH 分段方法优化,即分别计算两表关联字段的 HASH 值,将 HASH 同值记录拼到一起再做小范围遍历。网上有很多文章介绍这个算法,这里就不详述了。这样做后的复杂度能显著降低,但仍然要做多次 HASH 值计算和比对。

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

 SELECT SUM(quantity*productid.price) FROM sales

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

我们先考虑全内存的情况,如果所有数据都能够装入内存,我们可以实现外键指针化。

将事实表 sales 中的外键字段 productid,转换成指向维表 products 记录的指针,即 productid 的取值就已经是某个 products 表中的记录,那么就可以直接引用记录的字段进行计算了。

用 SQL 不方便描述这个运算的细节过程了,我们采用过程式语法、并用文件作为数据源来说明计算过程:

1. P=file(“products.txt”).import()                       读入商品信息表 P
2. P.index(id)                                                          为 P 的主键 id 建立索引方便查找
3. S=file(“sales.txt”).import()                              读入商品销售记录 S

4. S.switch(productid,P:id)                                   将 S 中的 productid 字段根据 P 的主键转换成 P 的记录
5. S.sum(quantity*productid.price)                    计算销售额。productid 字段取值已经转换为对象,可以直接引用其 price 字段

上面算法中,第 2 步建主键索引一般也是用 HASH 办法,对 id 计算 HASH 值,第 4 步转换指针还是计算 productid 的 HASH 值与 P 的 HASH 索引表对比。这样的话,如果只做一次关联运算,指针化的方案和传统 HASH 分段方案的计算量基本上一样,没有根本优势。

但不同的是,如果数据能在内存中放下,这个指针一旦建立起来之后可以复用,也就是说第 2 和第 4 步只要做一次,下次再做关于这两个字段的关联运算时就不必再计算 HASH 值和比对了,性能就能大幅提高。而关系代数体系下没有对象指针这个概念,并且基于笛卡尔积定义的 JOIN 运算也无法假定外键指向记录的唯一性,没办法使用外键指针化的方法,每次关联时都要计算 HASH 值并比对。

而且,如果事实表中有多个外键分别指向多个维表,传统的 HASH 分段 JOIN 方案每次只能解析掉一个,有 N 个 JOIN 要执行 N 遍动作,每次关联后都需要保持中间结果供下一轮使用,计算过程复杂得多,数据也会被遍历多次。而外键指针化方案在面对多个外键时,只要对事实表遍历一次, 没有中间结果,计算过程要清晰很多。

还有一点,内存本来应当是很适合并行计算的,但 HASH 分段 JOIN 算法却不容易并行。即使把数据分段并行计算 HASH 值,但要把相同 HASH 值的记录归聚到一起供下一轮比对,就会发生共享资源冲突的事情,这会把并行计算的优势完全抵消掉。而外键式 JOIN 模型下,关联两表的地位不对等,明确区分出维表和事实表后,只要简单地将事实表分段就可以并行计算。

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

内存数据库是当前比较火热的技术,但上述分析表明,采用 SQL 模型的内存数据库在 JOIN 运算上是很难快起来的!