从构建到使用,openLooKeng 如何实现 Hash Join ?

本文深入探讨了openLooKeng中的HashJoin实现,包括数据分区、HashTable构建和使用过程。首先介绍了LookupJoin的四种类型,然后详细阐述了HashJoin的工作原理,特别是HashBuilderOperator如何构建HashTable,以及LookupJoinOperator如何利用HashTable进行探测匹配。此外,还展示了关键类图和代码片段,解析了HashTable的内部结构和冲突处理。

Hash Join是在进行多表连接时常用的方式之一。那如何在openLooKeng上构建并实现Hash Join?openLooKeng支持的Join类型有哪些?本期,社区小伙伴将分享[openLooKeng Hash Join 实现原理],从构建到使用,内容十分详细,希望对大家有帮助。

1 openLooKeng Join概述

为了更好的介绍join,我们创建两个非常简单的表t1和t2。执行的SQL语句如下:

create table t1(id bigint, value bigint);insert into t1 values(1, 11);insert into t1 values(2, 22);insert into t1 values(3, 33);insert into t1 values(4, 44);

create table t2(id bigint, value bigint);insert into t2 values(1, 111);insert into t2 values(2, 11);insert into t2 values(3, 333);insert into t2 values(4, 33);

openLooKeng的join有四类:

1) Lookup Join 大部分类型的Join都由Lookup Join完成。例如我们执行SQL语句如下: select * from t1 inner join t2 on t1.value=t2.value;

其中,执行join所在stage涉及算子如下图所示:

▲ 图1-1 Lookup Join
▲ 图1-1 Lookup Join

完成Join的算子是HashBuilderOperator和LookupJoinOperator。而本文即将介绍Hash Join的原理,也就是这两个算子的实现原理。

2) Nested Loop Join 执行SQL语句“select * from t1 join t2 on t1.value > t2.value;”,join所在stage涉及算子如下图所示,其中完成Join的算子是NestedLoopBuilderOperator和NestedLoopJoinOperator。

▲ 图1-2 Nested Loop Join
▲ 图1-2 Nested Loop Join

3) Hash Semi Join 执行SQL语句“select * from t1 where value in (select value from t2);”,join所在stage涉及算子如下图所示,其中完成join的算子是SetBuilderOperator和HashSemiJoinOperator。

▲ 图1-3 Hash Semi Join
▲ 图1-3 Hash Semi Join

4) Spatial Join 执行SQL语句“select * from t1, t2 where ST_distance(ST_Point(t1.id, t1.value), ST_Point(t2.id, t2.value)) <= 10;”,join所在stage涉及算子如下图所示,其中完成join的算子是SpatialIndexBuilderOperator和SpatialJoinOperator。

▲ 图1-4 Spatial Join
▲ 图1-4 Spatial Join

本博客关注的是Hash Join的实现原理分析,其他类型的Join后续展开介绍。

2 openLooKeng Hash Join实现原理
通常,我们称Join操作的右表为build表,左表为probe表。 Hash Join对应的逻辑执行计划为JoinNode,物理执行计划则由两个算子完成工作,其中HashBuilderOperator根据build表来构建Hash Table,LookupJoinOperator完成对probe表逐行去Hash Table探测,找到匹配行。

2.1 build侧数据partition
数据进入HashBuilderOperator之前已经由LocalExchangeSinkOperator和LocalExchangeSourceOperator完成数据partition,即join key的哈希值相同的数据进入同一个HashBuilderOperator。LocalExchangeSinkOperator和LocalExchangeSourceOperator对应的逻辑执行计划的ExchangeNode。在openLooKeng中,ExchangeNode和JoinNode的模型关系如图2-1所示。

▲ 图2-1 LocalExchange与LookupJoin的模型关系
▲ 图2-1 LocalExchange与LookupJoin的模型关系

图2-1中,有16个partition,LocalExchangeSinkOperator接收page后,根据Join key计算hash值,将hash值相同的数据组成新page,再根据hash值计算partition index,选择相应的LocalExchangeSourceOperator。而LocalExchangeSourceOperator是HashBuilderOperator的上游算子,因此进入HashBuilderOperator的数据是已经partition过后的数据。

2.2 Hash Join类图

▲ 图2-2 Hash Join类图
▲ 图2-2 Hash Join类图

图2-2展示了Hash Join所涉及的类图,其中比较重要的类有以下几个:

1) HashBuilderOperatorFactory/HashBuilderOperator,针对build表构建Hash Table;

2) JoinHash,Hash Table承载类;

3) LookupJoinOperatorFactory/LookupJoinOperator,负责probe表逐行探测;

4) LookupJoinPageBuilder,负责构建输出page。

2.3 Hash Table构建
HashBuilderOperator负责对build表构建Hash Table。它的基本流程是: 1) addInput()时将Page累积在内存中;

2) finish()时,则创建Hash Table;

3) 不再阻塞LookupJoinOperator,即LookupJoinOperator可以开始处理。 我们重点讲一下Hash Table的构建。

JoinHash中有两个非常重要的类:PagesHash和PositionLinks。

我们先来看PagesHash。PagesHash的field有:

1) addresses,对page内每一行数据进行地址编码,编码公示为“pageIndex « 32 | rowIndex”,如有2个page,每个page有2行,则addresses内存放的是0,1,4294967296,4294967297;

2) PageHashStrategy,会将原始的数据保存下来;

3) key数组,可以理解为hash表,根据某行join key计算得到hash值,再将hash值进行hash计算得到一个hash表的offset,如果这个offset上没有值则存放该行的address,例如addresses中1这个地址对应的行计算出offset为6,而这个位置没有被占用,则key[6] = 1;

4) mask,掩码,用于对数组key求offset;

5) positionToHashes,byte数组,根据join key计算hash值,但是只保存低位的byte。PositionLinks处理的是,当hash值冲突且原始值也相同时,将满足这些情况的数据address使用数组链起来。核心代码片段如下:

下面我们举例来说明。

如图所示,join key只有1个,page只有1个,其值如①所示。对这些行进行地址编码,则编码后地址如②所示。Hash table构建步骤: 1) 对原始数据进行hash计算,结果如③所示; 2) 逐行处理addresses:

最终得到的key数组如④所示,得到的positionLinks如⑤所示。

2.4 Hash Table使用
HashBuilderOperator构建完hash table后,LookupJoinOperator才能开始处理数据进行探测。而LookupJoinOperator使用hash table的核心代码片段如下:

Hash table使用步骤:

1) 对原始数据进行hash计算得到rawHash;

2) 对rawHash再进行hash计算得到其在hash table的offset,即pos;

3) 若key[pos]为-1,则没有匹配;

4) 若key[pos]不是-1,则hash值匹配,若原始数据是否相等,相等则完全匹配上,返回key[pos],即原始数据的地址address;若原始数据不相等则pos加1再循环判断。

3 总结
本文介绍了openLooKeng支持的join类型,并展开介绍了Lookup join的partition,然后重点介绍了hash table的构建和使用过程,但其实Lookup Join的内容不止这些,比如HashBuilderOperator和LookupJoinOperator如何实现同步,LookupJoinOperator的probe后的输出数据如何构造,非等值的join又是如何实现的,请期待后续的文章!

<think>我们正在讨论MergeJoinHashJoin的性能比较。根据用户的问题,他想知道MergeJoin在哪些场景下性能优于HashJoin。我们可以结合数据库原理和引用材料来分析。引用[1]提到:HashJoin需要将BuildTable(小表)构建HashTable,且最好全部加载在内存中,因此适合至少一个小表的场景。而两个大表的Join场景并不适用。引用[2]提到:在Spark中使用sort-mergejoin或shufflehashjoin来避免过多的shuffle并缓解数据倾斜问题。结合数据库知识,我们可以分析:1.**数据已排序的情况**:-如果两个输入表在连接键上已经有序(例如有索引或预先排序),那么MergeJoin可以直接开始合并过程,避免了排序开销。-而HashJoin则需要先对至少一个表进行哈希分区(可能涉及shuffle)和构建哈希表,此时MergeJoin具有明显优势。2.**连接两个大表**:-当两个表都很大时,HashJoin可能需要将哈希表溢出到磁盘(如果内存不足),导致性能下降。-MergeJoin则没有内存中构建哈希表的需求,它通过流式处理两个有序输入,只需在内存中保持当前行和部分重复键的缓存,因此更适合大表连接。3.**内存限制**:-在内存受限的环境中,HashJoin可能因为哈希表太大而频繁进行磁盘I/O。-MergeJoin的内存占用相对较小(主要取决于重复键的数量),因此在这种场景下性能更优。4.**连接键重复率高**:-当连接键重复率很高时,HashJoin需要处理哈希冲突,而MergeJoin则能高效地处理重复键(通过顺序扫描重复块)。5.**避免数据倾斜**:-引用[2]提到sort-mergejoin可以缓解数据倾斜问题。因为HashJoin在数据倾斜时(某个键值的数据量极大)会导致哈希表不均匀,甚至某个分区过大而内存溢出。而MergeJoin对每个键的处理是顺序的,不会因为某个键值数据量特别大而导致内存不足(除非重复键的数量巨大,但通常可以分批处理)。6.**非等值连接**:-虽然问题主要讨论等值连接,但值得注意的是,MergeJoin可以扩展支持非等值连接(如范围连接),而HashJoin只能用于等值连接。**总结MergeJoin优于HashJoin的场景**:-两个输入表在连接键上已经有序。-两个表都很大,且内存不足以容纳哈希表。-内存受限的环境。-连接键重复率较高。-存在数据倾斜,导致HashJoin效率降低。**性能对比公式**:-假设两个表的大小分别为$m$和$n$,且均已有序。-MergeJoin的时间复杂度为$O(m+n)$。-HashJoin的平均时间复杂度也是$O(m+n)$,但实际性能受哈希函数、冲突处理、内存使用等因素影响。-当需要显式排序时,MergeJoin的排序阶段复杂度为$O(m\logm+n\logn)$,因此只有在输入已排序或排序成本较低时才有优势。**如何选择**:数据库优化器会根据表大小、索引、内存设置等因素自动选择。但我们可以通过以下方式影响优化器:-在连接键上建立索引,使输入有序,从而让优化器倾向于选择MergeJoin。-调整内存配置(如为HashJoin分配更多内存可能使其更高效)。**相关问题**:1.在PostgreSQL中如何查看执行计划以确认使用了MergeJoin?2.如何通过索引设计使MergeJoin成为优化器的首选?3.当两个表都很大且未排序时,选择MergeJoin还是HashJoin?4.MergeJoin在处理非等值连接时有什么优势?</think>### Merge Join 优于 Hash Join 的场景与条件分析 Merge Join 在以下场景中性能显著优于 Hash Join,这些场景主要由**数据特性**和**系统资源**决定: --- #### **1. 输入数据已有序** - **场景**:连接键上存在**预排序索引**(如 B-Tree)或上游算子已输出有序结果 - **优势**: - Merge Join 直接跳过排序阶段,复杂度为$O(m+n)$ - Hash Join 需额外$O(n \log n)$排序成本(无索引时) - **示例**: ```sql -- 索引加速 Merge Join CREATE INDEX idx_orders ON orders(customer_id); CREATE INDEX idx_customers ON customers(id); SELECT * FROM orders JOIN customers ON orders.customer_id = customers.id; -- 优化器自动选择 Merge Join ``` --- #### **2. 连接大表且内存受限** - **场景**:两个大表连接,可用内存小于较小表尺寸 - **优势**: - Merge Join **流式处理**数据,仅需缓存当前重复键行 - Hash Join 需将整个 Build Table 装入内存,否则触发磁盘溢出(性能下降 10-100 倍[^1]) - **临界条件**: $$ \text{内存容量} < \min(\text{Table}_A, \text{Table}_B) $$ > 当表大小超过内存时,Merge Join 性能优势显著 --- #### **3. 高重复键值场景** - **场景**:连接键**重复率高**(如状态字段、类型字段) - **优势**: - Merge Join 顺序扫描时**批量处理重复键**,减少比较次数 - Hash Join 需为每个键计算哈希值并处理冲突 - **数据特征**: $$ \frac{\text{distinct keys}}{\text{total rows}} < 0.1 $$ > 重复率越高,Merge Join 优势越明显 --- #### **4. 存在严重数据倾斜** - **场景**:连接键分布极度不均衡(如 90% 数据集中少数键) - **优势**: - Merge Join **无哈希冲突问题**,性能稳定 - Hash Join 的倾斜分区会拖累整个任务(Spark 中常见问题[^2]) - **解决方案**: ```sql -- Spark 强制使用 Sort-Merge Join 避免倾斜 SET spark.sql.join.preferSortMergeJoin = true; ``` --- #### **5. 非等值连接需求** - **场景**:连接条件为**范围比较**(如 `ON A.date BETWEEN B.start AND B.end`) - **优势**: - Merge Join 天然支持**有序流上的范围扫描** - Hash Join 仅支持等值连接,非等值需回退到嵌套循环 --- #### **性能对比总结** | **指标** | Merge Join | Hash Join | 优势场景 | |----------------|--------------------|--------------------|----------------------| | 内存要求 | $O(1)$ (流式) | $O(n)$ (哈希表) | 大表连接 | | 预处理开销 | $O(1)$ (已排序时) | $O(n \log n)$ | 有索引/预排序数据 | | 数据倾斜影响 | 低 | 极高 | 键值分布不均衡 | | 连接类型支持 | 等值/非等值 | 仅等值 | 范围连接 | | 重复键处理效率 | 高 (批量处理) | 中 (哈希冲突) | 高重复率数据 | --- #### **何时强制使用 Merge Join?** 在 PostgreSQL 中可通过优化器提示强制使用: ```sql /*+ MergeJoin(orders customers) */ SELECT * FROM orders JOIN customers ON orders.customer_id = customers.id; ``` > **适用条件**:满足上述 1-4 中任意场景时强制使用可提升性能[^1] ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值