淺談SQL Server中的三種物理連線操作

宋沄劍發表於2015-12-18

簡介

在SQL Server中,我們所常見的表與表之間的Inner Join,Outer Join都會被執行引擎根據所選的列,資料上是否有索引,所選資料的選擇性轉化為Loop Join,Merge Join,Hash Join這三種物理連線中的一種。理解這三種物理連線是理解在表連線時解決效能問題的基礎,下面我來對這三種連線的原理,適用場景進行描述。

 

巢狀迴圈連線(Nested Loop Join)

迴圈巢狀連線是最基本的連線,正如其名所示那樣,需要進行迴圈巢狀,這種連線方式的過程可以簡單的用下圖展示:

sqlthree1

圖1.迴圈巢狀連線的第一步

sqlthree2

圖2.迴圈巢狀連線的第二步

 

由上面兩個圖不難看出,迴圈巢狀連線查詢內部迴圈表的次數等於外部迴圈的行數,當外部迴圈沒有更多的行時,迴圈巢狀結束。另外,還可以看出,這種連線方式需要內部迴圈的表有序(也就是有索引),並且外部迴圈表的行數要小於內部迴圈的行數,否則查詢分析器就更傾向於Hash Join(會在本文後面講到)。

通過巢狀迴圈連線也可以看出,隨著資料量的增長這種方式對效能的消耗將呈現出指數級別的增長,所以資料量到一定程度時,查詢分析器往往就會採用這種方式。

下面我們通過例子來看一下迴圈巢狀連線,利用微軟的AdventureWorks資料庫:

sqlthree3

圖3.一個簡單的巢狀迴圈連線

圖3中ProductID是有索引的,並且在迴圈的外部表中(Product表)符合ProductID=870的行有4688條,因此,對應的SalesOrderDetail表需要查詢4688次。讓我們在上面的查詢中再考慮另外一個例子,如圖4所示。

sqlthree4

圖4.額外的列帶來的額外的書籤查詢

由圖4中可以看出,由於多選擇了一個UnitPrice列,導致了連線的索引無法覆蓋所求查詢,必須通過書籤查詢來進行,這也是為什麼我們要養成只Select需要的列的好習慣,為了解決上面的問題,我們既可以用覆蓋索引,也可以減少所需的列來避免書籤查詢。另外,上面符合ProductID的行僅僅只有5條,所以查詢分析器會選擇書籤查詢,假如我們將符合條件的行進行增大,查詢分析器會傾向於表掃描(通常來說達到表中行數的1%以上往往就會進行table scan而不是書籤查詢,但這並不絕對),如圖5所示。

sqlthree5

圖5.查詢分析器選擇了表掃描

 

可以看出,查詢分析器此時選擇了表掃描來進行連線,這種方式效率要低下很多,因此好的覆蓋索引和Select *都是需要注意的地方。另外,上面情況即使涉及到表掃描,依然是比較理想的情況,更糟糕的情況是使用多個不等式作為連線時,查詢分析器即使知道每一個列的統計分佈,但卻不知道幾個條件的聯合分佈,從而產生錯誤的執行計劃,如圖6所示。

sqlthree6

圖6.由於無法預估聯合分佈,導致的偏差

 

由圖6中,我們可以看出,估計的行數和實際的行數存在巨大的偏差,從而應該使用表掃描但查詢分析器選擇了書籤查詢,這種情況對效能的影響將會比表掃描更加巨大。具體大到什麼程度呢?我們可以通過強制表掃描和查詢分析器的預設計劃進行比對,如圖7所示。

sqlthree7

圖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的過程我們可以簡單用下面圖進行描述:

sqlthree8

圖8.Merge Join第一步

 

Merge Join首先從兩個輸入集合中各取第一行,如果匹配,則返回匹配行。假如兩行不匹配,則有較小值的輸入集合+1,如圖9所示。

sqlthree9

圖9.更小值的輸入集合向下進1

用C#程式碼表示Merge Join的話如程式碼1所示。

程式碼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所示。

sqlthree10

圖10.由聚集索引和非聚集索引保證輸入兩端有序

 

當然,當Order By,Group By時查詢分析器不得不用顯式Sort,從而可以一箭雙鵰時,也會選擇Merge Join而不是Hash Join,如圖11所示。

sqlthree11

圖11.一箭雙鵰的Merge Join

 

雜湊匹配(Hash Join)

雜湊匹配連線相對前面兩種方式更加複雜一些,但是雜湊匹配對於大量資料,並且無序的情況下效能均好於Merge Join和Loop Join。對於連線列沒有排序的情況下(也就是沒有索引),查詢分析器會傾向於使用Hash Join。

雜湊匹配分為兩個階段,分別為生成和探測階段,首先是生成階段,第一階段生成階段具體的過程可以如圖12所示。

sqlthree12

圖12.雜湊匹配的第一階段

 

圖12中,將輸入源中的每一個條目經過雜湊函式的計算都放到不同的Hash Bucket中,其中Hash Function的選擇和Hash Bucket的數量都是黑盒,微軟並沒有公佈具體的演算法,但我相信已經是非常好的演算法了。另外在Hash Bucket之內的條目是無序的。通常來講,查詢優化器都會使用連線兩端中比較小的哪個輸入集來作為第一階段的輸入源。

接下來是探測階段,對於另一個輸入集合,同樣針對每一行進行雜湊函式,確定其所應在的Hash Bucket,在針對這行和對應Hash Bucket中的每一行進行匹配,如果匹配則返回對應的行。

通過了解雜湊匹配的原理不難看出,雜湊匹配涉及到雜湊函式,所以對CPU的消耗會非常高,此外,在Hash Bucket中的行是無序的,所以輸出結果也是無序的。圖13是一個典型的雜湊匹配,其中查詢分析器使用了表資料量比較小的Product表作為生成,而使用資料量大的SalesOrderDetail表作為探測。

sqlthree13

圖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

相關文章