聯接 (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 值。 若是聯接的數據有空值,最好用常規聯接從結果中刪除這些空值。

相關文章
相關標籤/搜索