联接 (SQL Server)

SQL Server 使用内存中的排序和哈希联接技术执行排序、交集、并集、差分等操做。 SQL Server 利用这种类型的查询计划支持垂直表分区(有时称为分列存储)。算法

SQL Server 使用三种类型的联接操做:sql

  • 嵌套循环联接
  • 合并联接
  • 哈希联接

联接基础知识

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

联接条件可经过如下方式定义两个表在查询中的关联方式:服务器

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

能够在 FROM 或 WHERE 子句中指定内部联接。 只能在 FROM 子句中指定外部联接。 联接条件与 WHERE 和 HAVING 搜索条件相结合,用于控制从 FROM 子句所引用的基表中选定的行。异步

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

FROM first_table join_type second_table [ON (join_condition)]

join_type 指定执行的联接类型:内部、外部或交叉联接。 join_condition 定义用于对每一对联接行进行求值的谓词。 下面是 FROM 子句联接规范示例:函数

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

下面是使用此联接的一个简单 SELECT 语句:oop

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

此选择会返回某个公司所提供的一组产品以及供应商信息,该公司名以字母 F 开头,而且产品价格在 10 美圆以上。性能

当在单个查询中引用多个表时,全部列引用都必须是明确的。 在上面的示例中,ProductVendor 和 Vendor 表都具备一个名为 BusinessEntityID 的列。 在查询所引用的两个或多个表中,任何重复的列名都必须用表名加以限定。 此示例中对 Vendor 列的全部引用均已限定。优化

若是某个列名在查询用到的两个或多个表中不重复,则对该列的引用就必用表名加以限定。 如上例所示。 因为没有指明提供每一个列的表,所以这样的 SELECT 语句有时会难以理解。 若是全部的列都用它们的表名加以限定,将会提升查询的可读性。 若是使用了表的别名,将会进一步提升可读性,尤为是当表名自身必须用数据库名和全部者名加以限定时。 下例与上例相同,只不过度配了表的别名而且用表的别名对列加以限定,从而提升了可读性:

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

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

SQL
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%'; 

联接选择列表能够引用联接表中的全部列或任意一部分列。 选择列表没必要包含联接中每一个表的列。 例如,在三表联接中,只能用一个表做为中间表来联接另外两个表,而选择列表没必要引用该中间表的任何列。

虽然联接条件一般使用相等比较 (=),但也能够像指定其余谓词同样指定其余比较运算符或关系运算符。 有关详细信息,请参阅比较运算符 (Transact-SQL) 和 WHERE (Transact-SQL)

当 SQL Server 处理联接时,查询引擎会从多种可行的方法中选择最有效的方法来处理联接。 因为各类联接的实际执行过程会采用多种不一样的优化,所以没法可靠地预测。

联接条件中用到的列没必要具备相同的名称或相同的数据类型。 但若是数据类型不相同,则必须兼容,或者是可由 SQL Server 进行隐式转换的类型。 若是数据类型不能进行隐式转换,则联接条件必须使用 CAST 函数显式转换数据类型。 有关隐式和显式转换的详细信息,请参阅数据类型转换(数据库引擎)

大多数使用联接的查询能够用子查询(嵌套在其余查询中的查询)重写,而且大多数子查询能够重写为联接。 有关子查询的详细信息,请参阅子查询

 备注

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

了解嵌套循环联接

若是一个联接输入很小(不到 10 行),而另外一个联接输入很大并且已在其联接列上建立了索引,则索引 Nested Loops 链接是最快的联接操做,由于它们须要的 I/O 和比较都最少。

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

最简单的状况是,搜索时扫描整个表或索引;这称为单纯嵌套循环联接。 若是搜索时使用索引,则称为索引嵌套循环联接。 若是将索引生成为查询计划的一部分(并在查询完成后当即将索引破坏),则称为临时索引嵌套循环联接。 查询优化器考虑了全部这些不一样状况。

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

了解合并联接

若是两个联接输入并不小但已在两者联接列上排序(例如,若是它们是经过扫描已排序的索引得到的),则合并联接是最快的联接操做。 若是两个联接输入都很大,并且这两个输入的大小差很少,则预先排序的合并联接提供的性能与哈希联接相近。 可是,若是这两个输入的大小相差很大,则哈希联接操做一般快得多。

合并联接要求两个输入都在合并列上排序,而合并列由联接谓词的等效 (ON) 子句定义。 一般,查询优化器扫描索引(若是在适当的一组列上存在索引),或在合并联接的下面放一个排序运算符。 在极少数状况下,虽然可能有多个等效子句,但只用其中一些可用的等效子句得到合并列。

因为每一个输入都已排序,所以 Merge Join 运算符将从每一个输入获取一行并将其进行比较。 例如,对于内联接操做,若是行相等则返回。 若是行不相等,则废弃值较小的行并从该输入得到另外一行。 这一过程将重复进行,直处处理完全部的行为止。

合并联接操做能够是常规操做,也能够是多对多操做。 多对多合并联接使用临时表存储行。 若是每一个输入中有重复值,则在处理其中一个输入中的每一个重复项时,另外一个输入必须重绕到重复项的开始位置。

若是存在驻留谓词,则全部知足合并谓词的行都将对该驻留谓词取值,而只返回那些知足该驻留谓词的行。

合并联接自己的速度很快,但若是须要排序操做,选择合并联接就会很是费时。 然而,若是数据量很大且可以从现有 B 树索引中得到预排序的所需数据,则合并联接一般是最快的可用联接算法。

了解哈希联接

哈希联接能够有效处理未排序的大型非索引输入。 它们对复杂查询的中间结果颇有用,由于:

  • 中间结果未经索引(除非已经显式保存到磁盘上而后建立索引),并且一般不为查询计划中的下一个操做进行适当的排序。
  • 查询优化器只估计中间结果的大小。 因为对于复杂查询,估计可能有很大的偏差,所以若是中间结果比预期的大得多,则处理中间结果的算法不只必须有效并且必须适度弱化。

哈希联接能够减小使用非规范化。 非规范化通常经过减小联接操做得到更好的性能,尽管这样作有冗余之险(如不一致的更新)。 哈希联接则减小使用非规范化的须要。 哈希联接使垂直分区(用单独的文件或索引表明单个表中的几组列)得以成为物理数据库设计的可行选项。

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

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

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

内存中的哈希联接

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

Grace 哈希联接

若是生成输入大于内存,哈希联接将分为几步进行。 这称为“Grace 哈希联接”。 每一步都分为生成阶段和探测阶段。 首先,消耗整个生成和探测输入并将其分区(使用哈希键上的哈希函数)为多个文件。 对哈希键使用哈希函数能够保证任意两个联接记录必定位于相同的文件对中。 所以,联接两个大输入的任务简化为相同任务的多个较小的实例。 而后将哈希联接应用于每对分区文件。

递归哈希联接

若是生成输入很是大,以致于标准外部合并的输入须要多个合并级别,则须要多个分区步骤和多个分区级别。 若是只有某些分区较大,则只需对那些分区使用附加的分区步骤。 为了使全部分区步骤尽量快,将使用大的异步 I/O 操做以便单个线程就能使多个磁盘驱动器繁忙工做。

 备注

若是生成输入仅稍大于可用内存,则内存中的哈希联接和 Grace 哈希联接的元素将结合在一个步骤中,生成混合哈希联接。

在优化过程当中不能始终肯定使用哪一种哈希联接。 所以,SQL Server 开始时使用内存中的哈希联接,而后根据生成输入的大小逐渐转换到 Grace 哈希联接和递归哈希联接。

若是查询优化器错误地预计两个输入中哪一个较小并由此肯定哪一个做为生成输入,生成角色和探测角色将动态反转。 哈希联接确保使用较小的溢出文件做为生成输入。 这一技术称为角色反转。 至少一个文件溢出到磁盘后,哈希联接中才会发生角色反转。

 备注

角色反转的发生独立于任何查询提示或结构。 角色反转不会显示在查询计划中;角色反转对于用户是透明的。

哈希援助

术语“哈希援助”有时用于描述 Grace 哈希联接或递归哈希联接。

 备注

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

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

NULL 值和联接

联接表的列中的 null 值(若是有)互相不匹配。 若是其中一个联接表的列中出现空值,只能经过外部联接返回这些空值(除非 WHERE子句不包括空值)。

下面的两个表中,每一个表中要参与联接的列中均包含 NULL 值:

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

将列 a 中的值与列 c 中的值进行比较的联接在包含 NULL 值的列上不会得到匹配项:

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

而是只返回列 a 和 c 中具备 4 的一行:

a           b      c           d      
----------- ------ ----------- ------ 
4           join4  4           four   

(1 row(s) affected)

另外,从基表返回的空值与从外部联接返回的空值很难区分开。 例如,下面的 SELECT 语句对这两个表执行左向外部联接:

SQL
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 值。 若是联接的数据有空值,最好用常规联接从结果中删除这些空值。

相关文章
相关标签/搜索