簡介
當查詢最佳化工具(Query Optimizer)將T-SQL語句解析後並從執行計畫中選擇最低消耗的執行計畫後,具體的執行就會交由執行引擎(Execution Engine)來進行執行。本文旨在分類講述執行計畫中每一種操作的相關資訊。
資料訪問操作
首先最基本的操作就是訪問資料。這既可以通過直接存取表,也可以通過訪問索引來進行。表內資料的組織方式分為堆(Heap)和B樹,其中表中沒有建立叢集索引時資料是通過堆進行組織的,這個是無序的,表中建立叢集索引後和非叢集索引的資料都是以B樹方式進行組織,這種方式資料是有序儲存的。通常來說,非叢集索引僅僅包含整個表的部分列,對於過濾索引,還僅僅包含部分行。
除去資料的組織方式不同外,訪問資料也分為兩種方式,掃描(Scan)和尋找(Seek),掃描是掃描整個結構的所有資料,而尋找只是尋找整個結構中的部分資料。因此可以看出,由於堆是無序的,所以不可能在堆上面進行尋找(Seek)操作,而相對於B樹的有序,使得在B樹中進行尋找成為可能。當針對一個以堆組織的表進行資料訪問時,就會進行堆掃描,1所示。
圖1.表掃描
可以看出,表掃描的表徵圖很清晰的表明表掃描的性質,在一個無序組織表中從頭到尾掃描一遍。
而對於B樹結構的叢集索引和非叢集索引,同樣可以進行掃描,通常來講,為了擷取索引表中的所有資料或是獲得索引行樹佔了資料大多數使得掃描的成本小於尋找時,會進行叢集索引掃描。2所示。
圖2.叢集索引掃描
叢集索引掃描的表徵圖也同樣能夠清晰的表明叢集索引掃描的性質,找到最左邊的葉子節點後,依次掃描所有葉子節點,達到掃描整個結構的作用。當然對於非叢集索引也是同樣的概念,3所示。
圖3.非叢集索引的掃描
而對於僅僅選擇B樹結構中的部分資料,索引尋找(Seek)使得B樹變得有意義。根據所尋找的關索引值,可以使得從僅僅從B樹根部向下走單一路徑,因此免去了掃描不必要頁的消耗,圖4是查詢計劃中的一個索引尋找。
圖4.叢集索引尋找
索引尋找的表徵圖也是很傳神的,可以看到表徵圖那根線從根節點一路向下到葉子節點。也就是找到所求資料所在的頁,不難看出,如果我們需要尋找多條資料且分散在不同的頁中,這個尋找操作需要重複執行很多回,當這個次數大到一定程度時,SQL Server會選擇消耗比較低的索引掃描而不是再去重複索引尋找。對於非叢集索引尋找,概念是一樣的,就不再片了。
書籤尋找(Bookmark Lookup)
你也許會想,假如非叢集索引可以快速的找到所求的資料,但遺憾的是,非叢集索引卻不包含所有所求列時該怎麼辦?這時SQL Server會面臨兩個選擇,直接存取基本表去擷取資料或是在非叢集索引中找到資料後,再去基本表獲得非叢集索引沒有覆蓋到的所求列。這個選擇取決於所估計的行數等統計資訊。查詢分析器會選擇消耗比較少的那個。
一個簡單的書籤尋找5所示。
圖5.一個簡單的書籤尋找
從圖5可以看出,首先通過非叢集索引找到所求的行,但這個索引並不包含所有的列,因此還要額外去基本表中找到這些列,因此要進行鍵尋找,如果基本表是以堆進行組織的,那麼這個鍵尋找(Key Lookup)就會變成RID尋找(RID Lookup),鍵尋找和RID尋找統稱為書籤尋找。
不過有時候索引尋找所返回的行數過多導致書籤尋找的效能遠不如直接進行掃描操作,因此SQL Server這時會選擇掃描而不是書籤尋找。6所示。
圖6.StateProvinceID列有非叢集索引,但由於返回行數過多,分析器會選擇掃描而不是書籤尋找
這個估計是根據統計資訊進行的,關於統計資訊,可以看我之前的一篇博文:淺談SQL Server中統計對於查詢的影響
彙總操作(Aggregation)
彙總函式會導致彙總操作。彙總函式是將一個集合的資料按照某種規則匯總成1個資料,或基於分組按照規則匯總成多個資料的過程。一些彙總函式比如:avg,sum,min,另外還有distinct關鍵字都有可能導致兩類彙總操作:流彙總(Stream Aggregation)和雜湊彙總(Hash Aggregation)。
流彙總(Stream Aggregation)
流彙總需要再執資料列彙總函式之前,被彙總的資料集合是有序的,這個有序資料既可以通過執行計畫中的Sort進行,也可以直接從聚集或是非叢集索引中直接獲得有序資料,另外,沒有Group by的彙總操作被成為標量彙總,這類操作一定是會執行流彙總。
比如,我們直接進行標量彙總,7所示。
圖7.流彙總
但對於加了Group by的子句,因為需要資料按照group by 後面的列有序,就需要Sort來保證排序。注意,Sort操作是佔用記憶體的操作,當記憶體不足時還會去佔用tempdb。SQL Server總是會在Sort操作和散列匹配中選擇成本最低的。一個需要Sort的操作8所示。
圖8.需要排序的流彙總
圖8中排序操作按照ProductLine進行排序後,然後就根據各自的分組做彙總操作了。
散列彙總(Hash aggregation)
上面的流彙總適合比較少的資料,但是對於相對大一點的表。使用散列集合成本會比排序要低。散列集合通過在記憶體中建立散列表來實現彙總,因此無需對資料集合進行排序。記憶體中所建立的散列表以Group by後面的列作為索引值,9所示。
圖9.散列彙總
在記憶體中建立好散列表後,會按照group by後面的值作為鍵,然後依次處理集合中的每條資料,當鍵在散列表中不存在時,向散列表添加條目,當鍵已經在散列表中存在時,按照規則(規則是彙總函式,比如Sum,avg什麼的)計算散列表中的值(Value)。
串連(Join)
當多表串連時(書籤尋找,索引之間的串連都算),SQL Server會採用三類不同的串連方式:迴圈嵌套串連(Nested Loops Join),合并串連(Merge Join),散列串連(Hash Join)。這幾種串連並不是哪種會比另一種更好,而是每種串連方式都會適應特定情境。
迴圈嵌套串連(Nested Loops Join)
由圖10可以看到一個簡單的迴圈嵌套串連。
圖10.一個迴圈嵌套串連的執行個體
迴圈嵌套串連的表徵圖同樣十分傳神,處在上面的外部輸入(Outer input),這裡也就是叢集索引掃描。和處在下面的內部輸入(Inner Input),這裡也就是叢集索引尋找。外部輸入僅僅執行一次,根據外部輸入滿足Join條件的每一行,對內部輸入進行尋找。這裡由於是290行,對於內部輸入執行290次。
可以通過屬性視窗看到.11所示:
圖11.內部輸入的執行次數
根據嵌套迴圈的原理不難看出,由於外部輸入是掃描,內部輸入是尋找,當兩個Join的表外部輸入結果集比較小,而內部輸入所尋找的表非常大時,查詢最佳化工具更傾向於選擇迴圈嵌套方式。
合并串連(Merge Join)
不同於迴圈嵌套的是,合并串連是從每個表僅僅執行一次訪問。從這個原理來看,合并串連要比迴圈嵌套要快了不少。下面來看一個典型的合并串連,12所示。
圖12.合并串連
從合并串連的原理不難想象,首先合并串連需要雙方有序.並且要求Join的條件為等號。因為兩個輸入條件已經有序,所以從每一個輸入集合中取一行進行比較,相等的返回,不相等的捨棄,從這裡也不難看出Merge join為什麼只允許Join後面是等號。從圖11的表徵圖中我們可以看出這個原理。
如果輸入資料的雙方無序,則查詢分析器不會選擇合并串連,我們也可以通過索引提示強制使用合并串連,為了達到這一目的,執行計畫必須加上一個排序步驟來實現有序,13所示。
圖13.通過排序來實現Merge Join
散列串連(Hash Join)
散列串連同樣僅僅只需要只訪問1次雙方的資料。散列串連通過在記憶體中建立散列表實現。這比較消耗記憶體,如果記憶體不足還會佔用tempdb。但並不像合并串連那樣需要雙方有序。一個典型的散列串連14所示。
圖14.散列串連
這裡我刪除了Costomer的叢集索引,否則兩個有序輸入SQL Server會選擇代價更低的合并串連。SQL Server利用兩個上面的輸入產生雜湊表,下面的輸入來探測,可以在屬性視窗看到這些資訊,15所示。
圖15.散列鍵產生和散列鍵探測
通常來說,在兩個輸入資料比較大,且所求資料在其中一方或雙方沒有排序的條件達成時,會選用散列匹配。
並行
當多個表串連時,SQL Server還允許在多CPU或多核的情況下允許查詢並行,這樣無疑提高了效率,一個並行的例子16所示。
圖16.並行提高效率
總結
本文簡單介紹了SQL Server執行計畫中常見的操作極其原理,瞭解這些步驟和原理是最佳化查詢的基本功。