编程语言
首页 > 编程语言> > 连接(Join)算法

连接(Join)算法

作者:互联网

为什么需要JOIN?

JOIN算法

我们将专注于使用 等值连接(inner equijoin) 等连接算法一次合并两个表。

通常,我们希望较小的表始终是查询计划中的左表(“out table”)。

JOIN操作的输出:数据

将外tuple和内tuple中的属性值复制到新的输出tuple中。

JOIN操作的输出:记录ID

只复制join keys和匹配tuple的记录ID。

它被称为延迟物化(late materialization)

I/O开销分析

只关注I/O的使用次数而不关心实际得到输出结果的开销。

JOIN vs CROSS-PRODUCT

JOIN算法

嵌套循环连接(Nested Loop Join)

归并排序连接(Sort-Merge Join)
哈希连接(Hash Join)

嵌套循环连接

# 算法描述的伪代码
foreach tuple r ∈ R:
	foreach tuple s ∈ S:
		emit, if r and s match

该算法的愚蠢之处在于对于外表R中的每一个tuple,都要去扫描一遍内表S。
Cost: M + m * N
其中M为outer table R中的page数,m是R的tuple数,N是S的page数。

[例]一个数据库:

开销分析

如果用更小的表S作为外表呢?

尽管快了一点,但还是很糟糕。

块嵌套循环连接

foreach block B_R ∈ R:
	foreach block B_S ∈ S:
		foreach tuple r ∈ B_R:
			foreach tuple s ∈ B_S:
				emit, if r and s match

将R表和S表分别读取到内存的两个block中,对于外表R的block中的每一个tuple,它们全部完成对S表的block中的每一个tuple的匹配、连接后,才重新读取S表的下一个block,当扫描过一遍S表后,重新读取R表的下一块block,重复前面的步骤。

该算法需要更少的磁盘访问:

[例]一个数据库:

开销:

这比之前的算法好很多了,但还是很糟糕。我们可以再进行一些改进,让时间减少到1秒内。

如果我们有B个缓冲块可以用呢?

foreach B-2 blocks b_R ∈ R:
	foreach b_S ∈ S:
		foreach tuple r ∈ b_R:
			foreach tuple s ∈ b_S:
				emit, if r and s match

该算法使用B-2个缓冲块来扫描R
开销:M + (⌈M / (B-2)⌉ * N)

如果外表可以全部放进内存呢(B > M + 2)?

为什么基本的嵌套循环连接很糟糕?

对于外表的每一个tuple,我们必须顺序扫描去检验它和内表的每一个匹配。
我们可以使用索引来查找匹配以避免顺序扫描

索引循环嵌套连接

foreach tuple r ∈ R:
	foreach tuple s ∈ Index(r_i = s_j):
		emit, if r and s match

假设对于每一个tuple,每次索引探测的开销是一个常数C
开销:M + (m * C)
对于外表里的每一个tuple,经过索引探测后可以知道待连接的目标tuple所在的page,只要将那个page读进内存即可。我们不考虑连续的几个外表tuple是否有待连接的内表tuple在同一个page中的情况,仅仅是重复地对每个外表tuple,将内表tuple所在的page读入内存,即使这个page可能已经在内存当中了。

小结

排序合并连接

阶段一:排序

sort R, S on join keys 
cursor_R <- R_sorted, cursor_S <- S_sorted
while cursor_R and cursor_S:
	if cursor_R > cursor_S:
		increment cursor_S
	if cursor_R < cursor_S:
		increment cursor_R
	elif cursor_R and cursor_S match:
		emit
		increment cursor_S

【例】
下面两张表,按照id进行连接,已排好序
在这里插入图片描述
游标开始位置
在这里插入图片描述
匹配上了,内表游标下移
在这里插入图片描述
再一次匹配上了,继续下移
在这里插入图片描述
没匹配上,外表游标下移
在这里插入图片描述
匹配上了,内表游标下移
在这里插入图片描述
没匹配上,外表游标下移
在这里插入图片描述
此时,外表游标指向的id小于内表游标指向的id,但却仍是200,应该与S表中id为200的tuple进行匹配,如果外表游标继续往下走,将会错过这些匹配,内表游标必须回溯到上一个id值的开始位置,因此我们需要维护这个信息
在这里插入图片描述
回溯,匹配后,内表游标下移
在这里插入图片描述
不匹配,外表游标下移
在这里插入图片描述
继续下移
在这里插入图片描述
匹配上了,内表游标下移
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这里的要点还是在于,后面会不会还有id为500的tuple,如果有的话,内表游标还需要回溯
在这里插入图片描述
在这里插入图片描述

开销

排序开销(R):2M * (log M / log B)
排序开销(S):2N * (log N / log B)
归并开销:(M + N)

总开销:排序 + 归并

[例]一个数据库:

在有100个缓冲页(本文中提到的块、页、block、page含义相同)的情况下,RS的排序均需要两躺:

排序合并连接的最坏情况

合并阶段最坏的情况是两个关系中所有元组的连接属性包含相同的值。比如S表的id全部为1,而我们还去对S表按id排序。

开销:(M * N) + (排序开销)

什么时候排序合并连接有用?

哈希连接

如果元组r ∈ Rs ∈ S满足连接条件,那么它们含有相同的连接属性值。
如果那个值被散列到一个部分 i,那么R的元组必然在 r i r_i ri​ 中并且S的元组必然在 s i s_i si​ 中。
因此,R r i r_i ri​ 中的tuples只需要与S s i s_i si​ 中的tuples比较。

两个阶段

阶段一:构建

阶段二:探测

build hash table HT_R for R
foreach tuple s ∈ S
	output, if h_1(s) ∈ HT_R

Key: 查询连接表的属性
Value:因实现而异,取决于查询计划(查询树)中连接运算符上面的运算符期望得到的输入

哈希表存储的值

方法一:完整的Tuple

方法二:Tuple ID

布隆过滤器

用于集合成员资格查询(set membership query)的概率数据结构(bitmap)。

我们能使用布隆过滤器进行的操作:
Insert(x):

Lookup(x):

示例

如图,为一个布隆过滤器,由8位构成。
在这里插入图片描述


插入key,哈希函数数量k = 2
在这里插入图片描述


得到哈希值并进行模运算后,将相应位置1
在这里插入图片描述


类似的,再插入另一个key
在这里插入图片描述


查询key,计算哈希值,检索过滤器上相应的位,此时得到的位不全为1,结果表明这个key不在哈希表中,因为无假阴性,所以结果可信
在这里插入图片描述


再查询另一个key,这一次我们得到的位都是1,结果表明该key存在于哈希表中,但事实上,这两个位上的1分别来自‘RZA’和‘GZA’,这样的结果就是假阳性的,因此这个结果不可信
在这里插入图片描述

探测阶段的优化

当key可能不在哈希表中时,可以在构建阶段创建布隆过滤器。

在连接操作上执行布隆过滤,内表的元组只有在连接属性经散列后得到的每一位都是1才去哈希表中进行实际的匹配。
在这里插入图片描述


布隆过滤器在分布式数据库中十分有用,例如,当A、B分别为两台服务器或数据中心时,要执行连接操作,与其通过发送网络消息来执行探测阶段,不如把大小为几kb的布隆过滤器发送给其他机器,这样一来,在发送网络消息前,可以先在本地进行过滤。
之所以称它为横向消息传递,是因为它把查询操作执行的模型给拆分了,原本每个子节点通过单独的信道向父节点传递数据,不在兄弟节点间传递数据,但现在打破了这个模型。尽管违反了规则,但对我们却大有裨益。

标签:开销,Join,内表,tuple,游标,算法,哈希,连接
来源: https://blog.csdn.net/Ingsuifon/article/details/109095175