现在我们获取到了数据,让我们来关联它们吧。我会介绍3中常见你的关联方式:归并连接(Merge Join)、Hash连接(Hash Join)和嵌套循环连接(Nested Loop Join)。在这之前,我们需要介绍两个新概念:内关系和外关系,一个关系可能是:
- 一张表
- 一个索引
- 一个中间结果
当你连接两个关系时,每种连接算法管理关系会有所不同。在文章的其余部分,我假设:
- 外关系是指左边的数据集合
- 内关系是指右边的数据集合
例如:A连接B,A就是外关系,B就是内关系。
大部分时候,A连接B和B连接A的成本是不一样的。在这一章,我会假设外关系有N个元素,内关系有M个元素。记住,实际上通过统计信息中就可以知道N和M。
嵌套循环连接(Nested loop join)
嵌套循环连接是最简单的一种连接。下边是其连接原理:
- 遍历外关系的每一行;
- 遍历内关系中的每一行,并比较是否合当前外关系行匹配。
下面是伪代码:
nested_loop_join(array outer, array inner)
for each row a in outer
for each row b in inner
if (match_join_condition(a,b))
write_result_in_output(a,b)
end if
end for
end for
因为是一个两层遍历,所以其时间复杂度是O(N*M)。
从磁盘IO角度来看,外关系中N个元素每一次循环,都需要读取内关系中的M个元素,这样就需要从磁盘读取N+N*M次。如果内关系足够的小,就可以将内关系放入内存,这样实际只需要读取磁盘M+N次。上述情况,只有在内关系最小的情况下才可行,因为这样其更容易放入内存。
从时间复杂度来看,放入内存并无不同;但从磁盘IO会更好,因为其只需要读取一次内关系。当然,内关系也可以用索引来代替,这样其会有更好的磁盘IO性能。
这种算法非常简单,当内关系太大无法放入内存时会有一个变种,其相对会有更少的磁盘IO。下边是其思想:
- 无须一行一行的读取两个关系数据;
- 可以一组一组的读取,并且将其放入内存中;
- 比较两组数据得到匹配的行;
- 加载新的两组数据并比较;
- 知道所有数据比较完成。
下边时一段伪代码:
// improved version to reduce the disk I/O.
nested_loop_join_v2(file outer, file inner)
for each bunch ba in outer
// ba is now in memory
for each bunch bb in inner
// bb is now in memory
for each row a in ba
for each row b in bb
if (match_join_condition(a,b))
write_result_in_output(a,b)
end if
end for
end for
end for
end for
这个版本的算法时间复杂度没有任何变化,但是磁盘IO有所降低:
- 之前的版本需要访问磁盘N+M*N次;
- 这个新版本需要访问磁盘number_of_bunches_for(outer)+ number_of_ bunches_for(outer)* number_of_ bunches_for(inner)次;
- 如果增加每组的元素数量,会降低磁盘的访问次数。
虽然这个版本算法每次磁盘访问会获取更多的数据比之前的算法,但其实没关系,因为它是顺序读取(顺序读取的关键时间是耗在读取第一份数据时)。
译者注:其实是一种拿空间换时间的思路,这种思路很常见,比如Java中的TreadLocal实现。这样做有两个大前提:
1. 空间成本比时间成本更加廉价。想想我们的存储介质是不是在不断的降价呢?想想CPU的价格是不是远远高于磁盘呢?
2. 顺序读取每组数据。因为如果是随机读取每组数据中的元素,那么磁头仍然需要不断的调整磁道,虽然看起来是一次读取一组数据,实际仍然相当于读取多次。