# 一、基本概念
哈希连接(Hash Join)是一种两个表在做表连接时主要依靠哈希运算来得到连接结果集的表连接方法。
在Oracle 7.3之前,Oracle数据库中的常用表连接方法就只有排序合并连接和嵌套循环连接这两种,但这两种方法都各有其明显缺陷。对于排序合并连接,如果两个表在施加了目标SQL中指定的谓词条件(如果有的话)后得到的结果集很大且需要排序,则排序合并连接的执行效率一定不高;而对于嵌套循环连接,如果驱动表所对应的驱动结果集的记录数很大,即便在被驱动表的连接列上存在索引,此时使用嵌套循环连接的执行效率也会同样不高。
为了解决排序合并连接和嵌套循环连接在上述情形下执行效率不高的问题,同时也为了给优化器提供一种新的选择,Oracle在Oracle 7.3中引入了哈希连接。从理论上来说,哈希连接的执行效率会比排序合并连接和嵌套循环连接要高,当然,实际情况并不总是这样。
在Oracle 10g及其以后的Oracle数据库版本中,优化器(实际上是CBO,因为哈希连接仅适用于CBO)在解析目标SQL时是否考虑哈希连接是受限于隐含参数_HASH_JOIN_ENABLED,而在Oracle 10g以前,CBO在解析目标SQL时是否考虑哈希连接则是受限于参数HASH_JOIN_ENABLED。
_HASH_JOIN_ENABLED的默认值是TRUE,表示允许CBO在解析目标SQL时考虑哈希连接。当然,即使将该参数的值改成了FALSE,使用USE_HASH Hint依然可以让CBO在解析目标SQL时考虑哈希连接,这说明USE_HASH Hint的优先级比参数_HASH_JOIN_ENABLED的优先级要高。
# 二、哈希连接的执行过程
如果两个表(这里将它们分别命名为表T1和表T2)在做表连接时使用的是哈希连接,则Oracle会依次顺序执行如下步骤。
## 2.1 首先Oracle会根据参数HASH_AREA_SIZE、DB_BLOCK_SIZE和_HASH_MULTIBLOCK_IO_COUNT的值来决定Hash Partition的数量(Hash Partition是一个逻辑上的概念,它实际上是一组Hash Bucket的集合。所有Hash Partition的集合就被称为Hash Table,即一个Hash Table由多个Hash Partition所组成,而一个Hash Partition又是由多个Hash Bucket所组成的)。
## 2.2 表T1和T2在施加了目标SQL中指定的谓词条件(如果有的话)后,得到的结果集中数据量较少的那个结果集会被Oracle选为哈希连接的驱动结果集,这里我们假设T1所对应的结果集的数据量相对较少,记为S;T2所对应的结果集的数据量相对较多,记为B。显然这里S是驱动结果集,B是被驱动结果集。
## 2.3 接着Oracle会遍历S,读取S中的每一条记录,并对每一条记录按照该记录在表T1中的连接列做哈希运算。这个哈希运算会使用两个内置哈希函数,这两个哈希函数会同时对该连接列计算哈希值,我们把这两个内置哈希函数分别记为hash_func_1和hash_func_2,它们所计算出来的哈希值分别记为hash_value_1和hash_value_2。
## 2.4 然后Oracle会按照hash_value_1的值把相应的S中的对应记录存储在不同Hash Partition的不同Hash Bucket里,同时和该记录存储在一起的还有该记录用hash_func_2计算出来的hash_value_2。注意,存储在Hash Bucket里的记录并不是目标表的完整行记录,只需要存储位于目标SQL中与目标表相关的查询列和连接列就足够了。我们把S所对应的每一个Hash Partition记为Si。
## 2.5 在构建Si的同时,Oracle会构建一个位图(BITMAP),这个位图用来标记Si所包含的每一个Hash Bucket是否有记录(即记录数是否大于0)。
## 2.6 如果S的数据量很大,那么在构建S所对应的Hash Table时,就可能会出现PGA的工作区(WORK AREA)被填满的情况。这时候Oracle会把工作区中包含记录数最多的Hash Partition写到磁盘上(TEMP表空间)。接着Oracle会继续构建S所对应的Hash Table,在继续构建的过程中,如果工作区又满了,则Oracle会继续重复上述动作,即挑选包含记录数最多的Hash Partition并写回到磁盘上。如果要构建的记录所对应的Hash Partition已经事先被Oracle写回磁盘,则此时Oracle就会去磁盘上更新该Hash Partition,即把该条记录和hash_value_2直接加到这个已经位于磁盘上的Hash Partition的相应Hash Bucket中。注意,极端情况下可能会出现只有某个Hash Partition的部分记录还在内存中,该Hash Partition的剩余部分和余下的所有Hash Partition都已经被写回到磁盘上。
## 2.7 上述构建S所对应的Hash Table的过程会一直持续下去,直到遍历完S中的所有记录为止。
## 2.8 接着,Oracle会对所有的Si按照它们所包含的记录数来排序,然后把这些已经排好序的Hash Partition按顺序依次且尽可能全部放到内存中(PGA的工作区),当然,如果实在放不下,放不下的那部分Hash Partition还是会位于磁盘上。我个人认为这个按照Si的记录数来排序的动作不是必须要做的,因为这个排序动作的根本目的就是为了尽可能多地把那些记录数较小的Hash Partition保留在内存中,而将那些已经被写回磁盘、记录数较大且现有内存已经放不下的Hash Partition保留在磁盘上,显然,如果所有的Si本来就都在内存中,也没发生过将Si写回到磁盘的操作,这里根本就不需要排序了。
## 2.9 至此Oracle已经处理完S,现在可以开始处理B了。
## 2.10 Oracle会遍历B,读取B中的每一条记录,并按照该记录在表T2中的连接列做哈希运算,这个哈希运算和步骤3中的哈希运算是一模一样的,即还是会用步骤3中的hash_func_1和hash_func_2,并且也会计算出两个哈希值hash_value_1和hash_value_2。
接着Oracle会按照该记录所对应的哈希值hash_value_1去Si里找匹配的Hash Bucket;如果能找到匹配的Hash Bucket,则Oracle还会遍历该Hash Bucket中的每一条记录,并校验存储于该Hash Bucket中的每一条记录的连接列,看是否是真的匹配(即这里要校验S和B中的匹配记录所对应的连接列是否真的相等,因为对于哈希运算而言,不同的值经过哈希运算后的结果可能是相同的)。如果是真的匹配,则上述hash_value_1所对应B中记录的位于目标SQL中的查询列和该Hash Bucket中的匹配记录便会组合起来,一起作为满足目标SQL连接条件的记录返回。如果找不到匹配的Hash Bucket,则Oracle就会去访问步骤5中构建的位图。
## 2.11 上述去Si中查找匹配Hash Bucket和构建Bj的过程会一直持续下去,直到遍历完B中的所有记录为止。
## 2.12 至此Oracle已经处理完所有位于内存中的Si和对应的Bj,现在只剩下位于磁盘上的Si和Bj还未处理。
## 2.13 因为在构建Si和Bj时用的是同样的哈希函数hash_func_1和hash_func_2,所以Oracle在处理位于磁盘上的Si和Bj的时候可以放心地配对处理,即只有对应Hash Partition Number值相同的Si和Bj才可能会产生满足连接条件的记录。这里我们用Sn和Bn来表示位于磁盘上且对应Hash Partition Number值相同的Si和Bj。
## 2.14 对于每一对Sn和Bn,它们之中记录数较少的会被当作驱动结果集,然后Oracle会用这个驱动结果集Hash Bucket里记录的hash_value_2来构建新的Hash Table,另外一个记录数较多的会被当作被驱动结果集,然后Oracle会用这个被驱动结果集Hash Bucket里记录的hash_value_2去上述构建的新Hash Table中找匹配记录。注意,对每一对Sn和Bn而言,Oracle始终会选择它们中记录数较少的来作为驱动结果集,所以每一对Sn和Bn的驱动结果集都可能会发生变化,这就是所谓的“动态角色互换”。
## 2.15 步骤14中如果存在匹配记录,则该匹配记录也会作为满足目标SQL连接条件的记录返回。
## 2.16 上述处理Sn和Bn的过程会一直持续下去,直到遍历完所有的Sn和Bn为止。
# 三、查看数据库中的执行计划
select /*+ use_hash(a,b) */ * from dw0810 a , dw0810a b where a.object_id=b.object_id and a.object_id=20
Plan Hash Value : 3338344890
---------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost | Time |
---------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 196 | 4 | 00:00:01 |
| * 1 | HASH JOIN | | 1 | 196 | 4 | 00:00:01 |
| 2 | TABLE ACCESS BY INDEX ROWID | DW0810A | 1 | 98 | 2 | 00:00:01 |
| * 3 | INDEX RANGE SCAN | IDX_DW0810A | 1 | | 1 | 00:00:01 |
| 4 | TABLE ACCESS BY INDEX ROWID | DW0810 | 1 | 98 | 2 | 00:00:01 |
| * 5 | INDEX RANGE SCAN | IDX_DW0810 | 1 | | 1 | 00:00:01 |
---------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
------------------------------------------
* 1 - access("A"."OBJECT_ID"="B"."OBJECT_ID")
* 3 - access("B"."OBJECT_ID"=20)
* 5 - access("A"."OBJECT_ID"=20)# 四、 HASH JOIN哈希连接的注意事项
以上hash join连接的过程摘抄与崔华老师的《基于oracle的sql优化》,步骤详细介绍了hash join的整个过程,也比较难记住,但是需要记住以下几点:
## 4.1 哈希连接对驱动表及被驱动表只做一次扫描
## 4.2 哈希连接存在驱动表和被驱动表,驱动表是经谓词(如果有的话)过滤后结果集较小的,两表的关联条件经谓词过滤后(如果有的话),对两表的结果集做hash运算,hash运算的结果被放在不同的桶里(理解为将结果集经hash运算后被打散放在不同的桶里),T1表的id1经hash计算后为x1,T2表的id1经hash计算后的结果也是x1,T2表的这个行的x1就去T1表的hash链表去匹配,匹配上比较真实的值,如果匹配则真实返回需要的展示字段。
## 4.3 如果驱动表或者被驱动表结果集都非常大,session的pga放不下,就会使用到temp,会产出磁盘读写。
## 4.4 哈希连接只适用于CBO,它也只能用于等值连接条件(即使是哈希反连接,Oracle实际上也是将其转换成了等价的等值连接)。
## 4.5 哈希连接很适合于小表和大表之间做表连接且连接结果集的记录数较多的情形,特别是在小表的连接列的可选择性非常好的情况下,这时候哈希连接的执行时间就可以近似看作是和全表扫描那个大表所耗费的时间相当。
## 4.6 哈希连接时,当关联条件在驱动表及被驱动表都存在大两重复时,会产生性能问题(为什么会产生呢,因为关联字段大量重复,经hash运算的桶里的数据就特别多,被驱动表的hash值就需要每次都来遍历这个大桶,消耗大量cpu资源,可以想象,如果关联字段的值都是同一个值,就是笛卡尔连接了啊),消耗cpu资源,但是不是逻辑读,因为大量重复的关联字段,经hash运算生成的桶在pga中比较,不属于逻辑读的范畴。




