在深入聚集索引與非聚集索引(一)(二)中,(好吧,由於沒什么人看,因此沒寫二),我們詳細的分析了SQL SERVER是如何用堆和B樹來組織表,並用這兩個數據結構幫助我們查詢的。
這里我們繼續的內容就是探討SQL SERVER中的連接算法。
聯接算法是指在物理上把多個數據源如何聯接起來,SQL SERVER支持三種聯接算法
1.nested loop 嵌套循環算法
2.merge 合並算法
3.hash 哈希算法
其實這幾種算法我們在通常的編程中也經常會用到,並不是很難理解。
一、嵌套循環
一般來說嵌套循環就對應着兩層for循環,對於外層for中的每一個項,在內層循環中都要匹配一次。
相應的,外層for對應着外部輸入表,在執行計划的圖示中排在上面,內層for則是內層輸入表,在執行計划中排在下面。
這里值得強調的是,外部輸入是每一行的都要使用來匹配的,而內部表卻不一定每一行都在匹配中使用。假設外部輸入有N行,內部輸入表有M行,最差的時間復雜度就是O(N*M)。而在這種最差的情況下,優化器不會再采用嵌套循環,而是采用Hash匹配算法。關於Hash匹配算法,后面會有說明。
因此我們可以得到下面的推論
1.外部輸入越小越好,因為外部輸入每一行都要被用來匹配,不能減少。
2.內部輸入表作為匹配的,則可以利用索引來減少匹配條件的范圍,這樣就可以通過少量的搜索來獲取匹配行。
因此嵌套循環算法在連接條件的選擇性比較強,而且在內部輸入的連接列上有可以利用的有效索引時,是最有效的。
在下面這個例子中,Customers表的記錄數要遠比Orders表的記錄數少(客戶數肯定要比訂單數少),因此Customers表中的數據被優化器選擇作為外部輸入,從圖中可以看出Customers表是在Orders表的上方。
當我們直接查詢是,SQL SERVER還會很智能的告訴你缺少什么索引。在下面這個例子中,我們缺少的正是作為內部輸入的“連接列 custid”和“查詢列 orderdate”上的非聚集索引
當我們輸入下面這條語句加上索引后
CREATE INDEX idx_nc_cid_od_i_oid_eid_sid
ON dbo.Orders(custid, orderdate)
INCLUDE(orderid, empid, shipperid);
SQL SERVER仍然報缺少索引,是因為我們對於外部輸入的表仍然是可以利用索引來先篩選一輪,以起到減小外部輸入的目的。
為了達到這個目的,缺少的是這個非聚集索引,custname作為篩選列,加上custid同樣是為了起到覆蓋作用。
CREATE INDEX idx_nc_cn_i_cid
ON dbo.Customers(custname) INCLUDE(custid);
這時對於Customers表的index scan就會變成Index seek。
補全索引后,我們最終獲得的執行計划
二、合並排序
對於兩個輸入列都有序的情況下,合並聯接的效率高。
排序的重要性毋庸置疑了,什么二分查找等等查找都是建立在輸入序列有序的基礎上。
為什么先講索引呢?我們可以從索引中發現有現成的排序好的數據結構嗎?有的,B樹中的葉層就是按照一定的邏輯順序維護的。也就是說,聚集索引和非聚集覆蓋索引,都可以通過對葉層的有序掃描以較小的代價就可以獲取有序的數據。在這種情況下,就算輸入表的規模比較大,合並聯接也相當給力。如果計划分析器確定連接的一側記錄集中的元素是唯一確定的,那么就會采用一對多的匹配方式(多指另一側的元素會有重復),在這種情況下,合並排序效率應該是幾種連接方式中最高的。
但如果所需的數據列並不存在上述的條件的時候,對於較大的輸入來說排序往往是一個開銷非常大的操作(因為基於比較的排序最快也就是n log n的),因此優化器通常不會在這種情況下選用合並聯接。但是對於較小的輸入排序的消耗還是可以接受的。較小的輸入可以像上例一樣通過對自身的篩選來獲得。
分析:
對於連接列custid,對於Customers表不用說,是該表的聚集索引的聚集鍵,對於order表來說,我們在上面給custid創建了非聚集覆蓋索引,所以也可以按照有序掃描以較小的代價獲取有序數據。
因為都可以從兩張表中以較小的代價獲取按照連接列custid的順序獲取有序的數據,因此優化器選擇了合並排序。
可以看出Orders表的掃描在整體開銷中所占的比例是最大的達到68%,因為Orders表的數據非常多。那么假設我們在Orders表上還有其他的篩選條件呢?比如對orderdate進行限定
由於這個篩選器的選擇性非常高,所獲取的結果才1000多行,只占Orders表1000000總數下的0.1%。兩權之下,另可放棄有序的全表掃描,而是先過濾再排序。
當然,計划器也有可能估計錯誤,如果所獲取的結果選擇性不高的話,排序所占用的開銷往往非常大。這也是我們在發現優化器生成Merge計划時要注意的地方。
三、Hash聯接
原理參照我寫的這篇文章,為了減少內存占用因此使用數據量較小的表來構造hashtable,然后另逐行掃描另一張表通過hash函數算出hashtable的某個位置上是否已存在值來判斷相等。
通常用到hash聯接,是因為缺少現成的索引,特別是在數據倉庫類型(OLTP)的應用中.
我們在未創建任何索引的第一個示例中,采取的就是Hash匹配。逐行掃描Customer表構造hashtable,因為我們where條件中有orderdate可以減小匹配的范圍,所以先用聚集索引減少Orders表中匹配的記錄數,然后再用hash函數逐行匹配前面構造好的hashtable.
缺少合適的索引也可能會采用Hash匹配。我們把orderdate的范圍增加了幾十倍,由一天改成了查詢幾個月,這時合並聯接算法不再適合。
總結:
采用Hash聯接算法,從時間復雜度上來說是最優的,聯接一張M條記錄的表和一張N條記錄的表的時間復雜度為O(M+N),好於帶有聚集索引的嵌套聯接算法O(M * log2n)。但是如果在構造hashtable時內存不足以保存Hashtable時,會產生臨時空間交換,導致而外大量的IO從而抵消了聯接時所產生的益處。
當然如果你覺得有幫助,請點擊推薦,或者留點評論說說想法,討論討論。
下面打算寫寫我在學習Object C和Git中的一些心得體會系列文章。







