联接 (SQL Server)

适用于:SQL ServerAzure SQL 数据库Azure SQL 托管实例Azure Synapse AnalyticsMicrosoft Fabric 中的 SQL 数据库

SQL Server 使用联接基于它们之间的逻辑关系从多个表中检索数据。 连接是关系数据库操作的基础,能够将来自两个或多个表的数据合并到单个结果集中。

SQL Server 实现逻辑联接作(由 Transact-SQL 语法定义)和物理联接作(用于执行联接的实际算法)。 了解这两个方面有助于编写高效的查询并优化数据库性能。

逻辑联接操作包括:

  • 内部联接
  • 左、右和完整外部联接
  • 交叉联接

物理联接操作包括:

  • 嵌套循环联接
  • 合并联接
  • 哈希联接
  • 自适应联接(适用于: SQL Server 2017 (14.x) 及更高版本)

本文介绍联接的工作原理、何时使用不同的联接类型,以及查询优化器如何根据表大小、可用索引和数据分布等因素选择最有效的联接算法。

Note

有关联接语法的详细信息,请参阅 FROM 子句以及 JOIN、APPLY 和 PIVOT

联接基础知识

通过联接,可以从两个或多个表中根据各个表之间的逻辑关系来检索数据。 联接指明了 SQL Server 应如何使用一个表中的数据来选择另一个表中的行。

联接条件可通过以下方式定义两个表在查询中的关联方式:

  • 指定每个表中用于联接的列。 典型的联接条件在一个表中指定一个外键,而在另一个表中指定与其关联的键。
  • 指定用于比较各列的值的逻辑运算符(例如 = 或 <>)。

联接使用以下 Transact-SQL 语法以逻辑方式表示:

  • [ INNER ] JOIN
  • LEFT [ OUTER ] JOIN
  • RIGHT [ OUTER ] JOIN
  • FULL [ OUTER ] JOIN
  • CROSS JOIN

内联接可在 FROMWHERE 子句中指定。 外连接交叉连接只能在FROM子句中指定。 联接条件与 WHEREHAVING 搜索条件相结合,用于控制从 FROM 子句所引用的基表中选定的行。

FROM 子句中指定联接条件有助于将这些联接条件与 WHERE 子句中可能指定的其他任何搜索条件分开,建议用这种方法来指定联接。 简化的 ISO FROM 子句联接语法如下:

FROM first_table < join_type > second_table [ ON ( join_condition ) ]
  • join_type 指定执行的联接类型:内部、外部或交叉联接。 有关不同类型联接的相关说明,请参阅 From 子句
  • join_condition 定义了对每一对联接行进行求值时使用的谓词。

以下代码是 FROM 子句联接规范的示例:

FROM Purchasing.ProductVendor INNER JOIN Purchasing.Vendor
     ON ( ProductVendor.BusinessEntityID = Vendor.BusinessEntityID )

以下代码是一个使用此联接的简单 SELECT 语句:

SELECT ProductID, Purchasing.Vendor.BusinessEntityID, Name
FROM Purchasing.ProductVendor INNER JOIN Purchasing.Vendor
    ON (Purchasing.ProductVendor.BusinessEntityID = Purchasing.Vendor.BusinessEntityID)
WHERE StandardPrice > $10
  AND Name LIKE N'F%';
GO

SELECT 语句会返回某个公司所提供的一组产品以及供应商信息,该公司名以字母 F 开头,并且产品价格在 10 美元以上。

当在单个查询中引用多个表时,所有列引用都必须是明确的。 在上例中,ProductVendorVendor 表都含有名为 BusinessEntityID 的列。 在查询所引用的两个或多个表中,任何重复的列名都必须用表名加以限定。 此示例中对 Vendor 列的所有引用均已限定。

如果查询中使用的两个或多个表之间不存在重复的列名,则引用该列名时不必用表名加以限定。 如上例所示。 这种 SELECT 子句有时难以理解,因为没有任何信息表明每一列来自哪个表。 如果所有的列都用它们的表名加以限定,将会提高查询的可读性。 如果使用了表的别名,将会进一步提高可读性,尤其是当表名自身必须用数据库名和所有者名加以限定时。 下例 Code 与上例相同,只不过分配了表的别名并且用表的别名对列加以限定,从而提高了可读性:

SELECT pv.ProductID, v.BusinessEntityID, v.Name
FROM Purchasing.ProductVendor AS pv
INNER JOIN Purchasing.Vendor AS v
    ON (pv.BusinessEntityID = v.BusinessEntityID)
WHERE StandardPrice > $10
    AND Name LIKE N'F%';

上例是在 FROM 子句中指定联接条件的,这是首选的方法。 下列查询包含相同的联接条件,该联接条件在 WHERE 子句中指定:

SELECT pv.ProductID, v.BusinessEntityID, v.Name
FROM Purchasing.ProductVendor AS pv, Purchasing.Vendor AS v
WHERE pv.BusinessEntityID=v.BusinessEntityID
    AND StandardPrice > $10
    AND Name LIKE N'F%';

联接的 SELECT 列表可以引用联接表中的所有列或任意一部分列。 该 SELECT 列表不需要包含联接中每个表的列。 例如,在三表连接中,只能通过其中一个表将另外两个表中的一个与第三个表连接起来,而且 SELECT 列表不必引用该中间表中的任何列。 这也称为“反半联接”

虽然联接条件通常使用相等比较 (=),但也可以像指定其他谓词一样指定其他比较运算符或关系运算符。 有关详细信息,请参阅 比较运算符WHERE

当 SQL Server 处理联接时,查询优化器从多种可行方法中选择最高效的方法来处理联接。 这包括选择最有效的物理联接类型、表将联接的顺序,甚至使用无法直接用 Transact-SQL 语法表示的逻辑联接作类型,例如 半联接反半联接。 各种联接的物理执行可以使用许多不同的优化,因此无法可靠地预测。 有关半联接和反半联接的详细信息,请参阅 逻辑和物理 Showplan 运算符参考

联接条件中使用的列不需要具有相同的名称或相同数据类型。 但是,如果数据类型不相同,则它们必须兼容,或者是 SQL Server 可以隐式转换的类型。 如果无法隐式转换数据类型,则联接条件必须使用函数显式转换数据类型 CAST 。 有关隐式转换和显式转换的详细信息,请参阅数据类型转换(数据库引擎)。

大多数使用联接的查询可以用子查询(嵌套在其他查询中的查询)重写,并且大多数子查询可以重写为联接。 有关子查询的详细信息,请参阅子查询(SQL Server)。

Note

不能直接基于 ntext、text 或 image 列联接表。 但是,可以通过使用 SUBSTRING 在 ntext、text 或 image 列上将表间接联接起来。 例如,SELECT * FROM t1 JOIN t2 ON SUBSTRING(t1.textcolumn, 1, 20) = SUBSTRING(t2.textcolumn, 1, 20) 可对表 t1t2 中每个文本列的前 20 个字符进行两表内联。 此外,另一种可以采用的比较两个表中 ntext 或 text 列的方法是用 WHERE 子句比较这些列的长度,例如:WHERE DATALENGTH(p1.pr_info) = DATALENGTH(p2.pr_info)

理解嵌套循环联接

如果一个联接输入很小(少于 10 行),而另一个联接输入相当大且其联接列上建有索引,则索引嵌套循环联接是最快的联接操作,因为这种联接操作需要的 I/O 最少,比较次数也最少。

嵌套循环联接也称为嵌套迭代,它将一个联接输入用作外部输入表(显示为图形执行计划中的顶端输入),将另一个联接输入用作内部(底端)输入表。 外层循环逐行读取外层输入表。 内部循环会针对每个外部行执行,在内部输入表中搜索匹配行。

在最简单的情况下,搜索会扫描整个表或索引;这称为朴素嵌套循环连接。 如果查找利用了索引,则称为索引嵌套循环联接。 如果索引作为查询计划的一部分生成(并在查询完成后销毁),则它称为 临时索引嵌套循环联接。 查询优化器考虑了所有这些不同情况。

如果外部输入较小而内部输入较大且预先创建了索引,则嵌套循环联接尤其有效。 在许多小事务中(如那些只影响较小的一组行的事务),索引嵌套循环联接优于合并联接和哈希联接。 但在大型查询中,嵌套循环联接通常不是最佳选择。

当嵌套循环连接运算符的 OPTIMIZED 属性设置为 True 时,这表示当内侧表较大时,会使用优化的嵌套循环(或批量排序)来最大限度地减少 I/O,而无论是否并行化。 由于排序本身是一个隐藏的操作,因此在分析执行计划时,某个计划中是否存在这种优化可能并不容易看出来。 但是,通过查看计划 XML 中的 OPTIMIZED 属性,可以看出嵌套循环联接可能会尝试对输入行重新排序,以提高 I/O 性能。

合并联接

如果两个联接输入的规模都不小,但都已按其联接列排序(例如,这些输入是通过扫描已排序的索引获得的),那么合并联接是最快的联接操作。 如果两个联接输入都很大,而且这两个输入的大小差不多,则预先排序的合并联接提供的性能与哈希联接相近。 但是,如果这两个输入的大小相差很大,则哈希联接操作通常快得多。

合并联接要求两路输入都按合并列排序,而这些合并列由联接谓词中的相等(ON)子句定义。 通常,查询优化器扫描索引(如果在适当的一组列上存在索引),或在合并联接的下面放一个排序运算符。 在极少数情况下,可能会有多个相等条件,但合并列仅取自其中部分现有的相等条件。

由于每个输入都已排序,因此 Merge Join 运算符将从每个输入获取一行并将其进行比较。 例如,对于内连接操作,在行相等时,会返回这些行。 如果它们不相等,则会丢弃低值行,并从该输入获取另一行。 这一过程会重复进行,直到所有行都处理完毕。

合并联接操作可以是常规操作,也可以是多对多操作。 多对多合并联接使用临时表来存储各行数据。 如果每个输入中有重复值,则在处理其中一个输入中的每个重复项时,另一个输入必须重绕到重复项的开始位置。

如果存在驻留谓词,则所有满足合并谓词的行都将对该驻留谓词取值,而只返回那些满足该驻留谓词的行。

合并联接本身速度很快,但如果需要进行排序操作,它可能是一种代价高昂的选择。 然而,如果数据量很大且能够从现有 B 树索引中获得预排序的所需数据,则合并联接通常是最快的可用联接算法。

哈希连接

哈希联接可以有效处理未排序的大型非索引输入。 它们对复杂查询的中间结果很有用,因为:

  • 中间结果不会被建立索引(除非显式保存到磁盘,然后再建立索引),而且通常也不会按查询计划中的下一步操作进行适当排序。
  • 查询优化器只估计中间结果的大小。 由于对于复杂查询,估计可能有很大的误差,因此如果中间结果比预期的大得多,则处理中间结果的算法不仅必须有效而且必须适度弱化。

哈希连接可以减少对反规范化的依赖。 非规范化一般通过减少联接操作获得更好的性能,尽管这样做有冗余之险(如不一致的更新)。 哈希连接减少了进行反规范化的需求。 哈希联接使垂直分区(用单独的文件或索引代表单个表中的几组列)得以成为物理数据库设计的可行选项。

哈希联接有两种输入:生成输入和探测输入。 查询优化器指派这些角色,使两个输入中较小的那个作为生成输入。

哈希联接用于多种类型的集合匹配操作:内联接;左外部联接、右外部联接和全外部联接;左半联接和右半联接;交集;并集;以及差集。 此外,哈希联接的某种变形可以进行重复删除和分组,例如 SUM(salary) GROUP BY department。 这些修改对生成和探测角色只使用一个输入。

以下几节介绍了不同类型的哈希联接:内存中的哈希联接、Grace 哈希联接和递归哈希联接。

内存哈希连接

哈希联接先扫描或计算整个生成输入,然后在内存中生成哈希表。 根据计算得出的哈希键的哈希值,将每行插入哈希存储桶。 如果整个生成输入小于可用内存,则可以将所有行都插入哈希表中。 构建阶段之后是探测阶段。 一次一行地对整个探测输入进行扫描或计算,并为每个探测行计算哈希键的值,扫描相应的哈希存储桶并生成匹配项。

Grace 哈希连接

如果生成输入不适合内存,哈希联接将分几个步骤进行。 这称为“Grace 哈希联接”。 每一步都分为生成阶段和探测阶段。 首先,系统会读取整个构建端输入和探测端输入,并根据哈希键使用哈希函数将其划分到多个文件中。 对哈希键使用哈希函数可以保证任意两个联接记录一定位于相同的文件对中。 因此,联接两个大输入的任务简化为相同任务的多个较小的实例。 然后将哈希联接应用于每对分区文件。

递归哈希联接

如果构建输入非常大,以至于标准外部合并所需的输入需要经过多个合并层级,那么就需要多个分区步骤和多个分区层级。 如果只有某些分区较大,则仅对这些特定分区采用额外的分区步骤。 为了使所有分区步骤尽可能快,采用大规模的异步 I/O 操作,以便单个线程即可让多个磁盘驱动器保持忙碌。

Note

如果构建输入仅比可用内存稍大,则内存哈希联接和 Grace 哈希联接的元素会在单个步骤中结合起来,从而产生混合哈希联接。

在优化期间,并不总是能够确定使用哪个哈希联接。 因此,SQL Server 首先使用内存哈希联接,然后根据生成端输入的大小,逐渐过渡到宽限哈希联接和递归哈希联接。

如果查询优化器错误地预判了两个输入中哪个较小,因此本应由哪个作为生成输入,那么生成端和探测端的角色会动态对调。 哈希联接确保使用较小的溢出文件作为生成输入。 这一技术称为角色反转。 在至少发生一次磁盘溢写之后,哈希联接内部会发生角色互换。

Note

角色反转不受任何查询提示或结构的影响。 角色逆转不会显示在查询计划中;发生时,对用户是透明的。

哈希纾困

术语“哈希补救”有时用来指代 Grace 哈希连接或递归哈希连接。

Note

递归哈希联接或哈希回退会导致服务器性能下降。 如果跟踪中显示许多哈希警告事件,请更新正在联接的列上的统计信息。

有关哈希回退的详细信息,请参阅 Hash Warning 事件类

自适应联接

批处理模式自适应联接允许将选择哈希联接还是嵌套循环联接方法的时机推迟到扫描完第一个输入之后。 自适应联接运算符可定义用于决定何时切换到嵌套循环计划的阈值。 因此,查询计划可在执行期间动态切换到较好的联接策略,而无需进行重新编译。

Tip

频繁在小型和大型联接输入扫描之间波动的工作负载将最能从此功能中受益。

运行时决策基于以下步骤:

  • 如果生成联接输入的行计数足够小,以致于嵌套循环联接优于哈希联接,则计划将切换到嵌套循环算法。
  • 如果构建端联接输入超过特定的行数阈值,则不会发生切换,执行计划将继续使用哈希联接。

以下查询用于演示一个自适应联接示例:

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 行。 启用实时查询统计信息会显示以下计划:

执行计划的屏幕截图,显示最终自适应联接运算符中的查询结果 336 行。

在计划中,请注意以下事项:

  1. 用于为哈希联接生成阶段提供行的列存储索引扫描。
  2. 新的自适应联接运算符。 此运算符可定义用于决定何时切换到嵌套循环计划的阈值。 对于此示例,阈值为 78 行。 包含 >= 78 行的任何示例均将使用哈希联接。 如果小于阈值,将使用嵌套循环联接。
  3. 由于查询返回 336 行,超过了阈值,因此,第二个分支表示标准哈希联接操作的探测阶段。 实时查询统计信息显示流经各个运算符的行数——在本例中为“672/672”。
  4. 并且,最后一个分支是供未超出阈值的嵌套循环联接使用的聚集索引查找。 我们看到显示为“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;

查询将返回一行。 启用“实时查询统计信息”会显示以下计划:

执行计划的截图,显示最终的自适应联接为一行。

在计划中,请注意以下事项:

  • 返回了一行数据后,聚集索引查找现在已有数据行流经该运算符。
  • 由于哈希联接的构建阶段没有继续进行,因此不会有任何行流经第二个分支。

自适应联接备注

自适应联接引入了比索引嵌套循环联接等效计划更高的内存要求。 它会请求额外的内存,就像嵌套循环属于哈希联接一样。 构建阶段还会产生额外开销,因为它是停止-继续式操作,而等效的嵌套循环联接则是流式处理。 这项额外成本也带来了灵活性,适用于构建输入中的行数会发生变化的场景。

批处理模式自适应联接在语句首次执行时生效,编译完成后,后续连续执行将根据已编译的自适应联接阈值以及外部输入在生成阶段流过的运行时行数,继续保持自适应。

如果自适应联接切换为嵌套循环操作,它将使用哈希联接构建阶段已读取的行。 运算符不会再次重新读取外部引用行。

跟踪自适应联接活动

自适应联接运算符具有以下计划运算符属性:

计划属性 Description
AdaptiveThresholdRows 显示用于从哈希联接切换到嵌套循环联接的阈值。
EstimatedJoinType 可能的联接类型。
ActualJoinType 在实际计划中,显示根据阈值最终选择的联接算法。

估计的计划显示自适应联接计划形状,以及定义的自适应联接阈值和估计的联接类型。

Tip

查询存储可捕获并强制执行批处理模式自适应联接计划。

符合自适应联接条件的语句

以下多个条件可使逻辑联接符合批处理模式自适应联接的条件:

  • 数据库兼容性级别为 140 或更高级别。
  • 查询是 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'));

Note

USE HINT 查询提示的优先级高于数据库作用域配置或跟踪标志设置。

NULL 值和联接

如果联接的表的列中存在 null 值,则 null 值不匹配。 如果其中一个联接表的列中出现空值,只能通过外部联接返回这些空值(除非 WHERE 子句不包括空值)。

以下两个表中,各自在将参与联接的列中包含 NULL

table1                          table2
a           b                   c            d
-------     ------              -------      ------
      1        one                 NULL         two
   NULL      three                    4        four
      4      join4

将列 a 中的值与列 c 中的值进行比较的联接,无法在值为 NULL 的列上获得匹配:

SELECT *
FROM table1 t1 JOIN table2 t2
   ON t1.a = t2.c
ORDER BY t1.a;
GO

只返回列 ac 中值为 4 的一行:

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;
GO

结果集如下。

a           b      c           d
----------- ------ ----------- ------
NULL        three  NULL        NULL
1           one    NULL        NULL
4           join4  4           four

(3 row(s) affected)

这些结果使得人们难以区分数据中的 NULL 与表示联接失败的 NULL。 当待联接的数据中存在 NULL 值时,通常最好使用常规联接将这些值从结果中省略。