今天我將介紹在SQLServer 中的三種連接操作符類型,分別是:循環嵌套、哈希匹配和合並連接。主要對這三種連接的不同、復雜度用范例的形式一一介紹。
本文中使用了示例數據庫AdventureWorks ,下面是下載地址:http://msftdbprodsamples.codeplex.com/releases/view/4004
簡介:什么是連接操作符
連接操作符是一種算法類型,它是SQLServer優化器為了實現兩個數據集合之間的邏輯連接選擇的操作符。優化器可以基於請求查詢、可用索引、統計信息和估計行數等不同的場景為每一套數據選擇不同的算法
通過查看執行計划可以發現操作符如何被使用。接下來我們看一下如何具體使用。
NESTED LOOPS(循環嵌套)
我們通過下面的例子來展示一下(查詢2001年7月份的數據):
SELECT OH.OrderDate, OD.OrderQty, OD.ProductID, OD.UnitPrice FROM Sales.SalesOrderHeader AS OH JOIN Sales.SalesOrderDetail AS OD ON OH.SalesOrderID = OD.SalesOrderID WHERE OH.OrderDate BETWEEN '2001-07-01' AND '2001-07-31'
執行計划的結果如下:
圖右上方的叫“outer input”,在其下面的叫做“inner input”
本質上講,“Nested Loops”操作符就是:為每一個記錄的外部輸入找到內部輸入的匹配行。
技術上講,這意味着外表聚集索引被掃描獲取外部輸入相關的記錄,然后內表聚集索引查找每一個匹配外表索引的記錄。
我們可以通過把鼠標放在聚集索引掃描操作符上面來驗證這個信息,請看這個tooltip:
看這個執行的估計行數是1,索引查找tooltip如下:
這次發現執行的估計行數是179,這是很接近返回的外部輸入行的。
按照復雜度計算(假設N是外部輸出的行數,M是總行數在SalesOrderDetai表的):查詢復雜度是O(NlogM),這里logM是在內部輸入表的每次查找的復雜度。
當外部輸入比較小並且內部輸入有索引在連接的字段上的時候SQLServer 優化器更喜歡選擇這種操作符類型(Nested Loop)。外部和內部輸入的數據行差距越大,這個操作符提供的性能越高。
MERGE Join(合並連接)
“Merge”算法是連接兩個較大且按序存儲的在連接鍵上最有效的方式。請看一下下面這個查詢例子(查詢返回用戶和銷售表的ID):
SELECT OC.CustomerID, OH.SalesOrderID FROM Sales.SalesOrderHeader AS OH JOIN Sales.Customer AS OC ON OH.CustomerID = OC.CustomerID
查詢執行計划如下:
- 首先我們注意到兩套數據是在CustomerID上是有序的:因為聚集索引是有序的且在SalesorderHeader表上該字段是非聚集索引。
- 根據在操作符的箭頭(鼠標放在上面),我們能看到每個返回結果行數都是很大的。
- 除此之外,在On 的子句后面要用=操作符。
就是這三個因素會導致優化器選擇Merge Join查詢操作符。
使用這種連接操作符的最大的性能就是兩個輸入操作符執行一次。我們能把鼠標放在兩個數據的上面看一下執行的次數都是1,也就是說算法是很有效率的。
合並連接同時讀取兩個輸入然后比較他們。如果匹配就返回,否則,行數較小的被放棄,因為兩邊輸入是有序的。放棄的行不再匹配任何行。
知道其中一個表完畢一直重復匹配,即使另一個表還有數據,那么最大的時間復雜的消耗就是兩個表完全不同鍵值,那么最大的復雜度就是: O(N+M)。
HASH Match(哈希匹配)
“Hash”連接是我們稱為 “the go-to guy” 的操作符。當其他連接操作符都不支持的場景時,就會選擇這種操作符。比如當表恰好不排序,或者沒有索引時。當優化器選擇這種操作符,一般來說可能是我們沒有做好一些基礎工作(例如,加索引)。但是有些情況(復雜查詢)沒有別的方式,只能選擇它。
請看下面這個查詢(獲取contacts 表中姓和名中以“John”開始的包含銷售的ID字段的數據集):
SELECT OC.FirstName, OC.LastName, OH.SalesOrderID FROM Sales.SalesOrderHeader AS OH JOIN Person.Contact AS OC ON OH.ContactID = OC.ContactID WHERE OC.FirstName LIKE 'John%'
The execution plan looks like this:
由於ContactID列沒有索引,所以選擇哈希操作符。
在深入理解這個例子之前,介紹兩個重要的概念:一個是“Hashing”函數,一個是“Hash Table”。
函數是一個程序性函數,它接收1或者多個值然后轉換他們為一個符號值(通常是數字)。這個函數通常是單向的,意味着不能反轉回來原始值,但是確定性保證如果你提供了相同的值,符號值是完全一樣的。也就是說,幾個不同的輸入值,可以有相同的Hash值。
“Hash Table”是一個數據結構,把所有行都放到一個相同尺寸的桶里面。每一個桶代表一個哈希值。這意味着當你激活函數的行,使用結果你就會知道它屬於哪個桶。
利用統計信息,SQLServer 會選擇較小的兩個數據輸入來提供構造輸入,並且輸入被用來在內存中創建哈希表。如果沒有足夠的內存,在tempdb中會使用物理磁盤。在哈希表建立后,SQLServer將從較大的表中得到數據,叫做探測輸入。利用哈希匹配函數與哈希表比較,然后返回匹配行。在圖形執行計划中,構造輸入的查詢在上面,探測輸入的查詢在下面。
只要較小的表非常小,這個算法就是非常有效的。但是如果兩個表都非常大,這可能是非常低效的執行計划。
查詢Hints
利用Hints,破事SQLServer使用指定的連接類型。但是強烈不推薦這么做,尤其在生產環境,因為沒有永恆的最佳選擇(因為數據在變化),並且優化器通常是正確的。
添加OPTION 子句作為查詢的結尾,使用關鍵字LOOP JOIN, MERGE JOIN 或者 HASH JOIN可以強制執行連接。
看看如何實現:
SELECT OC.CustomerID, OH.SalesOrderID FROM Sales.SalesOrderHeader AS OH JOIN Sales.Customer AS OC ON OH.CustomerID = OC.CustomerID OPTION (HASH JOIN) SELECT OC.FirstName, OC.LastName, OH.SalesOrderID FROM Sales.SalesOrderHeader AS OH JOIN Person.Contact AS OC ON OH.ContactID = OC.ContactID WHERE OC.FirstName LIKE 'John%' OPTION (LOOP JOIN) SELECT OC.FirstName, OC.LastName, OH.SalesOrderID FROM Sales.SalesOrderHeader AS OH JOIN Person.Contact AS OC ON OH.ContactID = OC.ContactID WHERE OC.FirstName LIKE 'John%' OPTION (MERGE JOIN)
總結
Nested Loops
- 復雜度: O(NlogM)。
- 其中一個表很小的時候。
- 較大的表允許使用索引查找連接字段。
Merge Join
- 復雜度: O(N+M)。
- 兩個輸入的連接字段是有序的。
- 使用=操作符
- 適合非常大的表
Hash Match
- 復雜度: O(N*hc+M*hm+J)
- 最后默認的操作符
- 使用哈希表和動態哈希匹配函數匹配行
本篇隨筆詳細介紹了三種鏈接操作符和它們的觸發機制,當然這些也都是動態的,就像前面說的沒有最佳的操作符,只有最合適的,要根據實際請款選擇不同的操作符。





