如何在分布式数据库中实现 Hash Join ?

thire
发布于 2022-9-20 11:34
浏览
0收藏

Join 是关系数据库中非常重要的一种操作。数据库对于Join通常有三种主要的实现: Merge Join, Nested-loop Join, Hash Join。其中 Hash Join 适用于带有等值条件情况,由于 Hash Join 的算法复杂度在平均情况下是 O(n),通常在大规模数据做Hash Join是最优的选择。主流的关系数据库 (Oracle, SQL Server, PostgreSQL) 等都有 Hash Join 的实现。MySQL 主要应用于 oltp 场景,没有实现 Hash Join。

 

In-Memory Hash Join 


对于两张待 join 的表 t1, t2。选取其中的一张表按照 join 条件给的列建立hash 表。然后扫描另外一张表,一行一行去建好的 hash 表判断是否有对应相等的行来完成 join 操作,这个操作我们称之为 probe (探测)。前一张表我们叫做 build 表,后一张表我们的叫做 probe 表。

为了减少内存使用量,我们通常选择小表作为 hash 表,大表做为 probe 表。另外我们发现如果 hash 冲突比较严重的话,最极端的例子 hash 表所有的值都一样的话,那么单个 hash bucket (hash桶)内部的 Hash Join 就会退化成 Nested-Loop Join。


 Grace Hash Join 


当 build 表数据量比较大,无法在内存中全部存下的时候,我们需要利用磁盘来完成 join 操作。最直观的想法,就是分片加载 build 表。每加载一个 build 表的一个分片,然后从头到尾扫描一遍probe表,进行一次 probe 操作,完成 join 操作。这样做的问题是扫描磁盘的次数过多。总的扫描量是build表 + n *  probe 表。n 是 build 表的分片个数。

Grace Hash Join 的方法是: 先对两个即将做 Hash Join 的表按照同一个 hash 函数分配到不同的分片中。然后再分别对不同的分片做 In-Memory Hash Join。这是典型的分治的思想。如果单个分片还是不能全部放到内存中,可以迭代的使用 Grace Hash Join 方法。具体做法的伪代码如下:  

t1, t2 是待join两个表
PartCount是分片的个数def graceJoin(t1, t2):
    for row in t1:
        hashValue = hash_func(row)
        N = hashValue % PartCount;
        write row to file t1_N;

    for row in t2:
        hashValue = hash_func(row)
        N = hashValue % PartCount;
        write row to file t2_N;

    for i in range(0, PartSize):
        join(t1_i, t2_i)    


这里的 PartCount 选取很有讲究。不能太小,如果太小起不到分治效果,最后的那个 join 可能还需要做 Grace Hash Join。增加了读写磁盘的次数。比如极端情况下选取 PartCount = 1,那么就完全没有任何的效果了。那么是不是PartCount 选取越大越好呢?其实也不是这样的,主要是因为是磁盘是一个块设备,每次刷盘都需要刷一定大小的块 (block)才高效。如果 PartCount 设置的太大,会导致某些分片中包含的行数太少,达不到 block 的大小,刷盘不经济。所以笼统的说 PartCount 在保证刷盘经济上的情况下越大越好。这个需要优化器根据表的统计信息来确定。

这个算法有一个 Bad Case 。就是某个表的 row 经过 hash 以后都落入了同一个分片中,那么就起不到分治的效果。这个时候可以更换 hash 函数,但是如果本来 row 的值都是一样的,那么可以退化到直观的那种做法。其实如果发生这种情况的时候,应该在优化器选择 join 方法的时候就可能就不应该选择 Hash Join。例如可以选择 Nested-loop Join 会更好。


 Hybrid Hash Join 


Hybrid Hash Join 结合了 In-memory Hash Join 和 Grace Hash Join 的优点。Hybrid Hash Join 是 Grace Hash Join 的一种改进,在 Grace Hash Join 第一张表分片的过程中,尽量把越多的完整的分片保留在内存中。这样在第二张表做分片的过程中,就可以同时对留在内存中的分片做 probe 操作,这样省去了留在内存中分片的刷盘操作,同时第二张表对应的分片也不需要刷盘,提高效率。如果第一张表所有的分片都能留在内存中,其实就是 In-memory Hash Join。我们把第一张表叫做 build table,第一张表分片的过程叫做 build phase。第二张表叫做 probe table,第二张表分片的过程叫做 probe phase。那么我们详细介绍两个过程。

Build Phase 如下图所示,hash table 被分成了4个分片,图中每个分片用不同的颜色表示。build阶段读取build表中的数据,然后 hash 到各个 hash 桶里面。桶的数据存储在内存块 (block) 中,block 在图中使用正方形的小方块表示。 block 是一个刷盘的最小单位。当放 block 的内存快满的时候,就会把其中一些 block 刷到磁盘中,刷盘的时候会尽量保证某些分片完整的留在内存中,所以尽量把那些已经落到磁盘中的分片的 block 刷出去。在图中,红色和绿色的分片应该尽量保留在内存中,会优先刷蓝色和黄色的 block。  

如何在分布式数据库中实现 Hash Join ?-鸿蒙开发者社区

Probe Phase 如下图所示,在 probe 阶段,扫描probe表,然后在 hash table中找到对应的 hash 桶。如果 hash 到磁盘上没有数据的分片(红色,绿色)所在的hash桶,那么就可以直接做 join 返回结果。如果hash到在磁盘上有数据的分片(黄色,蓝色),那么就把 probe 表的行写内存中的对应分片的 block 中,当一个 block 被写满以后就刷到磁盘中,图中虚线小方块表示。  

如何在分布式数据库中实现 Hash Join ?-鸿蒙开发者社区

等 build 和 probe 阶段都做完,还有一部分数据在磁盘和内存中。那么递归分别对这些剩余的分片递归调用一遍 Hybrid Hash Join。Hybrid Hash Join 也会遇到 Grace Hash Join 的一些问题 Bad Case,处理的方法类似。这里不再赘述。

OceanBase 完整高效的实现了 Hybrid Hash Join 算法,欢迎使用。


本文转载自公众号OceanBase

分类
标签
已于2022-9-20 11:34:46修改
收藏
回复
举报
回复
    相关推荐