上回我們說到評估一條語句執行效率主要看邏輯IO(啥是邏輯IO,啥是物理IO見聯機文檔),這次我們繼續。
我們先說說,返回多行結果時,爲什麼SQLServer有時會選擇index seek,有時會選擇index scan。
以nonclustered index爲例說明。
像所有的索引B樹一樣,非聚集索引樹也包括完全由索引數據組成的根節點和中間級節點;但是和聚集索引樹不同的是,聚集索引樹葉節點包含的是基礎表的數據頁(我們常說,表的物理存儲順序和聚集索引相同,就是這個原因),非聚集索引樹葉節點是索引頁。SQLServer通過非聚集索引查找數據時,會通過這個非聚集索引鍵值去搜索聚集索引,進而檢索基礎表數據行。
假設有這樣一張表,非聚集索引樹深度爲2,一層根節點(1個索引頁),一層葉節點(4個索引頁)。聚集索引樹深度爲3,一層根節點(1個索引頁),一層中間級節點(2個索引頁),一層葉節點(250頁,也就是基礎表物理存儲頁)表的數據假設1w行。注:所有數據均爲假設,只爲說明原理。
我們首先,再強調一遍,SQLServer獲取數據,總是以頁爲單位,就算是隻讀取一行也會獲取整張頁(見《寫有效率的SQL查詢(I)》)
現在有一條簡單查詢(如:select * from tb where col2 = 99,col2是tb表中的非聚集索引),假設會返回100行。
Ok,我們來分析如果以Index seek來查找這100行會有多少IO。index seek每次都從索引樹根節點開始查找,找到中間級節點(99對應的索引行),然後從該節點行開始連續遍歷所有col2爲99的索引行。在遍歷這些行時,每拿到一條,都會通過該條索引行中聚集索引鍵值去聚集索引樹中index seek,然後從數據頁中獲取數據。在最壞的情況下,col2爲99對應的索引行跨越了全部4個葉級非聚集索引頁(當然,這沒啥可能性,舉例而已,切勿深究);每次通過聚集索引樹進行index seek,IO開銷最壞情況下是一個根節點,一箇中間級節點,一個數據頁,一共要seek100次,開銷300個邏輯IO。綜上,通過nonclustered index seek總共開銷是305個IO。
要知道,我們的基礎表數據頁一共才250頁,這說明了啥?說明就算是我從頭到尾掃描一遍表也比noncustered index seek快。這時,SQL2k5會產生一個完完全全的clustered index scan執行計劃來搞定表掃描。
好了,現在我們再來分析select * from tb1 where col2 = 1。假設它的結果集爲5行。如果這時還是進行nonclustered index seek的話,邏輯IO按照上面相似的分析,應該是19個IO,遠遠要小於整個的clustered index scan。這時,SQLServer自然會採用nonclustered index seek。
我們再來看聚集索引。聚集索引和非聚集索引最大的不同在於聚集索引的存儲順序就是基礎表的物理存儲順序。還是上面的表tb,假設聚集索引建在了col1上.如果where條件是col1 = XX的話,自然是index seek,因爲IO最小,撐死了只有3(一個聚集索引根節點頁,一個聚集索引中間級節點頁,一個數據頁);如果where條件是col1 > XX的話,不管行集是多大,SQLServer總是首先通過index seek拿到XX對應的數據頁,然後挨梆往後遍歷基礎表數據頁到尾巴就OK了。最壞情況XX恰好比表中最小的col1小,那就讀取所有行。如果where條件是col1 < XX,那就倒着檢索聚集索引,無他。
OK,到這裏,我們明白了爲啥SQLServer會選擇index seek和index scan。也順便明白了通過非聚集索引查詢時,結果集相對總行數多寡對查詢計劃選擇的巨大影響。
(結果集/總行數)被稱爲選擇性,比值越大,選擇性就越高。
你得到了它,本文的重點就是選擇性。
統計信息,說白了,就是表中某個字段取某個值時有多少行結果集。統計信息可以說是一種選擇性的度量,SQLServer就是根據它來估算不同查詢計劃的優劣。
後面將通過一個實際的例子來說明統計信息對查詢計劃的影響。
以下是示例表的表結構:
各位可以注意到,該表上有一個identity字段charge_no,聚集索引就創建在它上面。有兩個非聚集索引indx_category_no,indx_provider_no,我們重點關注indx_provider_no。現在來看看provider_no字段的統計信息(有點長,我前邊粘一部分,後邊粘一部分):
(上述各字段含義,見聯機文檔對DBCC SHOW_STATISTICS的描述)
從上面的貼圖可以看到,表中總行數爲1w,採樣行數爲1w。provider_no值爲21的只有1行,而值爲500的行則有4824行。下面兩張圖是兩條SQL的查詢計劃,我就不多嘴解釋了。
那麼問題來了:
我們知道,SQLServer會緩存查詢計劃,假如有這麼一個存儲過程:
create proc myproc
(
@pno int
)
as
select * from charge where provider_no = @pno
第一次我們傳進來一個21,OK,它會緩存該存儲過程的執行計劃爲nonclustered index seek那個。後來我們又傳進來一個500,完蛋了,服務器發現它有一個myproc的緩存,so,又通過nonclustered index seek執行,接着你的同夥看到你的查詢花費了巨量的IO,於是,你被鄙視了。
這說明了啥?說明如果你的查詢選擇性變動劇烈,你應該告訴SQLServer不要緩存查詢計劃,每次都應該重新評估、編譯。實現方法很簡單,查詢的尾巴上加一個option(recompile)好了。而且SQL2k5還有一個nb的feature,可以每次只重新編譯存儲過程的一部分(當然,你也可以選擇重新編譯整個存儲過程,這取決於你的需求。詳見聯機文檔。)
=======彪悍的分割線================================
後面blog會提到索引優化。其實百敬同學那本《SQL性能調校》這方面講的不少了。那本書唯一的缺憾就是某些規則在SQL2k5中不適合。我想我會盡力都寫出來。
原文地址