Hash连接的两个阶段是有先后顺序的,生成阶段在前,探测阶段在后。Hash连接在生成阶段需要生成Hash Table,这需要向系统申请授予内存(granted memory),以存储Hash值,如果授予内存不足,那么Hash连接就不会开始。
1,哈希连接的内存消耗和溢出
在Hash连接开始执行前,SQL Server尝试估计需要多少内存去创建Hash表,优化器使用预估的行数和每行的平均大小去估算需要的内存大小。为了最小化内存的使用,优化器从两个表中选择一个小表作为生成输入(Build Input),SQL Server通过向系统申请授予内存来保证Hash表可以有“足够”的内存创建成功。
如果SQL Server授予哈希连接的内存小于实际需要的内存,那么Hash连接在生成阶段会耗尽授予的内存,并把部分Hash表的数据溢出到硬盘上,也就是tempdb的workfile文件中。在这样的情景下,Hash连接维护的Hash 桶(Hash Bucket)实际上存储在两个地方:内存中的hash表和硬盘上的workfile。
Hash连接会记录Hash桶存储的位置,当从生成输入中继续读取一行数据时,它检查该行是映射到内存的Hash桶中,还是映射到硬盘的Hash桶中,如果映射到内存的Hash桶中,那么把该Hash值写入到内存中;如果映射到硬盘的Hash桶中,那么把该Hash值写入到硬盘上的workfile结构中。
在探测阶段,对于探测表的每一行,优化器都会检查该行是映射到内存中的Hash 桶,还是映射到硬盘上的Hash 桶。如果映射到内存中hash桶,优化器就探测Hash表,产生连接的数据集存储到内存中,并舍弃当前行;如果映射到硬盘的Hash桶,它会被写入到硬盘中,暂时不做Join运算,当Hash的探测阶段完成之后,Hash连接会把溢出到硬盘的数据读取到内存,为每个Hash 桶重建内存Hash表,做Join运算。也就是说,哈希连接把硬盘上的结果集和内存中的结果集合并为最终的结果集。
2,Hash连接的性能优化
Hash连接使用授予内存来创建哈希表,如果授予内存不足,那么部分哈希桶就会被溢出到硬盘,硬盘的读取和写入都会影响哈希连接的性能。因此,应该为Hash连接分配足够的授予内存。优化器使用统计信息来预估授予内存的大小,如果统计信息过时,导致预估的授予内存过小,这会严重降低Hash连接的性能。
还有一种可能性是,参与Hash连接的两个表都很大,以至于不能分配足够的内存来构建哈希表,这也会导致哈希桶被溢出到workfile,导致哈希连接的性能低下。对于大表之间的映射,尽量使连接列有序,进而使用合并算法来做连接。
3,哈希连接的类型
本文只简单介绍内存哈希连接和Grace 哈希连接,对于递归哈希连接(Recursive Hash Join)和 Hash Bailout 是这两个连接类型的优化,不再赘述。
(1),内存哈希连接(In-Memory Hash Join)
哈希连接计算整个生成输入,然后在内存中创建一个哈希表(Hash Table)。每一个行都根据哈希值(Hash Value)计算Hash键,插入到相应的哈希桶(Hash Bucket)中。如果整个生成输入小于可能的内存,那么所有的数据行都会被存储到内存中哈希表中。
(2),Grace 哈希连接(Grace Hash Join)
如果生成输入太大,不能放到内存中,那么Hash Join将会被处理成多个步骤,这就是Grace 哈希连接。每一个步骤都有一个生成阶段和探测阶段。首先,Grace 哈希连接使用一个哈希函数,把整个生成输入和探测输入按照Hash键进行分区,把分区输入到不同的文件中,分区的规则是:
hash_func(join-field)
由于使用相同的hash算法,在连接列上进行分区,这就保证了匹配的任意两行数据都位于相同的文件对中,我推测:文件是在tempdb中创建的workfile,也就是说,需要调用IO子系统,把数据写入到硬盘中。Grace hash join的主要的贡献就在于,它使得每条数据只需要Join一次。
Grace 哈希连接首先把两个大的输入,分割成多个小的输入,然后再对每个文件对进行哈希连接操作,最后把每个分区Join的结果合并作为连接操作最终输出的结果。
参考文档:
Advanced Query Tuning Concepts
你真的了解Join吗?