Oracle 采用内部一个 hash 函数作用于连接键上,将 S 和 B 分割成多个分区,在这里我们假设这个 hash 函数为求余函数,即 Mod(join_column_value,10) 。这样产生十个分区,如下表。
| 分区 | B0 | B1 | B2 | B3 | B4 | B5 | B6 | B7 | B8 | B9 | |
| 值 | 0,0,10,10 | 1,1,1,1,11 | 2,2,2,2,2,2 | 3 | NULL | NULL | NULL | NULL | 8 | 9,9,9 | |
| S0 | 10 | √ | |||||||||
| S1 | 1,1,1 | √ | |||||||||
| S2 | Null | ||||||||||
| S3 | 3,3 | √ | |||||||||
| S4 | 4,4,4,4 | ||||||||||
| S5 | 5 | ||||||||||
| S6 | NULL | ||||||||||
| S7 | NULL | ||||||||||
| S8 | 8,8,8,8 | √ | |||||||||
| S9 | NULL |
经过这样的分区之后,只需要相应的分区之间做 join 即可(也就是所谓的 partition pairs ),如果有一个分区为 NULL 的话,则相应的分区 join 即可忽略。
在将 S 表读入内存分区时,oracle将记录连接键的唯一值,构建成所谓的位图向量,它需要占 hash area 内存的 5% 左右。在这里即为 {1,3,4,5,8,10} 。
当对 B 表进行分区时,将每一个连接键上的值与位图向量相比较,如果不在其中,则将其记录丢弃。在我们这个例子中, B 表中以下数据将被丢弃 {0,0,2,2,2,2,2,2,9,9,9,9,9} 。这个过程就是位图向量过滤。
当 S1,B1 做完连接后,接着对 Si,Bi 进行连接,这里 oracle 将比较两个分区,选取小的那个做 build input ,就是动态角色互换,这个动态角色互换发生在除第一对分区以外的分区上面。
Hash Join 算法
第 1 步:判定小表是否能够全部存放在 hash area 内存中,如果可以,则做内存 hash join 。如果不行,转第二步。
第 2 步:决定 fan-out 数。
(Number of Partitions) * C<= Favm *M
其中 C 为 Cluster size ,
其值为 DB_BLOCK_SIZE*HASH_MULTIBLOCK_IO_COUNT ; Favm 为 hash area 内存可以使用的百分比,一般为 0.8 左右; M 为 Hash_area_size 的大小。
第 3 步:读取部分小表 S ,采用内部 hash 函数 ( 这里称为 hash_fun_1) ,将连接键值映射至某个分区,同时采用 hash_fun_2 函数对连接键值产生另外一个 hash 值,这个 hash 值用于创建 hash table 用,并且与连接键值存放在一起。
第 4 步:对 build input 建立位图向量。
第 5 步:如果内存中没有空间了,则将分区写至磁盘上。
第 6 步:读取小表 S 的剩余部分,重复第三步,直至小表 S 全部读完。
第 7 步:将分区按大小排序,选取几个分区建立 hash table( 这里选取分区的原则是使选取的数据最多 ) 。
第 8 步:根据前面用 hash_fun_2 函数计算好的 hash 值,建立 hash table 。
第 9 步:读取表 B ,采用位图向量进行位图向量过滤。
第 10 步:对通过过滤的数据采用 hash_fun_1 函数将数据映射到相应的分区中去,并计算 hash_fun_2 的 hash 值。
第 11 步:如果所落的分区在内存中,则将前面通过 hash_fun_2 函数计算所得的 hash 值与内存中已存在的 hash table 做连接, 将结果写致磁盘上。 如果所落的分区不在内存中,则将相应的值与表 S 相应的分区放在一起。
第 12 步:继续读取表 B ,重复第 9 步,直至表 B 读取完毕。
第 13 步:读取相应的 (Si,Bi) 做 hash 连接。在这里会发生动态角色互换。
Hash Join 的成本
1. In-Memory Hash Join
Cost(HJ)=Read(S)+ build hash table in memory(CPU)+Read(B) + Perform In memory Join(CPU)
忽略 cpu 的时间,则
Cost(HJ)=Read(S)+Read(B)
2. On-Disk Hash Join
根据上述的步骤描述,我们可以看出
Cost(HJ)=Cost(HJ1)+Cost(HJ2)
其中 Cost(HJ1) 的成本就是扫描 S,B 表,并将无法放在内存上的部分写回磁盘,对应前面第 2 步至第 12 步。
Cost(HJ2) 即为做 nested-loop hash join 的成本,对应前面的第 13 步至第 14 步。
其中 Cost(HJ1) 近似等于 Read(S)+Read(B)+Write((S-M)+(B-B*M/S)) 。
因为在做 nested-loop hash join 时,对每一 chunk 的 build input ,都需要读取整个 probe input ,因此
Cost(HJ2) 近似等于 Read((S-M)+n*(B-B*M/S))
其中 n 是 nested-loop hash join 需要循环的次数。 n=(S/F)/M
一般情况下,如果 n 大于 10 的话, hash join 的性能将大大下降。从 n 的计算公式可以看出, n 与 Fan-out 成反比例,
提高 fan-out ,可以降低 n 。当 hash_area_size 是固定时,可以降低 cluster size 来提高 fan-out 。
从这里我们可以看出,提高 hash_multiblock_io_count 参数的值并不一定提高 hash join 的性能。
当连接的两个表是用等值连接并且表的数据量比较大时,优化器才可能采用哈希连接。哈希连接是基于CBO的。只有在 数据库
初始化参数HASH_JOIN_ENABLED设为True,并且为参数PGA_AGGREGATE_TARGET设置了一个足够大的值的时候, Oracle才会使用哈
希边连接。HASH_AREA_SIZE是向下兼容的参数,但在Oracle9i之前的版本中应当使用HASH_AREA_SIZE。当使用ORDERED提示时,
FROM子句中的第一张表将用于建立哈希表。
当缺少有用的索引时,哈希连接比嵌套循环连接更加有效。哈希连接也可能比嵌套循环连接更快,因为处理内存中的哈希表
比检索B_树索引更加迅速。 Hash Join 适合于小表与大表连接、