SQL Server中的執行引擎入門

宋沄劍發表於2015-12-20

簡介

當查詢優化器(Query Optimizer)將T-SQL語句解析後並從執行計劃中選擇最低消耗的執行計劃後,具體的執行就會交由執行引擎(Execution Engine)來進行執行。本文旨在分類講述執行計劃中每一種操作的相關資訊。

 

資料訪問操作

首先最基本的操作就是訪問資料。這既可以通過直接訪問表,也可以通過訪問索引來進行。表內資料的組織方式分為堆(Heap)和B樹,其中表中沒有建立聚集索引時資料是通過堆進行組織的,這個是無序的,表中建立聚集索引後和非聚集索引的資料都是以B樹方式進行組織,這種方式資料是有序儲存的。通常來說,非聚集索引僅僅包含整個表的部分列,對於過濾索引,還僅僅包含部分行。

除去資料的組織方式不同外,訪問資料也分為兩種方式,掃描(Scan)和查詢(Seek),掃描是掃描整個結構的所有資料,而查詢只是查詢整個結構中的部分資料。因此可以看出,由於堆是無序的,所以不可能在堆上面進行查詢(Seek)操作,而相對於B樹的有序,使得在B樹中進行查詢成為可能。當針對一個以堆組織的表進行資料訪問時,就會進行堆掃描,如圖1所示。

sqlengine1

    圖1.表掃描

 

可以看出,表掃描的圖示很清晰的表明表掃描的性質,在一個無序組織表中從頭到尾掃描一遍。

而對於B樹結構的聚集索引和非聚集索引,同樣可以進行掃描,通常來講,為了獲取索引表中的所有資料或是獲得索引行樹佔了資料大多數使得掃描的成本小於查詢時,會進行聚集索引掃描。如圖2所示。

sqlengine2

    圖2.聚集索引掃描

 

聚集索引掃描的圖示也同樣能夠清晰的表明聚集索引掃描的性質,找到最左邊的葉子節點後,依次掃描所有葉子節點,達到掃描整個結構的作用。當然對於非聚集索引也是同樣的概念,如圖3所示。

sqlengine3

    圖3.非聚集索引的掃描

 

而對於僅僅選擇B樹結構中的部分資料,索引查詢(Seek)使得B樹變得有意義。根據所查詢的關鍵值,可以使得從僅僅從B樹根部向下走單一路徑,因此免去了掃描不必要頁的消耗,圖4是查詢計劃中的一個索引查詢。

sqlengine4

    圖4.聚集索引查詢

 

索引查詢的圖示也是很傳神的,可以看到圖示那根線從根節點一路向下到葉子節點。也就是找到所求資料所在的頁,不難看出,如果我們需要查詢多條資料且分散在不同的頁中,這個查詢操作需要重複執行很多回,當這個次數大到一定程度時,SQL Server會選擇消耗比較低的索引掃描而不是再去重複索引查詢。對於非聚集索引查詢,概念是一樣的,就不再上圖片了。

 

書籤查詢(Bookmark Lookup)

     你也許會想,假如非聚集索引可以快速的找到所求的資料,但遺憾的是,非聚集索引卻不包含所有所求列時該怎麼辦?這時SQL Server會面臨兩個選擇,直接訪問基本表去獲取資料或是在非聚集索引中找到資料後,再去基本表獲得非聚集索引沒有覆蓋到的所求列。這個選擇取決於所估計的行數等統計資訊。查詢分析器會選擇消耗比較少的那個。

一個簡單的書籤查詢如圖5所示。

sqlengine5

    圖5.一個簡單的書籤查詢

 

從圖5可以看出,首先通過非聚集索引找到所求的行,但這個索引並不包含所有的列,因此還要額外去基本表中找到這些列,因此要進行鍵查詢,如果基本表是以堆進行組織的,那麼這個鍵查詢(Key Lookup)就會變成RID查詢(RID Lookup),鍵查詢和RID查詢統稱為書籤查詢。

不過有時候索引查詢所返回的行數過多導致書籤查詢的效能遠不如直接進行掃描操作,因此SQL Server這時會選擇掃描而不是書籤查詢。如圖6所示。

sqlengine6

圖6.StateProvinceID列有非聚集索引,但由於返回行數過多,分析器會選擇掃描而不是書籤查詢

 

這個估計是根據統計資訊進行的,關於統計資訊,可以看我之前的一篇博文:淺談SQL Server中統計對於查詢的影響

 

聚合操作(Aggregation)

聚合函式會導致聚合操作。聚合函式是將一個集合的資料按照某種規則彙總成1個資料,或基於分組按照規則彙總成多個資料的過程。一些聚合函式比如:avg,sum,min,另外還有distinct關鍵字都有可能導致兩類聚合操作:流聚合(Stream Aggregation)和雜湊聚合(Hash Aggregation)。

 

流聚合(Stream Aggregation)

流聚合需要再執行聚合函式之前,被聚合的資料集合是有序的,這個有序資料既可以通過執行計劃中的Sort進行,也可以直接從聚集或是非聚集索引中直接獲得有序資料,另外,沒有Group by的聚合操作被成為標量聚合,這類操作一定是會執行流聚合。

比如,我們直接進行標量聚合,如圖7所示。

sqlengine7

    圖7.流聚合

 

但對於加了Group by的子句,因為需要資料按照group by 後面的列有序,就需要Sort來保證排序。注意,Sort操作是佔用記憶體的操作,當記憶體不足時還會去佔用tempdb。SQL Server總是會在Sort操作和雜湊匹配中選擇成本最低的。一個需要Sort的操作如圖8所示。

sqlengine8

    圖8.需要排序的流聚合

 

圖8中排序操作按照ProductLine進行排序後,然後就根據各自的分組做聚合操作了。

 

雜湊聚合(Hash aggregation)

上面的流聚合適合比較少的資料,但是對於相對大一點的表。使用雜湊集合成本會比排序要低。雜湊集合通過在記憶體中建立雜湊表來實現聚合,因此無需對資料集合進行排序。記憶體中所建立的雜湊表以Group by後面的列作為鍵值,如圖9所示。

   sqlengine9
圖9.雜湊聚合

 

在記憶體中建立好雜湊表後,會按照group by後面的值作為鍵,然後依次處理集合中的每條資料,當鍵在雜湊表中不存在時,向雜湊表新增條目,當鍵已經在雜湊表中存在時,按照規則(規則是聚合函式,比如Sum,avg什麼的)計算雜湊表中的值(Value)。

 

 

連線(Join)

當多表連線時(書籤查詢,索引之間的連線都算),SQL Server會採用三類不同的連線方式:迴圈巢狀連線(Nested Loops Join),合併連線(Merge Join),雜湊連線(Hash Join)。這幾種連線並不是哪種會比另一種更好,而是每種連線方式都會適應特定場景。

 

迴圈巢狀連線(Nested Loops Join)

由圖10可以看到一個簡單的迴圈巢狀連線。

sqlengine10

    圖10.一個迴圈巢狀連線的例項

 

迴圈巢狀連線的圖示同樣十分傳神,處在上面的外部輸入(Outer input),這裡也就是聚集索引掃描。和處在下面的內部輸入(Inner Input),這裡也就是聚集索引查詢。外部輸入僅僅執行一次,根據外部輸入滿足Join條件的每一行,對內部輸入進行查詢。這裡由於是290行,對於內部輸入執行290次。

可以通過屬性視窗看到.如圖11所示:

sqlengine11

    圖11.內部輸入的執行次數

 

根據巢狀迴圈的原理不難看出,由於外部輸入是掃描,內部輸入是查詢,當兩個Join的表外部輸入結果集比較小,而內部輸入所查詢的表非常大時,查詢優化器更傾向於選擇迴圈巢狀方式。

 

合併連線(Merge Join)

不同於迴圈巢狀的是,合併連線是從每個表僅僅執行一次訪問。從這個原理來看,合併連線要比迴圈巢狀要快了不少。下面來看一個典型的合併連線,如圖12所示。

sqlengine12

    圖12.合併連線

 

從合併連線的原理不難想象,首先合併連線需要雙方有序.並且要求Join的條件為等於號。因為兩個輸入條件已經有序,所以從每一個輸入集合中取一行進行比較,相等的返回,不相等的捨棄,從這裡也不難看出Merge join為什麼只允許Join後面是等於號。從圖11的圖示中我們可以看出這個原理。

如果輸入資料的雙方無序,則查詢分析器不會選擇合併連線,我們也可以通過索引提示強制使用合併連線,為了達到這一目的,執行計劃必須加上一個排序步驟來實現有序,如圖13所示。

sqlengine13

    圖13.通過排序來實現Merge Join

 

雜湊連線(Hash Join)

雜湊連線同樣僅僅只需要只訪問1次雙方的資料。雜湊連線通過在記憶體中建立雜湊表實現。這比較消耗記憶體,如果記憶體不足還會佔用tempdb。但並不像合併連線那樣需要雙方有序。一個典型的雜湊連線如圖14所示。

sqlengine14

    圖14.雜湊連線

 

這裡我刪除了Costomer的聚集索引,否則兩個有序輸入SQL Server會選擇代價更低的合併連線。SQL Server利用兩個上面的輸入生成雜湊表,下面的輸入來探測,可以在屬性視窗看到這些資訊,如圖15所示。

sqlengine15

圖15.雜湊鍵生成和雜湊鍵探測

 

通常來說,在兩個輸入資料比較大,且所求資料在其中一方或雙方沒有排序的條件達成時,會選用雜湊匹配。

 

並行

    當多個表連線時,SQL Server還允許在多CPU或多核的情況下允許查詢並行,這樣無疑提高了效率,一個並行的例子如圖16所示。

sqlengine16

   圖16.並行提高效率

 

總結

本文簡單介紹了SQL Server執行計劃中常見的操作極其原理,瞭解這些步驟和原理是優化查詢的基本功。

相關文章