在SQL Server中,咱們所常見的表與表之間的Inner Join,Outer Join都會被執行引擎根據所選的列,數據上是否有索引,所選數據的選擇性轉化爲Loop Join,Merge Join,Hash Join這三種物理鏈接中的一種。理解這三種物理鏈接是理解在錶鏈接時解決性能問題的基礎,下面我來對這三種鏈接的原理,適用場景進行描述。html
循環嵌套鏈接是最基本的鏈接,正如其名所示那樣,須要進行循環嵌套,這種鏈接方式的過程能夠簡單的用下圖展現:算法
圖1.循環嵌套鏈接的第一步數據庫
圖2.循環嵌套鏈接的第二步函數
由上面兩個圖不難看出,循環嵌套鏈接查找內部循環表的次數等於外部循環的行數,當外部循環沒有更多的行時,循環嵌套結束。另外,還能夠看出,這種鏈接方式須要內部循環的表有序(也就是有索引),而且外部循環表的行數要小於內部循環的行數,不然查詢分析器就更傾向於Hash Join(會在本文後面講到)。oop
經過嵌套循環鏈接也能夠看出,隨着數據量的增加這種方式對性能的消耗將呈現出指數級別的增加,因此數據量到必定程度時,查詢分析器每每就會採用這種方式。性能
下面咱們經過例子來看一下循環嵌套鏈接,利用微軟的AdventureWorks數據庫:優化
圖3.一個簡單的嵌套循環鏈接spa
圖3中ProductID是有索引的,而且在循環的外部表中(Product表)符合ProductID=870的行有4688條,所以,對應的SalesOrderDetail表須要查找4688次。讓咱們在上面的查詢中再考慮另一個例子,如圖4所示。3d
圖4.額外的列帶來的額外的書籤查找htm
由圖4中能夠看出,因爲多選擇了一個UnitPrice列,致使了鏈接的索引沒法覆蓋所求查詢,必須經過書籤查找來進行,這也是爲何咱們要養成只Select須要的列的好習慣,爲了解決上面的問題,咱們既能夠用覆蓋索引,也能夠減小所需的列來避免書籤查找。另外,上面符合ProductID的行僅僅只有5條,因此查詢分析器會選擇書籤查找,假如咱們將符合條件的行進行增大,查詢分析器會傾向於表掃描(一般來講達到表中行數的1%以上每每就會進行table scan而不是書籤查找,但這並不絕對),如圖5所示。
圖5.查詢分析器選擇了表掃描
能夠看出,查詢分析器此時選擇了表掃描來進行鏈接,這種方式效率要低下不少,所以好的覆蓋索引和Select *都是須要注意的地方。另外,上面狀況即便涉及到表掃描,依然是比較理想的狀況,更糟糕的狀況是使用多個不等式做爲鏈接時,查詢分析器即便知道每個列的統計分佈,但殊不知道幾個條件的聯合分佈,從而產生錯誤的執行計劃,如圖6所示。
圖6.因爲沒法預估聯合分佈,致使的誤差
由圖6中,咱們能夠看出,估計的行數和實際的行數存在巨大的誤差,從而應該使用表掃描但查詢分析器選擇了書籤查找,這種狀況對性能的影響將會比表掃描更加巨大。具體大到什麼程度呢?咱們能夠經過強制表掃描和查詢分析器的默認計劃進行比對,如圖7所示。
圖7.強制表掃描性能反而更好
談到合併鏈接,我忽然想起在西雅圖參加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
哈希匹配鏈接相對前面兩種方式更加複雜一些,可是哈希匹配對於大量數據,而且無序的狀況下性能均好於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