1. 程式人生 > >Nested Loops Join(巢狀連線) ,優化inner join的查詢速度

Nested Loops Join(巢狀連線) ,優化inner join的查詢速度

說明:最近找到了一個不錯的國外的部落格http://blogs.msdn.com/b/craigfr/,博主是Sql Server的開發人員,寫了很多Sql Server的內部原理性的文章,覺得非常有收穫。所以試著把他翻譯成中文,因為本人的英語和技術水平有限,難免會有錯誤,還請各位看官批評指教。


Nested Loops Join(巢狀連線)

Sql Server支援三種物理連線:nested loops join,merge join和hash join.這篇文章,我將描述nested loops join
(或者簡稱為NL)。

基本演算法


最簡單的情況是,nested loop會以連線謂詞為條件取出一張表裡的每一行(稱為外部表)
與另外一張表(稱為內部表)的每一行進行比較來尋找符合條件的行。(注意這裡的"內部"和"外部"是具有多層含義的,必須從
上下文中來理解它們。"內部表"和"外部表"是指連線的輸入,"內連線"和"外連線"是指邏輯操作。)

我們可以用偽碼來解釋這個演算法:


for each row R1 in the outer table
    for each row R2 in the inner table
        if R1 joins with R2
            return (R1, R2)

因為演算法裡的巢狀迴圈,所以命名為巢狀連線。

從比較的總行說來說,這種演算法的成本是與外部錶行數乘以內部表的行數成比例的。隨著驅動錶行數的增長
的成本增長是很快的,在實際情況我們通過減少內部錶行數來減小演算法的成本的。

還是以上篇文章給出的方案為例:


create table Customers (Cust_Id int, Cust_Name varchar(10))
insert Customers values (1, 'Craig')
insert Customers values (2, 'John Doe')
insert Customers values (3, 'Jane Doe')
 

create table Sales (Cust_Id int, Item varchar(10))
insert Sales values (2, 'Camera')
insert Sales values (3, 'Computer')
insert Sales values (3, 'Monitor')
insert Sales values (4, 'Printer')

進行如下查詢:


select *
from Sales S inner join Customers C
on S.Cust_Id = C.Cust_Id
option(loop join)


我加入了"loop join"提示來強迫優化器使用nested loops join.和"set statistics profile on"
一起執行得到如下的執行計劃:


Rows Executes
 
3    1         |--Nested Loops(Inner Join, WHERE:([C].[Cust_Id]=[S].[Cust_Id]))
 
3    1            |--Table Scan(OBJECT:([Customers] AS [C]))
 
12   3            |--Table Scan(OBJECT:([Sales] AS [S]))
 


這份執行計劃裡Customers是外部表,Sales是內部表。首先掃描Customers表。每次取出一個Customer,
對於每一個customer,都要掃描Sales表。因為有3個Customers,所以Sales表被掃描了3次。每次掃描返回
4行。判斷每一個sale與當前的customer是否具有相同的Cust_Id,如果相同就返回這一對行.我們有3個
customer和4個sale所以我們進行了3*4=12次比較。其中只有3次比較符合條件。

如果在Sales表建立索引會是什麼情況呢:


create clustered index CI on Sales(Cust_Id)

我們得到了如下的執行計劃:


Rows Executes
 
3    1        |--Nested Loops(Inner Join, OUTER REFERENCES:([C].[Cust_Id]))
 
3    1           |--Table Scan(OBJECT:([Customers] AS [C]))
 
3    3           |--Clustered Index Seek(OBJECT:([Sales].[CI] AS [S]), SEEK:([S].[Cust_Id]=[C].[Cust_Id]) ORDERED FORWARD)
 
這次,並沒有做全表掃描,而是進行了索引探尋。仍然進行了3次索引探尋-每個customer一次,
但是每次索引探尋只返回了與當前Cust-Id相匹配並滿足謂詞條件的一條記錄。所以,索引探尋只返回了
3行,而不是全表掃描的12行。

請注意這裡索引探尋的依賴條件C.CustId來自於連線的外部表-Customers全表掃描。
每次我們執行索引探尋(再次說明我們執行了3次-每個使用者一次),C_CustId有不同的值。
我們稱C.CustId為"關聯引數";如果一個nested loops join有關聯引數,執行計劃裡會以"OUTER REFERENCES"
顯示出來。我們經常把這種以依賴於關聯引數的索引探尋方式執行的nested loop join稱為
"索引連線"。這是非常常見的場景。


Nested loops join支援什麼型別的連線謂詞?


Nested loops join支援包括相等連線謂詞和不等謂詞連線在內的所有連線謂詞。

Nested loops join支援什麼型別的邏輯連線?


Nested loops join支援以下型別的邏輯連線:

* Inner join
* Left outer join
* Cross join
* Cross apply and outer apply
* Left semi-join and left anti-semi-join

Nested loops join不支援以下邏輯連線:

* Right and full outer join
* Right semi-join and right anti-semi-join

為什麼Nested loops join 只支援左連線?


我們很容易擴充套件Nested loops join 演算法來支援left outer 和semi-joins.例如,下邊是左外連線的偽碼。
我們可以寫出相似的程式碼來實現 left semi-join 和 left anti-semi-join.

for each row R1 in the outer table
    begin
        for each row R2 in the inner table
            if R1 joins with R2
                return (R1, R2)
        if R1 did not join
            return (R1, NULL)
    end

這個演算法記錄我們是否連線了一個特定的外部行。如果已經窮盡了所有內部行,但是沒有找到一個
符合條件的內部行,就把該外部行做為NULL擴充套件行輸出。

那麼我們為什麼不支援right outer join呢。在這裡,我們想返回符合條件的行對(R1,R2)
和不符合連線條件的(NULL,R2)。問題是我們會多次掃描內部表-對於外部表的每行都要掃描一次。
在多次掃描過程中我們可能會多次處理內部表的同一行。這樣我們就無法來判斷某一行到底符合
不符合連線條件。更進一步,如果我們使用index join,一些內部行可能都不會被處理,但是這些行在
外連線時是應該返回的。


幸運的是right outer join可以轉換為left outer join,right semi-join可以轉換為left semi-join,
所以right outer join和semi-joins是可以使用nested loops join的。但是,當執行轉換的時候可能會
影響效能。例如,上邊方案中的"Customer left outer join Sales",由於表內部表Sales有聚集索引,所以
我們在連線過程中可以使用索引探尋。如果"Customer right outer join Sales" 轉換為 "Sales left outer
join Customer”,我們則需要在Customer表上具有相應的索引了。

full outer joins是什麼情況呢?


nested loops join完全支援outer join.我們可以把"T1 full outer join T2"轉換為"T1 left outer join T2
UNION T2 left anti-semi-join T1".可以這樣來理解,將full outer join轉換為一個左連線-包含T1和T2所有的
符合條件的連線行和T1表裡沒有連線的行,然後加上那些使用anti-semi-join從T2返回的行。下邊是轉換過程:

select *
from Customers C full outer join Sales S
on C.Cust_Id = S.Cust_Id

Rows Executes
 
 
5    1        |--Concatenation
 
4    1           |--Nested Loops(Left Outer Join, WHERE:([C].[Cust_Id]=[S].[Cust_Id]))
 
3    1           |    |--Table Scan(OBJECT:([Customers] AS [C]))
 
12   3           |    |--Clustered Index Scan(OBJECT:([Sales].[Sales_ci] AS [S]))
 
0    0           |--Compute Scalar(DEFINE:([C].[Cust_Id]=NULL, [C].[Cust_Name]=NULL))
 
1    1                |--Nested Loops(Left Anti Semi Join, OUTER REFERENCES:([S].[Cust_Id]))
 
4    1                  |--Clustered Index Scan(OBJECT:([Sales].[Sales_ci] AS [S]))
 
3    4                  |--Top(TOP EXPRESSION:((1)))
 
3    4                       |--Table Scan(OBJECT:([Customers] AS [C]), WHERE:([C].[Cust_Id]=[S].[Cust_Id]))
 

注意:在上邊的例子中,優化器並選擇了聚集索引掃描而不是探尋。這完全是基於成本考慮而做出的決定。表非常小(只有一頁)
所以掃描或探尋並沒有什麼效能上的區別。

NL join好還是壞?


實際上,並沒有所謂"最好"的演算法,連線演算法也沒有好壞之分。每一種連線方式在正確的環境下效能非常好,
而在錯誤的環境下則非常差。因為nested loops join的複雜度是與驅動表大小和內部表大小乘積成比例的,所以在驅動表比較小
的情況下效能比較好。內部表不需要很小,但是如果非常大的話,在具有高選擇性的連線列上建立索引將很有幫助。

一些情況下,Sql Server只能使用nested loops join演算法,比如Cross join和一些複雜的cross applies,outer applies,
(full outer join是一個例外)。如果沒有任何相等連線謂詞的話nested loops join演算法是Sql Server的唯一選擇。