分析sqlserver查詢計划


最近使用到sqlserver數據庫並要對查詢語句進行調優,之前接觸的不多,搜索網上一些帖子和查閱微軟msdn資料對sqlserver的查詢計划才大致了解了一些,用這篇文章做個總結。本文主要側重對查詢計划的理解,尤其是對微軟復雜的運算函數的理解,如理解有誤請指出。

如何查看查詢計划

先介紹一下如何查看查詢計划。使用【SQL Server Management Studio】進行查詢,大致有如下幾個方式看查詢計划

1、set showplan_all on 在查詢前顯示計划顯示形式為每個查詢步驟一行

2、set statistics profile on 在查詢后顯示計划並包括每個查詢步驟的掃描行數和執行次數

3、選中sql語句按Ctrl + L(同工具欄顯示預估的查詢計划按鈕) 以圖形方式顯示查詢計划這個還可以使用setshowplan_xml on命令后者生成的xmlSQL Server Management Studio打開就是圖形查詢計划

 

關於查詢計划的說明

在msdn上找到set showplan_all on返回結果各列的說明

 

 

 

具體查詢計划分析

初步分析計划

開啟set showplan_all on

執行如下sql

SELECT dbll.N_SZJY GBM, sum(data.N_SL) NNum

FROM DB_SHARE.dbo.DA_JGXT_VW_QBF_FFJL data

left join DB_SHARE.dbo.T_DBLL dbll

ON dbll.C_ZFBH = data.ZFBH and dbll.D_KSRQ <= data.D_RQ and (dbll.D_JSRQ >= data.D_RQ ORdbll.D_JSRQ IS NULL)

where data.D_RQ >= '2012-07-02 00:00:00.0' and data.D_RQ <= '2012-08-01 23:59:59.0'

GROUP BY dbll.N_SZJY

 

執行計划左半部分

 

 

 接上圖計划的右半部分如下:

 

 

 

Sqlserver的執行計划一出來給人一種特別復雜的感覺有木有,其中命令特別多,還可以看到我們沒有寫出來的語句比如Expr1006Expr1012Bmk1003這都是什么啊?

一步一步來先看看執行計划中每個列的說明

msdn有對執行計划所有運算符的說明

http://msdn.microsoft.com/zh-cn/library/ms191158(v=sql.105)

根據msdn說明,對上面的執行計划分析如下,對於整個計划樹的執行是從下到上,從葉子到根的,最上邊是整個查詢語句:

12、【RID Lookup(OBJECT:([DB_SHARE].[dbo].[T_DBLL] AS [dbll]), SEEK:([Bmk1003]=[Bmk1003]), WHERE:([DB_SHARE].[dbo].[T_DBLL].[D_JSRQ] as [dbll].[D_JSRQ]>=[DB_SHARE].[dbo].[DA_JGXT_VW_QBF_FFJL].[D_RQ] as [data].[D_RQ] OR [DB_SHARE].[dbo].[T_DBLL].[D_JSRQ] as [dbll].[D_JSRQ] IS NULL) LOOKUP ORDERED FORWARD)

這是一個書簽查找步驟,,RIDrecord id Lookup是使用行標示符在堆上進行書簽查找,SEEK后面有一個[Bmk1003]=[Bmk1003],看命名是一個書簽,在11行的DefinedValues列有對此的定義。

 

11、【Index Seek(OBJECT:([DB_SHARE].[dbo].[T_DBLL].[I_DBLL_ZFBH_KSRQ] AS [dbll]), SEEK:([dbll].[C_ZFBH]=[DB_SHARE].[dbo].[DA_JGXT_VW_QBF_FFJL].[ZFBH] as [data].[ZFBH] AND [dbll].[D_KSRQ] <= [DB_SHARE].[dbo].[DA_JGXT_VW_QBF_FFJL].[D_RQ] as [data].[D_RQ]) ORDERED FORWARD)

這是使用I_DBLL_ZFBH_KSRQ索引在T_DBLL表檢索數據,SEEK部分是檢索的具體條件,此步驟定義了Bmk100311步應該先於12執行,看來在同一層級下順序是從上到下的。

11步驟和12步驟的意思是先用I_DBLL_ZFBH_KSRQ索引檢索數據,再使用書簽查找的方式獲取每一行的其他數據。因為I_DBLL_ZFBH_KSRQ是一個非聚集索引,它只包含索引列的數據,實際上對T_DBLL檢索出的數據列還包括N_SZJYD_JSRQ,前者要進行Group,后者是進行與DA_JGXT_VW_QBF_FFJLD_RQ的比對,查詢語句涉及到非聚集索引不包含的列時就要通過書簽查找或聚集索引查找來提取非索引列(T_DBLL此時沒有聚集索引,只能用書簽查找的方式)。

 

10、【Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1003]))

一次嵌套循環連接,將1112步驟數據連接起來,11步驟的數據作為Outer,在進行書簽查找的父步驟一般都是Nested Loops Join,這基本符合內表較大且有索引的條件。

 

9、【Table Scan(OBJECT:([DB_SHARE].[dbo].[DA_JGXT_VW_QBF_FFJL] AS [data]), WHERE:([DB_SHARE].[dbo].[DA_JGXT_VW_QBF_FFJL].[D_RQ] as [data].[D_RQ]>='2012-07-02 00:00:00.000' AND [DB_SHARE].[dbo].[DA_JGXT_VW_QBF_FFJL].[D_RQ] as [data].[D_RQ]<='2012-08-01 23:59:59.000'))

DA_JGXT_VW_QBF_FFJL表全表掃描數據,條件是D_RQ在一個時間段內。顯然這是需要增加索引的

 

8、【Parallelism(Repartition Streams, RoundRobin Partitioning)

這是什么呢?看看微軟古板的說明“Parallelism 運算符執行分發流、收集流和對流重新分區邏輯操作。Argument 列可以包含一個 PARTITION COLUMNS:() 謂詞和一個以逗號分隔的分區列的列表。Argument 列還可以包含一個 ORDER BY:() 謂詞,以列出分區過程中要保留排序順序的列。Repartition Streams 運算符處理多個流並生成多個記錄流。記錄的內容和格式不會改變”。大約是這樣,Parallelism表示查詢會被並行執行(如果服務器負荷太高可能最終不會並行),這算是sqlserver的一個優化處理,如果服務器處理多任務能力強這就會比串行更有效率,這個並行應該是表示和其他任務的關系,由於10步驟會依賴8步驟的數據,8步驟應該先於10步驟執行

 

7、【Nested Loops(Left Outer Join, OUTER REFERENCES:([data].[ZFBH], [data].[D_RQ], [Expr1012]) WITH UNORDERED PREFETCH)

一次嵌套循環連接,它將810兩個步驟得到的數據連接,Outer表是DA_JGXT_VW_QBF_FFJL,因OUTER REFERENCES中的字段都是屬於該表。

這里出現了Expr1012,這是個神秘的列,聯系上下語句,這個列應該是T_DBLLN_SZJY,因為后面要用此列數據進行分組。

 

6、【Sort(ORDER BY:([dbll].[N_SZJY] ASC))

使用N_SZJY列排序,后面要進行Group,這里必須先進行一次排序

 

5、【Stream Aggregate(GROUP BY:([dbll].[N_SZJY]) DEFINE:([partialagg1007]=COUNT_BIG([DB_SHARE].[dbo].[DA_JGXT_VW_QBF_FFJL].[N_SL] as [data].[N_SL]), [partialagg1009]=SUM([DB_SHARE].[dbo].[DA_JGXT_VW_QBF_FFJL].[N_SL] as [data].[N_SL])))

名詞解釋,“Stream Aggregate 運算符按一列或多列對行分組,然后計算查詢返回的一個或多個聚合表達式。此運算符的輸出可供查詢中的后續運算符引用和/或返回到客戶端。Stream Aggregate 運算符要求輸入在組中按列進行排序。如果由於前面的 Sort 運算符或已排序的索引查找或掃描導致數據尚未排序,優化器將在此運算符前面使用一個 Sort 運算符

繼續根據微軟生硬的解釋進行分析,這行的處理就是進行一個分組,因為GROUP BY dbll.N_SZJY一句而產生此行處理。其中還有一個COUNT_BIG,這是一個類似COUNT的函數,區別是前者返回bigint后者返回int,不過語句中只有個sum沒有count,為什么還要計算呢?跳過先。

 

4、【Parallelism(Gather Streams, ORDER BY:([dbll].[N_SZJY] ASC))

又一個並行查詢。“Gather Streams 運算符僅用在並行查詢計划中。Gather Streams 運算符處理幾個輸入流並通過組合這幾個輸入流生成單個記錄輸出流。不更改記錄的內容和格式。如果此運算符保留順序,則所有的輸入流都必須有序。如果輸出已排序,則參數列包含一個 ORDER BY:() 謂詞和正在排序的列名稱。 Gather Streams函數把輸入流組合,不過4行只有一個子節點,我理解到4之前仍存在兩個輸入流,分別來自DA_JGXT_VW_QBF_FFJL表和T_DBLL表,上面的步驟對兩個輸入流分別有索引條件過濾和分組,但仍然沒有整合,在Gather Streams步驟將兩個輸入流數據整合起來,那么之后的數據看到的就是一個輸入流了

3、【Stream Aggregate(GROUP BY:([dbll].[N_SZJY]) DEFINE:([globalagg1008]=SUM([partialagg1007]), [globalagg1010]=SUM([partialagg1009])))

再次進行流聚合。這里和第5步驟的不同在哪里

5步驟DefinedValues中有如下定義

[partialagg1007]=COUNT_BIG([DB_SHARE].[dbo].[DA_JGXT_VW_QBF_FFJL].[N_SL] as [data].[N_SL]), [partialagg1009]=SUM([DB_SHARE].[dbo].[DA_JGXT_VW_QBF_FFJL].[N_SL] as [data].[N_SL])

而此步驟執行了

[globalagg1008]=SUM([partialagg1007]), [globalagg1010]=SUM([partialagg1009])

這就是它們的不同,此步驟再次用N_SZJY分組對之前的計算結果求和,這應該是個避免整合流后分組數據出現重復的操作。

2、【Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [globalagg1008]=(0) THEN NULL ELSE [globalagg1010] END))

Compute Scalar的意思是計算標量。標量,相對於向量而言,無方向數據,就是計算一個數值。這個步驟sqlserver優化器執行了一個case whenglobalagg1008是針對每個N_SZJYcountN_SL),看來之前自動執行count是為了此步,globalagg1010是針對每個N_SZJYsumN_SL),對照我們的語句是SELECT dbll.N_SZJY GBM, sum(data.N_SL) NNum此步驟的意圖看來sqlserver會對sum特殊情況的檢測,對於整個語句而言就是如果T_DBLL表中N_SZJY列有數據而DA_JGXT_VW_QBF_FFJLN_SL都為null,那么sum就返回null

 

1、【整條語句】。作為查詢計划樹的根節點,在計划列表中大多數列都是Null,這個計划的估算行是12行,僅供參考,實際查詢的結果是1

 

關於表掃描

Sql Server 會有以下方法來查找您需要的數據記錄:
1. 
Table Scan】:遍歷整個表,查找所匹配的記錄行。這個操作將會一行一行的檢查,當然,效率也是最差的。
2. 
Index Scan】:根據非聚集索引,掃描索引的全部記錄,查找所匹配的記錄行,匹配的條件可從查詢計划的Argument 列中看到。比第一種方式的查找范圍要小(B+索引葉子之間有指針類似鏈表),因此比【Table Scan】要快。
3. 
Index Seek】:根據非聚集索引,定位(獲取)記錄的存放位置,然后取得記錄(這會使用B+索引查找樹的定位算法,基本一條記錄2-4IO,取決於表數據量產生的索引樹高度),這個方式比起前二種方式會更快。
4. 
Clustered Index Scan】:根據聚集索引掃描全部記錄。這個的效率要根據實際情況分析。出現這個步驟可能是效率很差的表現,因為如果條件中的列沒有索引,數據庫引擎在提取數據的時會考慮進行優化,基於磁盤順序讀比隨機讀快的原理,數據按照聚集索引的順序存放,那么用聚集索引來提取數據是一種對更差方式的優化。

比如DA_JGXT_VW_QBF_FFJL表有715455條記錄,

如下記錄返回8條記錄,優化器使用Clustered Index Scan

select * from DB_SHARE.dbo.DA_JGXT_VW_QBF_FFJL where N_ID = 14000

執行時間10s,計划如下,IO消耗是7.21多,此時使用聚集索引掃描來順序提取數據,這個步驟在這里就是避免更差的隨機磁盤讀取

 

  

如下語句返回24條記錄優化器使用Clustered Index Seek

select * from DB_SHARE.dbo.DA_JGXT_VW_QBF_FFJL where D_RQ = '2012-07-25 00:00:00.0'

執行時間0s毫秒級,計划如下,IO消耗是0.000232

 

  

 而直接使用select top 10 * from DB_SHARE.dbo.DA_JGXT_VW_QBF_FFJL查詢計划也會使用Clustered Index Scan

select top 10 * from DB_SHARE.dbo.DA_JGXT_VW_QBF_FFJL

|--Top(TOP EXPRESSION:((10)))

|--Clustered Index Scan (OBJECT:([DB_SHARE].[dbo].[DA_JGXT_VW_QBF_FFJL].[I_DA_JGXT_VW_QBF_FFJL_RQ]))

對於這個無條件的語句這個計划已經是最優的了
5. 
Clustered Index Seek】:根據聚集索引獲取記錄,不解釋,最快!

 

優化Table Scan

使用日期過濾數據,對D_RQ建立聚集索引 

CREATE CLUSTERED INDEX I_DA_JGXT_VW_QBF_FFJL_RQ ON dbo.DA_JGXT_VW_QBF_FFJL(D_RQ)

 

再次查看執行計划

左半部分:

 

 

 

右半部分

 

  

Table Scan變為Clustered Index Seek,看右半部分計划中的EstimateRowsEstimateIOEstimateCPUTocalSubTreeCost等都有很大提升

優化RID Lookup

前面提到RID Lookup是使用非聚集索引時提取了索引外的列產生的一種操作,中文解釋為書簽查找。

微軟有一篇專門的文章http://blogs.msdn.com/b/craigfr/archive/2006/06/30/652639.aspx對此作出了解釋。每次書簽查找會產生一次隨機IO,隨機IO對於磁盤來說是比較耗費資源的,雖然sqlserver優化器認為這個比不用索引的消耗小些因而選擇了這個方式,但可能的情況下我們還是要考慮優化。

優化書簽查找的方式大致兩種,一種是給目前已經使用的索引加入要查詢的列,使得查詢的列都在索引中;另一種是使索引成為聚集索引。那么可以考慮創建(C_ZFBH, D_KSRQ, D_JSRQ, N_SZJY)4鍵聯合索引或將(C_ZFBH,D_KSRQ)的索引改為聚集索引。

我先采用了聚集索引優化,查詢計划的IOCPUCost都有所提升,主要是TotalSubTreeCost一項提升較多。按照微軟對此項的說明為查詢開銷,這是一個綜合的數值,一般這個開銷較小的更好,不過也不絕對。

  

 

那么如果用4鍵聯合索引呢?

我發現對比兩者的查詢計划相差很小,於是我用了set statistics profile on來查看實際的執行情況,這個開關比前面的計划多兩列,會返回每個步驟的實際掃描行數和執行次數

列名

說明

Rows

各運算符生成的實際行數

Executes

運算符執行的次數

先看聚集索引,本次執行耗費8s

  

 

 

再看4鍵聯合索引,本次執行耗費12s,值得一提的是此時兩個索引都存在,是sqlserver優化器選擇了4鍵聯合索引

 

 

 

相比之下,后者的TotalSubTreeCost較小因IO少,但是前者實際掃描的行數較少,且執行時間更短。我在執行前已使用了dbcc dropcleanbuffersdbcc freeproccache清除緩存,不過我使用的數據庫是虛擬機,在執行效率上經常有波動,后者的執行時間長可能因為索引還沒有全部加載到內存中,實際測試時有時后者的時間更短。不過鑒於D_JSRQ是存在空值並且檢索的時候都要使用(D_JSRQ>日期 or D_JSRQ is null)這樣的條件,前者可能更好。

 

推薦閱讀

看懂Sqlserver查詢計划

http://www.cnblogs.com/fish-li/archive/2011/06/06/2073626.html

msdn邏輯運算符和物理運算符引用

http://msdn.microsoft.com/zh-cn/library/ms191158(v=sql.105)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM