http://msdn.microsoft.com/zh-cn/library/dn144699.aspx
SQL Server三種表連接原理
013年2月
簡介
在SQL Server中,我們所常見的表與表之間的Inner Join,Outer Join都會被執行引擎根據所選的列,數據上是否有索引,所選數據的選擇性轉化為Loop Join,Merge Join,Hash Join這三種物理連接中的一種。理解這三種物理連接是理解在表連接時解決性能問題的基礎,下面我來對這三種連接的原理,適用場景進行描述。
嵌套循環連接(Nested Loop Join)
循環嵌套連接是最基本的連接,正如其名所示那樣,需要進行循環嵌套,嵌套循環是三種方式中唯一支持不等式連接的方式,這種連接方式的過程可以簡單的用下圖展示:
圖1.循環嵌套連接的第一步
圖2.循環嵌套連接的第二步
由上面兩個圖不難看出,循環嵌套連接查找內部循環表的次數等於外部循環的行數,當外部循環沒有更多的行時,循環嵌套結束。另外,還可以看出,這種連接方式需要內部循環的表有序(也就是有索引),並且外部循環表的行數要小於內部循環的行數,否則查詢分析器就更傾向於Hash Join(會在本文后面講到)。
通過嵌套循環連接也可以看出,隨着數據量的增長這種方式對性能的消耗將呈現出指數級別的增長,所以數據量到一定程度時,查詢分析器往往就會采用這種方式。
下面我們通過例子來看一下循環嵌套連接,利用微軟的AdventureWorks數據庫:
圖3.一個簡單的嵌套循環連接
圖3中ProductID是有索引的,並且在循環的外部表中(Product表)符合ProductID=870的行有4688條,因此,對應的SalesOrderDetail表需要查找4688次。讓我們在上面的查詢中再考慮另外一個例子,如圖4所示。
圖4.額外的列帶來的額外的書簽查找
由圖4中可以看出,由於多選擇了一個UnitPrice列,導致了連接的索引無法覆蓋所求查詢,必須通過書簽查找來進行,這也是為什么我們要養成只Select需要的列的好習慣,為了解決上面的問題,我們既可以用覆蓋索引,也可以減少所需的列來避免書簽查找。另外,上面符合ProductID的行僅僅只有5條,所以查詢分析器會選擇書簽查找,假如我們將符合條件的行進行增大,查詢分析器會傾向於表掃描(通常來說達到表中行數的1%以上往往就會進行table scan而不是書簽查找,但這並不絕對),如圖5所示。
圖5.查詢分析器選擇了表掃描
可以看出,查詢分析器此時選擇了表掃描來進行連接,這種方式效率要低下很多,因此好的覆蓋索引和Select *都是需要注意的地方。另外,上面情況即使涉及到表掃描,依然是比較理想的情況,更糟糕的情況是使用多個不等式作為連接時,查詢分析器即使知道每一個列的統計分布,但卻不知道幾個條件的聯合分布,從而產生錯誤的執行計划,如圖6所示。
圖6.由於無法預估聯合分布,導致的偏差
由圖6中,我們可以看出,估計的行數和實際的行數存在巨大的偏差,從而應該使用表掃描但查詢分析器選擇了書簽查找,這種情況對性能的影響將會比表掃描更加巨大。具體大到什么程度呢?我們可以通過強制表掃描和查詢分析器的默認計划進行比對,如圖7所示。
圖7.強制表掃描性能反而更好
合並連接(Merge Join)
談到合並連接,我突然想起在西雅圖參加SQL Pass峰會晚上酒吧排隊點酒,由於我和另外一哥們站錯了位置,貌似我們兩個在插隊一樣,我趕緊說:I’m sorry,i thought here is end of line。對方無不幽默的說:”It’s OK,In SQL Server,We called it merge join”。
由上面的小故事不難看出,Merge Join其實上就是將兩個有序隊列進行連接,需要兩端都已經有序,所以不必像Loop Join那樣不斷的查找循環內部的表。其次,Merge Join需要表連接條件中至少有一個等號查詢分析器才會去選擇Merge Join。
Merge Join的過程我們可以簡單用下面圖進行描述:
圖8.Merge Join第一步
Merge Join首先從兩個輸入集合中各取第一行,如果匹配,則返回匹配行。加入兩行不匹配,則有較小值的輸入集合+1,如圖9所示。
圖9.更小值的輸入集合向下進1
用C#代碼表示Merge Join的話如代碼1所示。
public class MergeJoin { // Assume that left and right are already sorted public static Relation Sort(Relation left, Relation right) { Relation output = new Relation(); while (!left.IsPastEnd() && !right.IsPastEnd()) { if (left.Key == right.Key) { output.Add(left.Key); left.Advance(); right.Advance(); } else if (left.Key < right.Key) left.Advance(); else //(left.Key > right.Key) right.Advance(); } return output; } }
代碼1.Merge Join的C#代碼表示
因此,通常來說Merge Join如果輸入兩端有序,則Merge Join效率會非常高,但是如果需要使用顯式Sort來保證有序實現Merge Join的話,那么Hash Join將會是效率更高的選擇。但是也有一種例外,那就是查詢中存在order by,group by,distinct等可能導致查詢分析器不得不進行顯式排序,那么對於查詢分析器來說,反正都已經進行顯式Sort了,何不一石二鳥的直接利用Sort后的結果進行成本更小的MERGE JOIN?在這種情況下,Merge Join將會是更好的選擇。
另外,我們可以由Merge Join的原理看出,當連接條件為不等式(但不包括!=),比如說> < >=等方式時,Merge Join有着更好的效率。
下面我們來看一個簡單的Merge Join,這個Merge Join是由聚集索引和非聚集索引來保證Merge Join的兩端有序,如圖10所示。
圖10.由聚集索引和非聚集索引保證輸入兩端有序
當然,當Order By,Group By時查詢分析器不得不用顯式Sort,從而可以一箭雙雕時,也會選擇Merge Join而不是Hash Join,如圖11所示。
圖11.一箭雙雕的Merge Join
哈希匹配(Hash Join)
哈希匹配連接相對前面兩種方式更加復雜一些,但是哈希匹配對於大量數據,並且無序的情況下性能均好於Merge Join和Loop Join。對於連接列沒有排序的情況下(也就是沒有索引),查詢分析器會傾向於使用Hash Join。
哈希匹配分為兩個階段,分別為生成和探測階段,首先是生成階段,第一階段生成階段具體的過程可以如圖12所示。
圖12.哈希匹配的第一階段
圖12中,將輸入源中的每一個條目經過散列函數的計算都放到不同的Hash Bucket中,其中Hash Function的選擇和Hash Bucket的數量都是黑盒,微軟並沒有公布具體的算法,但我相信已經是非常好的算法了。另外在Hash Bucket之內的條目是無序的。通常來講,查詢優化器都會使用連接兩端中比較小的哪個輸入集來作為第一階段的輸入源。
接下來是探測階段,對於另一個輸入集合,同樣針對每一行進行散列函數,確定其所應在的Hash Bucket,在針對這行和對應Hash Bucket中的每一行進行匹配,如果匹配則返回對應的行。
通過了解哈希匹配的原理不難看出,哈希匹配涉及到散列函數,所以對CPU的消耗會非常高,此外,在Hash Bucket中的行是無序的,所以輸出結果也是無序的。圖13是一個典型的哈希匹配,其中查詢分析器使用了表數據量比較小的Product表作為生成,而使用數據量大的SalesOrderDetail表作為探測。
圖13.一個典型的哈希匹配連接
上面的情況都是內存可以容納下生成階段所需的內存,如果內存吃緊,則還會涉及到Grace哈希匹配和遞歸哈希匹配,這就可能會用到TempDB從而吃掉大量的IO。這里就不細說了,有興趣的同學可以移步:http://msdn.microsoft.com/zh-cn/library/aa178403(v=SQL.80).aspx。
總結
下面我們通過一個表格簡單總結這幾種連接方式的消耗和使用場景:
嵌套循環連接 |
合並連接 |
哈希連接 |
|
適用場景 |
外層循環小,內存循環條件列有序 |
輸入兩端都有序 |
數據量大,且沒有索引 |
CPU |
低 |
低(如果沒有顯式排序) |
高 |
內存 |
低 |
低(如果沒有顯式排序) |
高 |
IO |
可能高可能低 |
低 |
可能高可能低 |
理解SQL Server這幾種物理連接方式對於性能調優來說必不可少,很多時候當篩選條件多表連接多時,查詢分析器就可能不是那么智能了,因此理解這幾種連接方式對於定位問題變得尤為重要。此外,我們也可以通過從業務角度減少查詢范圍來減少低下性能連接的可能性。
參考文獻:
http://msdn.microsoft.com/zh-cn/library/aa178403(v=SQL.80).aspx
http://www.dbsophic.com/SQL-Server-Articles/physical-join-operators-merge-operator.html
在SQL Server數據庫中,查詢優化器在處理表連接時,通常會使用一下三種連接方式:
- 嵌套循環連接(Nested Loop Join)
- 合並連接 (Merge Join)
- Hash連接 (Hash Join)
充分理解這三種表連接工作原理,可以使我們在優化SQL Server連接方面的代碼有據可依,為開展優化工作提供一定的思路。接下來我們來認識下這三種連接。
1. 嵌套循環連接(Nested Loop Join)
該連接方式通常在小數據量並且語句比較簡單的場景中使用,也是比較常見的連接方式,比如以下示例:
1: use AdventureWorks2008
2: go
3: SELECT H.*
4: FROM Sales.SalesOrderHeader H
5: JOIN Sales.Sale
1: use AdventureWorks2008
sOrderDetail D
6: ON H.SalesOrderID=D.SalesOrderID
7: WHERE H.SalesOrderID = 43659
AdventureWorks2008數據庫是SQL Server的一個sample,你可以在微軟官方網站上自由下載。http://msftdbprodsamples.codeplex.com/releases/view/37109
我們在數據庫中運行這段代碼:
通過執行計划我們可以看到,數據庫的優化器使用了嵌套連接(Neasted Loops),上面第一行中的Sales.SalesOrderHeader表因為只有一行數據所以做為外部表使用,SalesOrderDetail有12行數據做為內部表使用。
嵌套循環的工作原理如圖所示:
圖1 嵌套循環工作原理圖
其原理就是根據條件從表中過濾出一個外部鏈接表,循環的從外部表中讀取一行數據,去內部表中進行匹配,偽碼如下:
For (i=0;i< Number of outerTable Row;i++)
{
OuterTable[i] connect InnerTable[1,2.....N] To Create New Row
WHERE OuterTable[i].data.value = OuterTable[1,2.....N].data.Value
}
了解嵌套的工作原理后,我們不難發現,這種連接的方式具有一定的局限性的:
1. 因為算法是循環進行的,所以比較適合數據量較小的表進行連接,尤其是外部表的數據。
2. 兩張表最好是排序的。表中的條件列和連接列最好有索引,尤其是內部表必須有索引,這樣工作效率會成倍增加。
當外部表較小,而內部表較大並且連接字段上有索引的情況下,循環嵌套非常高效。並且嵌套循環是三種方式中唯一支持不等式連接的方式。
2. 合並連接 (Merge Join)
在SQL Server數據庫中,如果查詢優化器,發現要連接的兩張對象表,在連接列上都已經排序並包含索引,那么優化器將會極大可能選擇“合並”連接策略。條件是:兩個表都是排序的,並且表連接條件中至少有一個等號連接,查詢分析器會去選擇合並連接。
代碼示例:
1: USE AdventureWorks2008
2:
3: GO
4:
5: SELECT P.*
6:
7: FROM Production.ProductModel P
8:
9: JOIN Production.ProductModelProductDescriptionCulture PPMD
10:
11: ON P.ProductModelID = PPMD.ProductModelID
根據執行計划我們可以看到,這次的連接操作使用的合並連接:
這兩張表中,數據量分別為128和762行數據,連接列是表中的主鍵並且數據是有序的,因此數據庫的查詢優化器自動選擇了合並連接。合並連接的工作原理如下圖所示:
圖2 合並連接的工作原理
數據庫優化器在決定使用合並連接后,並行的在兩個表(術語叫輸入集合)中各取第一行數據,進行匹配,匹配則返回匹配行並進行連接。如果不匹配,那么小的那一個表(輸入集合),則順序取下一行數據繼續嘗試匹配。
通過其工作原理我們可以發現,合並連接可以看成是一個類似於並發工作機制。操作分別在兩個表(輸入集合)依次獲取數據並進行比較,這就要求兩張表是有序的,有序的排列會極大的提高工作的效率。
有關表排序的問題,如果連接語句中使用Sort關鍵字來排序數據表,那么SQL Server的優化器會比較傾向於Hash Join。在合並連接中,並不排斥order by, group by, distinct等關鍵字,在使用這些語句時,查詢優化器也有極大的可能選擇合並連接。
當我們使用一些查詢限定條件,比如不等式(>,<,>=等)限定條件范圍,那么合並連接的效率會有更好。
合並連接的限定條件:
1. 兩張表的連接列需要排序
2. 連接列必須有索引
3. 哈希連接(Hash Join)
當我們嘗試將兩張數據量較大,沒有排序和索引的兩張表進行連接時,SQL Server的查詢優化器會嘗試使用Hash Join。
代碼示例:
1: SELECT *
2:
3: FROM Production.Product P
4:
5: Join Production.ProductSubcategory SPC
6:
7: on P.ProductSubcategoryID = SPC.ProductSubcategoryID
根據執行計划我們可以看到,這次的連接操作使用的哈希連接:
該連接在處理大量無序的數據時,效率較高,但是對處理器和內存資源的消耗較大。實現過程如下:
Hash Join連接的執行操作分為兩個階段,建立和探測。
建立是指對輸入表進行的一系列的操作。首先優化器會將輸入表中的每一行數據掃描到系統內存中,然后根據內置的散列算法計算出相應散列值,相同散列值的數據會被分到一個Hash池中。這些散列值和數據地址保存在一個Hash表中,提供給探測使用。通常優化器會選擇數據較少的表作為建立輸入表。
建立完成后,開始探查工作。另一個連接表(我們叫探查輸入)同樣會被逐行的掃描、計算,得出一個Hash值。連接操作會使用探查輸入的Hash值和建立輸入的Hash值列表進行掃描和匹配工作,最終建立連接。
上圖是Hash連接的工作流程,接下來我們可以來了解下哈希算法的實現的機制,以下的內容是個人對算法的理解,若有偏頗請指正。
Hash的實際含義是“散列”的意思,它主要的功能就是將一組數據,通過算法,變換成固定長度的輸出,這個輸出我們就稱之為散列值(Hash值),通常在安全領域,如密碼學中使用較多。
在SQL Server里面哈希散列函數是黑盒的,沒有具體的算法可以參考。實際上很多開發人員在解決海量數據查詢的時候,都會采用Hash方式,並且開發適合需求的散列算法。常用的一些算法包括一些取余、MD2、MD4、MD5 和 SHA-1等等。
因為算法,不同的數據可能會生成相同的散列值。它將大量的數據按照規則分散到不同數據堆或者鏈表中,建立內部的映射關系。我們可以認為他是將數組和鏈表結合在一起,想要達到一種尋址容易、插入刪除方便的數據結構,而Hash表就是一種數據內容和數據存放地址之間的映射關系。
散列函數的選擇會決定影響Hash表元數量大小和每個鍵值包含的數據多少,這個是數學上的問題這里不進行進一步討論。
說到這里,可能大家還是不太理解,我們這里舉例來說明:
比如說有兩張表:
表A{A,F,C,D,B,E……}
表B{F,B,E,D,A,F…….}
並且表A的數據量小於表B,這兩張表進行Hash連接的過程如下:
1. 首先數據庫會將表A中的所有數據,掃描存入內存中。
2. 內存中的表A的數據,經過散列函數依次得到對應的散列值(Hash值)。
3. 表A中相同散列值(鍵值)的數據,會統一的放入到一個Hash池中。個人認為Hash池中的數據,就是數組和鏈表的集合。Hash的鍵值可以看到是一個數組的下標,而池中的數據以鏈表的形式連接在數組中。
Hash【鍵值】-->數據1-->數據2..............
如圖中的一組數據,數據A和數據C具有相同的Hash值,值為001,那么他們都被分配到以001命名的Hash池中。
4. 將Hash值和對應的數據,依次存入到一個Hash表中,建立結束。
5. 探測階段,數據庫依次讀取掃描表B中的每一行數據,並通過散列函數計算出一個Hash值。
6. 根據Hash值,去Hash表中和表A的鍵值進行匹配,找到對應的Hash池。
7. 接下來將表B的數據去和對應的Hash池中的每條數據,去對比和匹配。如果匹配成功則進行數據連接。
通過對原理的了解,我們可以看到這種連接方式,需要大量的計算操作,對CPU帶來一定的壓力。通常Hash 連接操作在內存中進行,如果內存不足,數據庫會將數據寫入到硬盤中,影響性能。
4.小結
三種連接方式的特點:
類型 |
連接列上索引 |
表的大小 |
排序 |
連接子句 |
嵌套 |
內部表:必須 外部表:有最好 |
小 |
可選 |
所有類型 |
合並 |
內部表:必須 聚簇索引或者覆蓋索引 外部表:必須 聚簇索引或者覆蓋索引 |
大 |
需要 |
Equi-join |
HASH |
內部表:不需要 外部表:可選,最好有 小的外部表,大得內部表 |
任意 |
不需要 |
Equi-join |
三種方式對資源的壓力:
嵌套循環連接 |
合並連接 |
哈希連接 |
|
CPU |
低 |
低(如果沒有顯式排序) |
高 |
內存 |
低 |
低(如果沒有顯式排序) |
高 |
IO |
可能高可能低 |
低 |
可能高可能低 |
以上是個人對三種連接的個人理解,不當之處請指正。
題外話:
其實我們可以把這三種連接比喻成相親。
嵌套連接就是熟人介紹,親戚朋友根據你的條件,搜索下周圍的資源,然后安排你和幾個姑娘見面,看看能不能匹配上。如果你的條件很明確(外部表索引),並且朋友對姑娘比較熟悉,對方的要求也很明確(內部表索引),那么成功率就會比較高。
合並連接就是社區或者網站組織的小型相親聯誼會,比如電影《戀愛33天中》那種8分鍾面對面的形式。男女雙方面對面進行交談(匹配判斷),每幾分鍾就換一個人再次交談,由於大家條件和目的性明確(都有索引),所以整個流程效率會比較高。
Hash連接則就像是萬人相親大會,比如上海的中山公園(條件好的已婚人士慎入)。單身青年的父母,入園后由於各種原因隨機的分成各個小群組(經過散列函數分成Hash池)。然后參與者根據自己的判斷(確認Hash鍵值),找到合適小組后(Hash鍵值相等),依次交談交換條件和信息(嘗試匹配),看看里面有沒有合適人選,有就進一步了解(匹配成功,連接)。
2013年11月14日 Ralf Wang