SQL Server調優系列基礎篇(經常使用運算符總結——三種物理鏈接方式剖析)

前言html

上一篇咱們介紹瞭如何查看查詢計劃,本篇將介紹在咱們查看的查詢計劃時的分析技巧,以及幾種咱們經常使用的運算符優化技巧,一樣側重基礎知識的掌握。算法

經過本篇能夠了解咱們日常所寫的T-SQL語句,在SQL Server數據庫系統中是如何分解執行的,數據結果如何經過各個運算符組織造成的。數據庫

技術準備併發

基於SQL Server2008R2版本,利用微軟的一個更簡潔的案例庫(Northwind)進行解析。app

1、數據鏈接函數

數據鏈接是咱們在寫T-SQL語句的時候最經常使用的,經過兩個表之間關聯獲取想要的數據。oop

SQL Server默認支持三種物理鏈接運算符:嵌套循環鏈接、合併鏈接以及哈希鏈接。三種鏈接各有用途,各有特色,不一樣的場景會數據庫會爲咱們選擇最優的鏈接方式。post

 

a、嵌套循環鏈接(nested loops join)性能

嵌套循環鏈接是最簡單也是最基礎的鏈接方式。兩張表經過關鍵字進行關聯,而後經過雙層循環依次進行兩張表的行進行關聯,而後經過關鍵字進行篩選。大數據

能夠參照下圖進行理解分析

其實嵌套掃描是很簡單的獲取數據的方式,簡單點就是兩層循環過濾出結果值。

咱們能夠經過以下代碼加深理解

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

舉個列子

SELECT o.OrderID
FROM Customers C JOIN Orders O
ON C.CustomerID=O.CustomerID
WHERE C.City=N'London'

以上這個圖標就是嵌套循環鏈接的圖標了。並且解釋的很明確。

這種方法的消耗就是外表和內表的乘積,其實就是咱們所稱呼的笛卡爾積。因此消耗的大小是隨着兩張表的數據量增大而增長的,尤爲是內部表,由於它是屢次重複掃描的,因此咱們在實踐中的採起的措施就是減小每一個外表或者內表的行數來減小消耗。

對於這種算法還有一種提升性能的方式,由於兩張表是經過關鍵字進行關聯的,因此在查詢的時候對於底層的數據獲取速度直接關乎着此算法的性能,這裏優化的方式儘可能使用兩個表關鍵字爲索引查詢,提升查詢速度。

還有一點就是在嵌套循環鏈接中,在兩張表關聯的時候,對外表都是有篩選條件的,好比上面例子中【WHERE C.City=N'London'】就是對外表(Customers)的篩選,而且這裏的City列在該表中存在索引,因此該語句的兩個子查詢都爲索引查找(Index Seek)。

可是,有些狀況咱們的查詢條件不是索引所覆蓋的,這時候,在嵌套循環鏈接下的子運算符就變成了索引掃描(Index scan)或者RID查找。

舉個例子

SELECT E1.EmployeeID,COUNT(*)
FROM Employees E1 JOIN Employees E2
ON E1.HireDate>E2.HireDate
GROUP BY E1.EmployeeID

以上代碼是從職工表中獲取出每位職工入職前的人員數。咱們看一下該查詢的執行計劃

這裏很顯然兩個表的關聯經過的是HireDate列進行,而此列又不爲索引項所覆蓋,因此兩張表的獲取只能經過全表的彙集索引掃描進行,若是這兩張表數據量特別大的話,無疑又是一個很是耗性能的查詢。

經過文本能夠看出,該T-SQL的查詢結果的獲取是經過在嵌套循環運算符中,對兩個表通過全表掃描以後造成的笛卡兒積進行過濾篩選的。這種方式其實不是一個最優的方式,由於咱們獲取的結果實際上是能夠先經過兩個表過濾以後,再經過嵌套循環運算符獲取結果,這樣的話性能會好不少。

咱們嘗試改一下這個語句

SELECT E1.EmployeeID,ECNT.CNT 
FROM Employees E1 CROSS APPLY
(
   SELECT COUNT(*) CNT
   FROM Employees E2
   WHERE E1.HireDate<E2.HireDate
)ECNT

經過上述代碼查詢的結果項,和上面的是同樣的,只是咱們根據外部表的結果對內部表進行了過濾,這樣執行的時候就不須要獲取所有數據項了。

咱們查看下文本執行計劃

咱們比較一下,先後兩條語句的執行消耗,對比一下執行效率

 

 執行時間從1秒179毫秒減小至93毫秒。效果明顯。

對比CPU消耗、內存、編譯時間等整體消耗都有所下降,參考上圖。

因此對嵌套循環鏈接鏈接的優化方式就是集中在這幾點:對兩張表數據量的減小、鏈接關鍵字上創建索引、謂詞查詢條件上覆蓋索引最好能減小符合謂詞條件的記錄數。

 

b、合併鏈接(merge join)

上面提到的嵌套循環鏈接方式存在着諸多的問題,尤爲不適合兩張表都是大表的狀況下,由於它會產生N屢次的全表掃描,很顯然這種方式會嚴重的消耗資源。

鑑於上述緣由,在數據庫裏又提供了另一種鏈接方式:合併鏈接。記住這裏沒有說SQL Server所提供的,是由於此鏈接算法是市面全部的RDBMS所共同使用的一種鏈接算法。

合併鏈接是依次讀取兩張表的一行進行對比。若是兩個行是相同的,則輸出一個鏈接後的行並繼續下一行的讀取。若是行是不相同的,則捨棄兩個輸入中較少的那個並繼續讀取,一直到兩個表中某一個表的行掃描結束,則執行完畢,因此該算法執行只會產生每張表一次掃描,而且不須要整張表掃描完就能夠中止。

 

該算法要求按照兩張表進行依次掃描對比,可是有兩個前提條件:一、必須預先將兩張表的對應列進行排序;二、對兩張表進行合併鏈接的條件必須存在等值鏈接。

咱們能夠經過如下代碼進行理解

get first row R1 from input1
get first row R2 from input2
while not at the end of either input
begin
     if R1 joins with R2
         begin
              output(R1,R2)
              get next row R2 from input2
         end
     else if R1<R2   
             get next row R1 from input1
          else
             get next row R2 from input2
end              

合併鏈接運算符總的消耗是和輸入表中的行數成正比的,並且對錶最多讀取一次,這個和嵌套循環鏈接不同。所以,合併鏈接對於大表的鏈接操做是一個比較好的選擇項。

對於合併鏈接能夠從以下幾點提升性能:

  1. 兩張表間的鏈接值內容列類型,若是兩張表中的關聯列都爲惟一列,也就說都不存在重複值,這種關聯性能是最好的,或者有一張表存在惟一列也能夠,這種方式關聯爲一對多關聯方式,這種方式也是咱們最經常使用的,比咱們常用的主從表關聯查詢;若是兩張表中的關聯列存在重複值,這樣在兩表進行關聯的時候還須要藉助第三張表來暫存重複的值,這第三張表叫作」worktable 「是存放在Tempdb或者內存中,而這樣性能就會有所影響。因此鑑於此,咱們常作的優化方式有:關聯連儘可能採用彙集索引(惟一性)
  2. 咱們知道採用該種算法的前提是,兩張表都通過排序,因此咱們在應用的時候,最好優先使用排序後的表關聯。若是沒有排序,也要選擇的關聯項爲索引覆蓋項,由於大表的排序是一個很耗資源的過程,咱們選擇索引覆蓋列進行排序性能要遠遠好於普通列的排序。

咱們來舉個例子

SELECT O.CustomerID,C.CustomerID,C.ContactName 
FROM Orders O JOIN Customers C
ON O.CustomerID=C.CustomerID

咱們知道這段T-SQL語句中關聯項用的是CustomerID,而此列爲主鍵彙集索引,都是惟一的而且通過排序的,因此這裏面沒有顯示的排序操做。

並且凡是採用合併鏈接的全部輸出結果項,都是已經通過排序的。

咱們找一個稍複雜的狀況,沒有提早排序的利用合併查詢的T-SQL

SELECT O.OrderID,C.CustomerID,C.ContactName
FROM Orders O JOIN Customers C
ON O.CustomerID=C.CustomerID AND O.ShipCity<>C.City
ORDER BY C.CustomerID

上述代碼返回那些客戶的發貨訂單不在客戶本地的。

上面的查詢計劃能夠看出,排序的消耗老是巨大的,其實咱們上面的語句按照邏輯應該是在合併鏈接獲取數據後,才採用顯示的按照CustomerID進行排序。

可是由於合併鏈接運算符以前自己就須要排序,因此此處SQL Server採起了優先排序的策略,把排序操做提早到了合併鏈接以前進行,而且在合併鏈接以後,就不須要在作額外的排序了。

這其實這裏咱們要求對查詢結果排序,正好也利用了合併鏈接的特色。

 

c、哈希鏈接(hash join) 

咱們分析了上面的兩種鏈接算法,兩種算法各有特色,也各有本身的應用場景:嵌套循環鏈接適合於相對小的數據集鏈接,合併鏈接則應對與中型的數據集,可是又有它本身的缺點,好比要求必須有等值鏈接,而且須要預先排序等。

那對於大型的數據集合的鏈接數據庫是怎麼應對的呢?那就是哈希鏈接算法的應用場景了。

哈希鏈接對於大型數據集合的並行操做上都比其它方式要好不少,尤爲適用於OLAP數據倉庫的應用場景中。

哈希鏈接不少地方和合並鏈接相似,好比都須要至少一個等值鏈接,一樣支持全部的外鏈接操做。但不一樣於合併鏈接的是,哈希鏈接不須要預先對輸入數據集合排序,咱們知道對於大表的排序操做是一個很大的消耗,因此去除排序操做,哈希操做性能無疑會提高不少。

哈希鏈接在執行的時候分爲兩個階段:

  • 構建階段

在構建階段,哈希鏈接從一個表中讀入全部的行,將等值鏈接鍵的行機型哈希話處理,而後建立造成一個內存哈希表,而將原來列中行數據依次放入不一樣的哈希桶中。

  • 探索階段

在第一個階段完成以後,開始進入第二個階段探索階段,該階段哈希鏈接從第二個數據表中讀入全部的行,一樣也是在相同的等值鏈接鍵上進行哈希。哈希過程桶上一階段,而後再從哈希表中探索匹配的行。

上述的過程當中,在第一個階段的構建階段是阻塞的,也就是說在,哈希鏈接必須讀入和處理全部的構建輸入,以後才能返回行。並且這一過程是須要一塊內存存儲提供支持,而且利用的是哈希函數,因此相應的也會消耗CPU等。

而且上述流程過程當中通常採用的是併發處理,充分利用資源,固然系統會對哈希的數量有所限制,若是數據量超大,也會發生內存溢出等問題,而對於這些問題的解決,SQL Server有它自身的處理方式。

咱們可經過如下代碼進行理解

--構建階段
for each row R1 in the build table
begin
   calculate hash value on R1 join key(s)
   insert R1 into the appropriate hash bucket
end
--探索階段
for each row R2 in the probe table
begin
   calculate hash value on R2 join key(s)   
   for each row R1 in the corresponding hash bucket
       if R1 joins with R2
          output(R1,R2)
end    

在哈希鏈接執行以前,SQL Server會估算須要多少內存來構建哈希表。基本估算的方式就是經過表的統計信息來估算,因此有時候統計信息不許確,會直接影響其運算性能。

SQL Server默認會盡力預留足夠的內存來保證哈希鏈接成功的構建,可是有時候內存不足的狀況下,就必須採起將一小部分的哈希表分配到硬盤中,這裏就存入到了tempdb庫中,而這一過程會反覆屢次循環執行。

舉個列子來看看

SELECT O.OrderID,O.OrderDate,C.CustomerID,C.ContactName
FROM Orders O JOIN Customers C
ON O.CustomerID=C.CustomerID

咱們來分析上面的執行語句,上面的執行結果經過CustomerID列進行關聯,理論將最合適的應該是採用合併鏈接操做,可是合併鏈接須要排序,可是咱們在語句中沒有指定Order by 選項,因此通過評估,此語句採用了哈希鏈接的方式進行了鏈接。

咱們給它加上一個顯示的排序,它就選用合併鏈接做爲最優的鏈接方式

咱們來總結一下這個算法的特色

  • 和合並鏈接同樣算法複雜度基本就是分別遍歷兩邊的數據集各一遍
  • 它不須要對數據集事先排序,也不要求上面有什麼索引,經過的是哈希算法進行處理
  • 基本採起並行的執行計劃的方式

 可是,該算法也有它自身的缺點,由於其利用的是哈希函數,因此運行時對CPU消耗高,一樣對內存也比較大,可是它能夠採用並行處理的方式,因此該算法用於超大數據表的鏈接查詢上顯示出本身獨有的優點。

關於哈希算法在哈希處理過程的時候對內存的佔用和分配方式,是有它本身獨有哈希方法,好比:左深度樹、右深度樹、濃密哈希鏈接樹等,這裏不作詳細介紹了,只須要知道其使用方式就能夠了。

Hash Join並非一種最優的鏈接算法,只是它對輸入不優化,由於輸入數據集特別大,而且對鏈接符上有沒有索引也沒要求。其實這也是一種不得已的選擇,可是該算法又有它適應的場景,尤爲在OLAP的數據倉庫中,在一個系統資源相對充足的環境下,該算法就獲得了它發揮的場景。

固然前面所介紹的兩種算法也並非一無可取,在業務的OLTP系統庫中,這兩種輕量級的鏈接算法,以其自身的優越性也得到了承認。

因此這三種算法,沒有誰好誰壞,只有合適的場景應用合適的鏈接算法,這樣才能發揮它自身的長處,而恰巧這些就是咱們要掌握的技能。

 

這三種鏈接算法咱們也能夠顯示的指定,可是通常不建議這麼作,由於默認SQL Server會爲咱們評估最優的鏈接方式進行操做,固然有時候它評估不對的時候就須要咱們本身指定了,方法以下:

 

2、聚合操做

聚合也是咱們在寫T-SQL語句的時候常常遇到的,咱們來分析一下一些經常使用的聚合操做運算符的特性和可優化項。

a、標量聚合

標量聚合是一種經常使用的數據聚合方式,好比咱們寫的語句中利用的如下聚合函數:MAX()、MIN()、AVG()、COUNT()、SUM()

以上的這些數據結果項的輸出基本都是經過流聚合的方式產生,而且這個運算符也被稱爲:標量聚合

先來看一個列子

SELECT COUNT(*) FROM Orders

上面的圖表就是流聚合的運算符了。

上圖還有一個計算標量的運算符,這是由於在流聚合產生的結果項數據類型爲Bigint類型,而默認輸出爲int類型,因此增長了一個類型轉換的運算符。

咱們來看一個不須要轉換的

SELECT MIN(OrderDate),MAX(OrderDate) FROM Orders

看一下求平均數的運算符

SELECT AVG(Freight) FROM Orders

求平均數的時候,在SQL Server執行的時候也給咱們添加了一個case when分類,防止分母爲0的狀況發生。

咱們來看DISTINCT下的狀況下,執行計劃

SELECT COUNT(DISTINCT ShipCity) FROM Orders
SELECT COUNT(DISTINCT OrderID) FROM Orders

上面相同的語句,可是產生了不一樣的執行計劃,只是由於發生在不一樣列的數量彙總上,由於OrderID不存在重複列,因此SQL Server不須要排序直接流聚合就能夠產生彙總值,而ShipCity不一樣它會有重複的值,因此只能通過排序後再流聚合依次獲取彙總值。

 

其實,流聚合這種算法最經常使用的方式是分組(GROUP BY)計算,上面的標量計算也是利用這個特性,只不過把總體造成了一個大組進行聚合。

我麼經過以下代碼理解

clear the current aggredate results
clear the current group by columns
for each input row
begin
    if the input row does not match the current group by columns
    begin
       output the current aggreagate results(if any)
       clear the current aggreagate results
       set the current group by columns to the input row
    end
   update the aggregate results with the input row
end

流聚合運算符其實過程很簡單,維護一個聚合組和聚合值,依次掃描表中的數據,若是能不匹配聚合組則忽略,若是匹配,則加入到聚合組中而且更新聚合值結果項。

舉個例子

SELECT ShipAddress,ShipCity,COUNT(*)
FROM Orders
GROUP BY ShipAddress,ShipCity

這裏使用了流聚合,而且以前先對兩列進行排序,排序的消耗老是很大。

以下代碼就不會產生排序

SELECT CustomerID,COUNT(*)
FROM Orders
GROUP BY CustomerID

因此這裏咱們已經總結出對於流聚合的一種優化方式:儘可能避免排序產生,而要避免排序就須要將分組(Group by)字段在索引覆蓋範圍內。

 

b、哈希聚合

上述的流聚合的方式須要提早排序,咱們知道排序是一個很是大的消耗過程,因此不適合大表的分組聚合操做,爲了解決這個問題,又引入了另一種聚合運算:哈希聚合

所謂的哈希聚合內部的方法和本篇前面提到的哈希鏈接機制同樣。

哈希聚合不須要排序和過大的內存消耗,而且很容易並行執行計劃,利用多CPU同步進行,可是有一個缺點就是:這一過程是阻塞的,也就說哈希聚合不會產生任何結果直到完整的輸入。

因此在大數據表中採用哈希聚合是一個很好的應用場景。

經過以下代碼加深理解

for each input row
begin
   calculate hash value on group by columns
   check for a matching row in the hash table
   if maching row not found
      insert a new row into the hash table
   else
      update the matching row with the input row
end
--最後輸出結果
ouput all rows in the hash table        

簡單點將就是在進行運算匹配前,先將分組列進行哈希處理,分配至不一樣的哈希桶中,而後再依次匹配,最後才輸出結果。

舉個例子

SELECT ShipCountry,COUNT(*)
FROM Orders
GROUP BY ShipCountry

 

這個語句頗有意思,咱們利用了ShipCountry進行了分組,咱們知道該列沒有被索引覆蓋,按照道理,其實選擇流聚合應該也是不錯的方式,跟上面咱們列舉的列子同樣,先對這個字段進行排序,而後利用流聚合造成結果項輸出。

可是,爲何這個語句SQL Server爲咱們選擇了哈希匹配做爲了最優的算法呢!!!

我麼來比較兩個分組字段:ShipCountry和前面的ShipAddress

前面是國家,後面是地址,國家是不少重複的,而且只有少數的惟一值。而地址就不同了,離散型的分佈,咱們知道排序是很耗資源的一件事情,可是利用哈希匹配只須要將不一樣的列值進行提取就能夠,因此相比性能而言,無疑哈希匹配算法在這裏是略勝一籌的算法。

而上面關於這兩列內容分佈類型SQL Server是怎樣知道的?這就是SQL Server的強大的統計信息在支撐了。

在SQL Server中並非固定的語句就會造成特定的計劃,而且生成的特定計劃也不是老是最優的,這和數據庫現有數據表中的內容分佈、數據量、數據類型等諸多因素有關,而記錄這些詳細信息的就是統計信息。

全部的最優計劃的選擇都是基於現有統計信息來評估,若是咱們的統計信息未及時更新,那麼所評估出來最優的執行計劃將不是最好的,有時候反而是最爛的。 

 

參考文獻

結語

此篇文章先到此吧,本篇主要介紹了關於T-SQL語句調優從執行計劃下手,並介紹了三個常見的鏈接運算符和聚合操做符,下一篇將着重介紹咱們其它最經常使用的一些運算符和調優技巧,包括:CURD等運算符、聯合運算符、索引運算、並行運算等吧,關於SQL Server性能調優的內容涉及面很廣,後續文章中依次展開分析。 

文章最後給出上一篇的鏈接

SQL Server調優系列基礎篇

 

 

若是您看了本篇博客,以爲對您有所收穫,請不要吝嗇您的「推薦」。

相關文章
相關標籤/搜索