如果两个联接输入不小,但在联接列上排序(例如,如果通过扫描排序索引获得联接输入),则合并联接是最快的联接作。 如果两个联接输入都很大,而且这两个输入的大小差不多,则预先排序的合并联接提供的性能与哈希联接相近。 但是,如果这两个输入的大小相差很大,则哈希联接操作通常快得多。
合并联接操作可以是常规操作,也可以是多对多操作。 多对多合并联接使用临时表存储行。 如果每个输入中有重复值,则在处理其中一个输入中的每个重复项时,另一个输入必须重绕到重复项的开始位置。
如果存在驻留谓词,则所有满足合并谓词的行都将对该驻留谓词取值,而只返回那些满足该驻留谓词的行。
合并联接本身的速度很快,但如果需要排序操作,选择合并联接就会非常费时。 然而,如果数据量很大且能够从现有 B 树索引中获得预排序的所需数据,则合并联接通常是最快的可用联接算法。
哈希联接可以有效处理未排序的大型非索引输入。 它们对复杂查询的中间结果很有用,因为:
中间结果不会编制索引(除非显式保存到磁盘,然后编制索引),并且通常不会对查询计划中下一个作进行适当排序。
查询优化器只估计中间结果的大小。 由于对于复杂查询,估计可能有很大的误差,因此如果中间结果比预期的大得多,则处理中间结果的算法不仅必须有效而且必须适度弱化。
哈希联接可以减少使用非规范化。 非规范化一般通过减少联接操作获得更好的性能,尽管这样做有冗余之险(如不一致的更新)。 哈希联接则减少使用非规范化的需要。 哈希联接使垂直分区(用单独的文件或索引代表单个表中的几组列)得以成为物理数据库设计的可行选项。
哈希联接有两种输入:生成输入和探测输入。 查询优化器指派这些角色,使两个输入中较小的那个作为生成输入。
哈希联接用于多种设置匹配操作:内部联接;左外部联接、右外部联接和完全外部联接;左半联接和右半联接;交集;并集和差异。 此外,哈希联接的某种变形可以进行重复删除和分组,例如 SUM(salary) GROUP BY department。 这些修改对生成和探测角色只使用一个输入。
以下几节介绍了不同类型的哈希联接:内存中的哈希联接、Grace 哈希联接和递归哈希联接。
内存中的哈希联接
哈希联接先扫描或计算整个生成输入,然后在内存中生成哈希表。 根据计算得出的哈希键的哈希值,将每行插入哈希存储桶。 如果整个生成输入小于可用内存,则可以将所有行都插入哈希表中。 生成阶段之后是探测阶段。 一次一行地对整个探测输入进行扫描或计算,并为每个探测行计算哈希键的值,扫描相应的哈希存储桶并生成匹配项。
Grace 哈希联接
如果生成输入不适合内存,哈希联接将分几个步骤进行。 这称为“Grace 哈希联接”。 每一步都分为生成阶段和探测阶段。 首先,消耗整个生成和探测输入并将其分区(使用哈希键上的哈希函数)为多个文件。 对哈希键使用哈希函数可以保证任意两个联接记录一定位于相同的文件对中。 因此,联接两个大输入的任务简化为相同任务的多个较小的实例。 然后将哈希联接应用于每对分区文件。
递归哈希联接
如果生成输入非常大,以至于标准外部合并的输入需要多个合并级别,则需要多个分区步骤和多个分区级别。 如果只有某些分区较大,则只需对那些分区使用附加的分区步骤。 为了使所有分区步骤尽可能快,将使用大的异步 I/O 操作以便单个线程就能使多个磁盘驱动器繁忙工作。
如果生成输入仅稍大于可用内存,则内存中的哈希联接和 Grace 哈希联接的元素将结合在一个步骤中,生成混合哈希联接。
在优化期间,并不总是能够确定使用哪个哈希联接。 因此,SQL Server 开始时使用内存中的哈希联接,然后根据生成输入的大小逐渐转换到 Grace 哈希联接和递归哈希联接。
如果查询优化器错误地预计两个输入中哪个较小并由此确定哪个作为生成输入,生成角色和探测角色将动态反转。 哈希联接确保使用较小的溢出文件作为生成输入。 这一技术称为角色反转。 至少一个文件溢出到磁盘后,哈希联接中才会发生角色反转。
角色反转的发生独立于任何查询提示或结构。 角色逆转不会显示在查询计划中;发生时,对用户是透明的。
术语“哈希援助”有时用于描述 Grace 哈希联接或递归哈希联接。
递归哈希联接或哈希援助会导致服务器性能降低。 如果跟踪中显示许多哈希警告事件,请更新正在联接的列上的统计信息。
有关哈希援助的详细信息,请参阅 Hash Warning 事件类。
自适应联接
借助批处理模式自适应联接功能,可延迟选择哈希联接或嵌套循环联接方法,将其延迟到扫描第一个输入之后。 自适应联接运算符可定义用于决定何时切换到嵌套循环计划的阈值。 因此,查询计划可在执行期间动态切换到较好的联接策略,而无需进行重新编译。
小型和大型联接输入扫描之间频繁振荡的工作负荷将从此功能获益最大。
运行时决策基于以下步骤:
如果生成联接输入的行计数足够小,以致于嵌套循环联接优于哈希联接,则计划将切换到嵌套循环算法。
如果生成联接输入超过特定行计数阈值,则不会进行切换并且计划将通过哈希联接继续。
以下查询用于说明自适应联接示例:
SELECT [fo].[Order Key], [si].[Lead Time Days], [fo].[Quantity]
FROM [Fact].[Order] AS [fo]
INNER JOIN [Dimension].[Stock Item] AS [si]
ON [fo].[Stock Item Key] = [si].[Stock Item Key]
WHERE [fo].[Quantity] = 360;
查询将返回 336 行。 启用实时查询统计信息会显示以下计划:
用于为哈希联接生成阶段提供行的列存储索引扫描。
新的自适应联接运算符。 此运算符可定义用于决定何时切换到嵌套循环计划的阈值。 对于此示例,阈值为 78 行。 包含 >= 78 行的任何示例均将使用哈希联接。 如果小于阈值,将使用嵌套循环联接。
由于查询返回 336 行,超过了阈值,因此,第二个分支表示标准哈希联接操作的探测阶段。 实时查询统计信息将显示流经运算符的行,在本示例中为“672 行,共 672 行”。
并且,最后一个分支是供未超出阈值的嵌套循环联接使用的聚集索引查找。 我们将看到显示“0 行,共 336 行”(未使用分支)。
现将计划与同一查询进行对比,但当表中的 Quantity 值只有一行时:
SELECT [fo].[Order Key], [si].[Lead Time Days], [fo].[Quantity]
FROM [Fact].[Order] AS [fo]
INNER JOIN [Dimension].[Stock Item] AS [si]
ON [fo].[Stock Item Key] = [si].[Stock Item Key]
WHERE [fo].[Quantity] = 361;
查询将返回一行。 启用“实时查询统计信息”会显示以下计划:
自适应联接引入了比索引嵌套循环联接等效计划更高的内存要求。 它会请求额外的内存,就像嵌套循环属于哈希联接一样。 生成阶段还有一个开销,即即点即用作与嵌套循环流式处理等效联接。 这笔额外成本产生的同时也实现了行计数可在生成输入中波动的方案灵活性。
批处理模式自适应联接适用于语句的初始执行,编译后,根据编译的自适应联结阈值和流经外部输入生成阶段的运行时行,连续执行将保持自适应状态。
如果自适应联接切换到嵌套循环操作,它将使用哈希联接生成已经读取的行。 运算符不会再次重新读取外部引用行。
跟踪自适应联接活动
自适应联接运算符具有以下计划运算符属性:
Plan 属性
Description
查询是 SELECT 语句(数据修改语句当前不符合条件)。
联接符合同时由索引嵌套循环联接或哈希联接物理算法执行的条件。
哈希联接使用批处理模式,通过整体查询中的列存储索引状态、联接直接引用的列存储索引表,或通过使用针对行存储功能批处理模式来启用。
嵌套循环联接和哈希联接生成的替代解决方案的第一个子级(外部引用)应相同。
自适应阈值行
下图显示了哈希联接的成本与嵌套循环联接替代的成本之间的示例交集。 在这个交点处,确定了阈值,该阈值将反过来确定将实际用于联接操作的算法。
在不更改兼容性级别的情况下禁用自适应联接
可在数据库或语句范围内禁用自适应联接,同时将数据库兼容性级别维持在 140 或更高。
若要对源自数据库的所有查询执行禁用自适应联接,请在对应数据库的上下文中执行以下命令:
-- SQL Server 2017
ALTER DATABASE SCOPED CONFIGURATION SET DISABLE_BATCH_MODE_ADAPTIVE_JOINS = ON;
-- Azure SQL Database, SQL Server 2019 and later versions
ALTER DATABASE SCOPED CONFIGURATION SET BATCH_MODE_ADAPTIVE_JOINS = OFF;
启用后,此设置在 sys.database_scoped_configurations 中将显示为已启用。
若要对源自数据库的所有查询执行重新启用自适应联接,请在对应数据库的上下文中执行以下命令:
-- SQL Server 2017
ALTER DATABASE SCOPED CONFIGURATION SET DISABLE_BATCH_MODE_ADAPTIVE_JOINS = OFF;
-- Azure SQL Database, SQL Server 2019 and later versions
ALTER DATABASE SCOPED CONFIGURATION SET BATCH_MODE_ADAPTIVE_JOINS = ON;
此外,将 DISABLE_BATCH_MODE_ADAPTIVE_JOINS 指定为 USE HINT 查询提示也可为特定查询禁用自适应联接。 例如:
SELECT s.CustomerID,
s.CustomerName,
sc.CustomerCategoryName
FROM Sales.Customers AS s
LEFT OUTER JOIN Sales.CustomerCategories AS sc
ON s.CustomerCategoryID = sc.CustomerCategoryID
OPTION (USE HINT('DISABLE_BATCH_MODE_ADAPTIVE_JOINS'));
USE HINT查询提示优先于数据库范围的配置或跟踪标志设置。
NULL 值和联接
如果联接的表的列中存在 null 值,则 null 值不匹配。 如果其中一个联接表的列中出现空值,只能通过外部联接返回这些空值(除非 WHERE 子句不包括空值)。
下面的两个表中,每个表中要参与联接的列中均包含 NULL:
table1 table2
a b c d
------- ------ ------- ------
1 one NULL two
NULL three 4 four
4 join4
将列中的值与列ac的值进行比较的联接不会对具有以下NULL值的列进行匹配:
SELECT *
FROM table1 t1 JOIN table2 t2
ON t1.a = t2.c
ORDER BY t1.a;
只返回列 4 和 a 中值为 c 的一行:
a b c d
----------- ------ ----------- ------
4 join4 4 four
(1 row(s) affected)
另外,从基表返回的空值与从外部联接返回的空值很难区分开。 例如,下面的 SELECT 语句对这两个表执行左向外部联接:
SELECT *
FROM table1 t1 LEFT OUTER JOIN table2 t2
ON t1.a = t2.c
ORDER BY t1.a;
结果集如下。
a b c d
----------- ------ ----------- ------
NULL three NULL NULL
1 one NULL NULL
4 join4 4 four
(3 row(s) affected)
结果不容易区分 NULL 数据 NULL 中表示联接失败的情况。 当 NULL 数据中存在值时,通常最好使用常规联接从结果中省略这些值。
逻辑和物理显示计划运算符参考
比较运算符 (Transact-SQL)
数据类型转换 (数据库引擎)
子查询 (SQL Server)
自适应联接
FROM 子句以及 JOIN、APPLY、PIVOT (Transact-SQL)