淺談SQL Server中的三種物理連接操作
簡(jiǎn)介
在SQL Server中,我們所常見(jiàn)的表與表之間的Inner Join,Outer Join都會(huì)被執(zhí)行引擎根據(jù)所選的列,數(shù)據(jù)上是否有索引,所選數(shù)據(jù)的選擇性轉(zhuǎn)化為L(zhǎng)oop Join,Merge Join,Hash Join這三種物理連接中的一種。理解這三種物理連接是理解在表連接時(shí)解決性能問(wèn)題的基礎(chǔ),下面我來(lái)對(duì)這三種連接的原理,適用場(chǎng)景進(jìn)行描述。
嵌套循環(huán)連接(Nested Loop Join)
循環(huán)嵌套連接是最基本的連接,正如其名所示那樣,需要進(jìn)行循環(huán)嵌套,嵌套循環(huán)是三種方式中***支持不等式連接的方式,這種連接方式的過(guò)程可以簡(jiǎn)單的用下圖展示:
圖1.循環(huán)嵌套連接的***步
圖2.循環(huán)嵌套連接的第二步
由上面兩個(gè)圖不難看出,循環(huán)嵌套連接查找內(nèi)部循環(huán)表的次數(shù)等于外部循環(huán)的行數(shù),當(dāng)外部循環(huán)沒(méi)有更多的行時(shí),循環(huán)嵌套結(jié)束。另外,還可以看出,這種連接方式需要內(nèi)部循環(huán)的表有序(也就是有索引),并且外部循環(huán)表的行數(shù)要小于內(nèi)部循環(huán)的行數(shù),否則查詢分析器就更傾向于Hash Join(會(huì)在本文后面講到)。
通過(guò)嵌套循環(huán)連接也可以看出,隨著數(shù)據(jù)量的增長(zhǎng)這種方式對(duì)性能的消耗將呈現(xiàn)出指數(shù)級(jí)別的增長(zhǎng),所以數(shù)據(jù)量到一定程度時(shí),查詢分析器往往就會(huì)采用這種方式。
下面我們通過(guò)例子來(lái)看一下循環(huán)嵌套連接,利用微軟的AdventureWorks數(shù)據(jù)庫(kù):
圖3.一個(gè)簡(jiǎn)單的嵌套循環(huán)連接
圖3中ProductID是有索引的,并且在循環(huán)的外部表中(Product表)符合ProductID=870的行有4688條,因此,對(duì)應(yīng)的SalesOrderDetail表需要查找4688次。讓我們?cè)谏厦娴牟樵冎性倏紤]另外一個(gè)例子,如圖4所示。
圖4.額外的列帶來(lái)的額外的書(shū)簽查找
由圖4中可以看出,由于多選擇了一個(gè)UnitPrice列,導(dǎo)致了連接的索引無(wú)法覆蓋所求查詢,必須通過(guò)書(shū)簽查找來(lái)進(jìn)行,這也是為什么我們要養(yǎng)成只 Select需要的列的好習(xí)慣,為了解決上面的問(wèn)題,我們既可以用覆蓋索引,也可以減少所需的列來(lái)避免書(shū)簽查找。另外,上面符合ProductID的行僅僅只有5條,所以查詢分析器會(huì)選擇書(shū)簽查找,假如我們將符合條件的行進(jìn)行增大,查詢分析器會(huì)傾向于表掃描(通常來(lái)說(shuō)達(dá)到表中行數(shù)的1%以上往往就會(huì)進(jìn)行 table scan而不是書(shū)簽查找,但這并不絕對(duì)),如圖5所示。
圖5.查詢分析器選擇了表掃描
可以看出,查詢分析器此時(shí)選擇了表掃描來(lái)進(jìn)行連接,這種方式效率要低下很多,因此好的覆蓋索引和Select *都是需要注意的地方。另外,上面情況即使涉及到表掃描,依然是比較理想的情況,更糟糕的情況是使用多個(gè)不等式作為連接時(shí),查詢分析器即使知道每一個(gè)列的統(tǒng)計(jì)分布,但卻不知道幾個(gè)條件的聯(lián)合分布,從而產(chǎn)生錯(cuò)誤的執(zhí)行計(jì)劃,如圖6所示。
圖6.由于無(wú)法預(yù)估聯(lián)合分布,導(dǎo)致的偏差
由圖6中,我們可以看出,估計(jì)的行數(shù)和實(shí)際的行數(shù)存在巨大的偏差,從而應(yīng)該使用表掃描但查詢分析器選擇了書(shū)簽查找,這種情況對(duì)性能的影響將會(huì)比表掃描更加巨大。具體大到什么程度呢?我們可以通過(guò)強(qiáng)制表掃描和查詢分析器的默認(rèn)計(jì)劃進(jìn)行比對(duì),如圖7所示。
圖7.強(qiáng)制表掃描性能反而更好
#p#
合并連接(Merge Join)
談到合并連接,我突然想起在西雅圖參加SQL Pass峰會(huì)晚上酒吧排隊(duì)點(diǎn)酒,由于我和另外一哥們站錯(cuò)了位置,貌似我們兩個(gè)在插隊(duì)一樣,我趕緊說(shuō):I’m sorry,i thought here is end of line。對(duì)方無(wú)不幽默的說(shuō):”It’s OK,In SQL Server,We called it merge join”。
由上面的小故事不難看出,Merge Join其實(shí)上就是將兩個(gè)有序隊(duì)列進(jìn)行連接,需要兩端都已經(jīng)有序,所以不必像Loop Join那樣不斷的查找循環(huán)內(nèi)部的表。其次,Merge Join需要表連接條件中至少有一個(gè)等號(hào)查詢分析器才會(huì)去選擇Merge Join。
Merge Join的過(guò)程我們可以簡(jiǎn)單用下面圖進(jìn)行描述:
圖8.Merge Join***步
Merge Join首先從兩個(gè)輸入集合中各取***行,如果匹配,則返回匹配行。加入兩行不匹配,則有較小值的輸入集合+1,如圖9所示。
圖9.更小值的輸入集合向下進(jìn)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#代碼表示
因此,通常來(lái)說(shuō)Merge Join如果輸入兩端有序,則Merge Join效率會(huì)非常高,但是如果需要使用顯式Sort來(lái)保證有序?qū)崿F(xiàn)Merge Join的話,那么Hash Join將會(huì)是效率更高的選擇。但是也有一種例外,那就是查詢中存在order by,group by,distinct等可能導(dǎo)致查詢分析器不得不進(jìn)行顯式排序,那么對(duì)于查詢分析器來(lái)說(shuō),反正都已經(jīng)進(jìn)行顯式Sort了,何不一石二鳥(niǎo)的直接利用 Sort后的結(jié)果進(jìn)行成本更小的MERGE JOIN?在這種情況下,Merge Join將會(huì)是更好的選擇。
另外,我們可以由Merge Join的原理看出,當(dāng)連接條件為不等式(但不包括!=),比如說(shuō)> < >=等方式時(shí),Merge Join有著更好的效率。
下面我們來(lái)看一個(gè)簡(jiǎn)單的Merge Join,這個(gè)Merge Join是由聚集索引和非聚集索引來(lái)保證Merge Join的兩端有序,如圖10所示。
圖10.由聚集索引和非聚集索引保證輸入兩端有序
當(dāng)然,當(dāng)Order By,Group By時(shí)查詢分析器不得不用顯式Sort,從而可以一箭雙雕時(shí),也會(huì)選擇Merge Join而不是Hash Join,如圖11所示。
圖11.一箭雙雕的Merge Join
哈希匹配(Hash Join)
哈希匹配連接相對(duì)前面兩種方式更加復(fù)雜一些,但是哈希匹配對(duì)于大量數(shù)據(jù),并且無(wú)序的情況下性能均好于Merge Join和Loop Join。對(duì)于連接列沒(méi)有排序的情況下(也就是沒(méi)有索引),查詢分析器會(huì)傾向于使用Hash Join。
哈希匹配分為兩個(gè)階段,分別為生成和探測(cè)階段,首先是生成階段,***階段生成階段具體的過(guò)程可以如圖12所示。
圖12.哈希匹配的***階段
圖12中,將輸入源中的每一個(gè)條目經(jīng)過(guò)散列函數(shù)的計(jì)算都放到不同的Hash Bucket中,其中Hash Function的選擇和Hash Bucket的數(shù)量都是黑盒,微軟并沒(méi)有公布具體的算法,但我相信已經(jīng)是非常好的算法了。另外在Hash Bucket之內(nèi)的條目是無(wú)序的。通常來(lái)講,查詢優(yōu)化器都會(huì)使用連接兩端中比較小的哪個(gè)輸入集來(lái)作為***階段的輸入源。
接下來(lái)是探測(cè)階段,對(duì)于另一個(gè)輸入集合,同樣針對(duì)每一行進(jìn)行散列函數(shù),確定其所應(yīng)在的Hash Bucket,在針對(duì)這行和對(duì)應(yīng)Hash Bucket中的每一行進(jìn)行匹配,如果匹配則返回對(duì)應(yīng)的行。
通過(guò)了解哈希匹配的原理不難看出,哈希匹配涉及到散列函數(shù),所以對(duì)CPU的消耗會(huì)非常高,此外,在Hash Bucket中的行是無(wú)序的,所以輸出結(jié)果也是無(wú)序的。圖13是一個(gè)典型的哈希匹配,其中查詢分析器使用了表數(shù)據(jù)量比較小的Product表作為生成,而使用數(shù)據(jù)量大的SalesOrderDetail表作為探測(cè)。
圖13.一個(gè)典型的哈希匹配連接
上面的情況都是內(nèi)存可以容納下生成階段所需的內(nèi)存,如果內(nèi)存吃緊,則還會(huì)涉及到Grace哈希匹配和遞歸哈希匹配,這就可能會(huì)用到TempDB從而吃掉大量的IO。這里就不細(xì)說(shuō)了,有興趣的同學(xué)可以移步:http://msdn.microsoft.com/zh-cn/library/aa178403(v=SQL.80).aspx。
總結(jié)
下面我們通過(guò)一個(gè)表格簡(jiǎn)單總結(jié)這幾種連接方式的消耗和使用場(chǎng)景:
嵌套循環(huán)連接 | 合并連接 | 哈希連接 | |
適用場(chǎng)景 | 外層循環(huán)小,內(nèi)存循環(huán)條件列有序 | 輸入兩端都有序 | 數(shù)據(jù)量大,且沒(méi)有索引 |
CPU | 低 | 低(如果沒(méi)有顯式排序) | 高 |
內(nèi)存 | 低 | 低(如果沒(méi)有顯式排序) | 高 |
IO | 可能高可能低 | 低 | 可能高可能低 |
理解SQL Server這幾種物理連接方式對(duì)于性能調(diào)優(yōu)來(lái)說(shuō)必不可少,很多時(shí)候當(dāng)篩選條件多表連接多時(shí),查詢分析器就可能不是那么智能了,因此理解這幾種連接方式對(duì)于定位問(wèn)題變得尤為重要。此外,我們也可以通過(guò)從業(yè)務(wù)角度減少查詢范圍來(lái)減少低下性能連接的可能性。
原文鏈接:http://www.cnblogs.com/CareySon/archive/2013/01/09/2853094.html