本文出處:http://www.cnblogs.com/wy123/p/6082338.html html
現實中遇到過到這麼一種狀況:
在某些特殊場景下:進行查詢的時候,加了TOP 1比不加TOP 1要慢(並且是慢不少)的狀況,
也就是說對於符合條件的某種的數據,查詢1條(符合該條件)數據比查詢全部(符合該條件)數據慢的狀況,
這種狀況每每只有在某些特殊條件下會出現,那麼,就有兩個問題:爲何加了TOP 1 會比不加TOP 1慢?這種「特殊條件」是什麼條件?
本文將對此狀況進行演示和原理分析,以及針對此種狀況採用什麼方法來解決。算法
按照一向風格,先造一個測試環境:1000W+的數據
數據的特色爲:
1,表中有一個狀態列BusinessStatus ,這個列的分佈爲1,2,3,4,5
2,表中有一個 業務ID列BusinessId , BusinessId列是呈遞增趨勢 數據庫
CREATE TABLE TestTOP ( Id INT IDENTITY(1,1) primary key, BusinessColumn VARCHAR(50), BusinessId INT, BusinessStatus TINYINT, CreateDate DATETIME ) GO
--5年的時間,一分鐘六條數據的數據頻率 DECLARE @i int = 0 WHILE @i<24*60*365*5 BEGIN INSERT INTO TestTOP VALUES (NEWID(),@i,RAND()*5+1, DATEADD(SS,@i,DATEADD(YEAR,-5,GETDATE()))) INSERT INTO TestTOP VALUES (NEWID(),@i,RAND()*5+1, DATEADD(SS,@i,DATEADD(YEAR,-5,GETDATE()))) INSERT INTO TestTOP VALUES (NEWID(),@i,RAND()*5+1, DATEADD(SS,@i,DATEADD(YEAR,-5,GETDATE()))) INSERT INTO TestTOP VALUES (NEWID(),@i,RAND()*5+1, DATEADD(SS,@i,DATEADD(YEAR,-5,GETDATE()))) INSERT INTO TestTOP VALUES (NEWID(),@i,RAND()*5+1, DATEADD(SS,@i,DATEADD(YEAR,-5,GETDATE()))) INSERT INTO TestTOP VALUES (NEWID(),@i,RAND()*5+1, DATEADD(SS,@i,DATEADD(YEAR,-5,GETDATE()))) SET @i=@i+1 END
另外,在此表中查詢一小部分BusinessStatus=0的分佈較少的數據,且分佈在最大的BusinessId上,這裏暫定爲5000行,利用以下腳本生成 緩存
DECLARE @i int = 15768000 WHILE @i<15768000+5000 BEGIN INSERT INTO TestTOP VALUES (NEWID(),@i,0, DATEADD(SS,@i,GETDATE())) SET @i=@i+1 END
如今這個測試環境已經搭建完成,如今建立兩個非彙集索引,一個是在BusinessStatus上,一個是在BusinessId測試
CREATE INDEX idx_BusinessStatus ON TestTOP(BusinessStatus) CREATE INDEX idx_BusinessId on TestTOP(BusinessId)
下面開始測試:優化
說明:1,如下測試,不用考慮緩存之類的因素,本機測試,內存也足夠大,所有緩存這麼點數據仍是夠的。也暫不分析IO具體值,粗看執行時間已經很明顯了
2,讀者要對SQL Server索引結構,統計信息,執行計劃,執行計劃預估等知識有必定的認識,不然不少理論上的東西就看的雲裏霧裏
3,本文測試數據庫爲SQL Server 2012,SQL Server每一個版本的預估算法可能都不同,具體環境具體分析spa
SELECT TOP 1 比不加 TOP 1慢3d
1,首先執行TOP 1 *的查詢,耗時13秒code
2,而後執行不加TOP 1 *的查詢,也即SELECT * ,以下,耗時0秒(固然不是0秒,意思是很快就能夠完成這個查詢)htm
3,上面兩個查詢就能夠重現第一個問題了,也就是說在當前這種查詢條件下,TOP 1要比不加TOP 1慢不少
分析二者的執行計劃:
首先看加了 TOP 1 的執行計劃:能夠看到走的是idx_BusinessId的索引掃描
接着看不加TOP 1 的執行計劃:能夠看到走的是idx_BusinessStatus這個索引的索引查找
緣由分析:
那麼爲何加了TOP 1就走BusinessId列上的索引掃描,不加TOP 1就走BusinessStatus上的索引掃描?
由於在加了TOP 1以後,只要求返回一條數據,
優化器認爲(應該說是誤認爲)能夠很快找到符合條件的那條記錄,採用了idx_BusinessId列上的索引掃描
因爲數據的分佈可知,符合BusinessStatus=0的BusinessId,是分佈在BusinessId值最大的一小部分數據中,而BusinessId又是遞增的,
也就是說複合條件的數據是集中分佈在idx_BusinessId索引樹的一個很小的特定區域,
採用的是與idx_BusinessId順序一致的(ForWard順序)索引掃描,有數據分佈特色可知,一開始找到的絕大多數的BusinessId,都不是符合BusinessStatus=0的
以致於幾乎要掃描整個idx_BusinessId索引樹才能找到符合BusinessStatus=0條件的數據,所以效率就會很低
反觀不加TOP 1的時候,由於是要找全部符合BusinessStatus=0的數據,優化器就索引採起了idx_BusinessStatus索引查找的方式,至此,緣由大概是這樣的。
問題到這裏纔剛剛開始
若是說上述推斷不足以說明問題,那麼咱們繼續看在加了TOP 1的時候,執行計劃是怎麼預估的?
繼續觀察加了TOP 1的時候的預估,發現此時走idx_BusinessId的索引掃描,預估行數爲3154.6行,這個數字是怎麼獲得的?
如今觀察idx_BusinessStatus列上的統計信息,統計信息是100%取樣的,先不考慮統計信息不許確的問題
由於在加了TOP 1的時候,優化器認爲符合條件的數據是平均分佈在整個表中的,
也就是說BusinessStatus=0的5000行數據是平均分佈在15773000行數據中,查詢條件又要求按照BusinessId正向排序,
那麼幹脆走BusinessId列上的索引掃描,(誤覺得)平均找15773000/5000 行數據,就能夠找到一條(TOP 1)符合條件的數據
其實是不是這樣子呢?用總行數處於BusinessStatus=0的行數,與預估的值比較,都是3154.6呢?那麼上面的推斷也就是成立的
這裏查詢加了TOP 1比不加TOP 1慢的根本緣由就是以下:
事實狀況下是複合條件的數據分佈是不均勻的,而優化器誤覺得符合條件的數據分佈(在整張表中)是均勻的,
正是由於有了這麼一個矛盾,因此在加了TOP 1 的時候,優化器採用非最優化的方式形成的。
繼續測試 TOP N
爲了證實上述推斷,關於TOP的預估,我再補充一個小例子,但願各位看官能明白
當符合條件的數據(BusinessStatus=0)爲15000行的時候,咱們看看TOP 1與TOP 2,以及繼續增長TOP N的值得預估的行數,就大概明白了
DECLARE @i int = 15768000 WHILE @i<15768000+15000 BEGIN INSERT INTO TestTOP VALUES (NEWID(),@i,0, DATEADD(SS,@i,GETDATE())) SET @i=@i+1 END
TOP 1 的預估1052.2 = 1 * RowCount/15000
TOP 2的預估行數 2014.4 = 2 * RowCount/15000
TOP 14 的預估行數 2014.4 = 14 * RowCount/15000
爲何TOP 15開始峯迴路轉,執行計劃也變成index seek了,打破 N * RowCount/15000這個規律??請自行思考
優化器會根據預估返回行數,由於TOP 15的時候,預估行數 =15 * RowCount/15000 =15783.0 >15000 ,
優化器會回頭選擇一種他本身的預估方式較低的方式執行,選擇一個它認爲代價較小(預估行數較小)的執行方式.也即idx_BusinessStatus索引的Index Seek
什麼狀況下才會發生TOP 1要比不加TOP 1慢(或者慢不少)
事實上,相似結構的數據分佈,並不是全部的狀況下都會出現TOP 1比不加TOP 1慢的狀況
那麼何時TOP 1 能夠選擇正確的執行計劃,而非採用低效的執行計劃(排序列上的索引掃描)?
固然是跟符合條件的數據BusinessStatus=0的數據行數有關,只有符合條件的數據(BusinessStatus=0)達到必定數量以後纔會發生(TOP 1比不加TOP 1慢)
上面說了,優化器誤覺得符合條件的數據(BusinessStatus=0)分佈是均勻的,採用了排序列上的索引掃描的執行方式,
即使是優化器誤覺得符合條件的數據(BusinessStatus=0)分佈是均勻的,
採用一開始的預估算法(平均分佈:總行數/符合條件的數據行數)獲得一個值,與符合條件的數據的行數自己對比,若是前者較大,就不會採用排序列上的索引掃描
這裏太拗口了也很難表達清楚,直接上例子吧。
首先我改變符合條件(BusinessStatus=0)的數據的行數,讓複合條件的數據變的少一些,
這裏刪除原來的BusinessStatus=0的5000行數據,插入符合條件的數據爲1000行,而後重建索引,試試看TOP 1 的效果
(插入以後注意重建一下BusinessStatus上的索引,獲得最準確的統計信息)
此時再看SELECT TOP 1的查詢方式,不會走排序列上的索引掃描了,走了查詢條件列(idx_BusinessStatus)的索引查找,效率也上來了。
事實上我這裏說了這麼多,一直在想引出一個問題,那麼符合條件(BusinessStatus=0)這個數據分佈多少,SELECT TOP 1不會引發問題(比不加TOP 1慢)?
根據上述推論,這個值是動態的,大概以下:
假如:X=總行數/符合條件數據行數,Y = 符合條件數據行數
在統計信息徹底準確的請下
若是X>Y,也即:總行數/符合條件數據行數>符合條件數據行數,則會致使在SELECT TOP 1的時候使用排序列的索引掃描替代查詢列的索引查找。
那麼這個閾值是多少?按照這種算法推論,理論上講,就是符合條件的數據的行數等於總行數的平方根,數學推到也很簡單,事實上下面也測試了。
這個閾值在理論上是:3970行左右,
那麼插入符合條件的數據爲3900的時候(小於閾值,也即小於總行數的平方根),SELECT TOP 1是能夠走索引的,以下兩個截圖
修改符合條件(BusinessStatus=0)的數據分佈
而符合條件的數據大於閾值(大於閾值,也即大於總行數的平方根,)的時候,SELECT TOP 1 就開始走排序列的索引掃描,效率開始變慢
事實上致使SELECT TOP 1執行計劃發生變化的這個閾值,具體的數值能夠弄得更加精確,能夠作到大於總行數的平方根一行,或者小於總行數的平方根一行。
但實際上測試發現,這個偏差在三行左右,也就是說閾值具體的值爲總行數的平方根加減三條:POWER(TableRowCount,0.5)±3左右。
固然也不是說「SELECT TOP 1的時候使用排序列的索引掃描替代查詢列的索引查找」永遠是低效的,
想象一下,整個表中絕大多數數據是複合條件的(BusinessStatus=0)的條件下,SELECT TOP 1能夠很快地找到符合條件的一條數據
只是說,在某個閾值區間內,SQL Server查詢引擎在生成執行計劃的時候有一個盲區,此時查詢引擎沒法作出最明智的決定。
實際條件是變幻無窮的,規律是可尋的,不能認死了規律而不考慮實際狀況。
如何解決SELECT TOP 1比不加TOP 1慢的狀況:
上文中說了,查詢加了TOP 1比不加TOP 1慢的根本緣由就是以下:
事實狀況下是複合條件的數據分佈是不均勻的,而優化器誤覺得符合條件的數據分佈(在整張表中)是均勻的,
正是由於有了這麼一個矛盾,因此在加了TOP 1 的時候,優化器採用非最優化的方式形成的。
此時複合條件(BusinessStatus=0)爲一開始的5000行,大於上述閾值
若是此時將查詢條件列和排序列作成一個複合索引,就能夠避免這種狀況,
目的是走這個索引以後,找到的第一條複合條件的數據必定是拍序列上最小的,而且不會由於找多而再次排序浪費CPU時間
好比 create index ix_indexName on TableName(查詢字段列,排序字段列),且複合索引的順序不能改變,本身結合B樹索引的結構想清楚爲何
具體緣由,就很少說了,非要說的話,合理的索引就是讓優化器更加清楚地弄清楚數據分佈,能夠作出更加明智的選擇。
另外能夠針對具體狀況作filter索引,使得索引更加精確
固然也有其餘辦法,好比強制索引等,可是一旦加了強制索引就屏蔽掉優化器的做用了,若是沒辦法保證索引實在任什麼時候候都是比較高效的狀況下,不建議增強制索引。
總結:
本文分析了在某些特定的場景下,重現了SELCET TOP 1比不加TOP 1慢的場景,致使的緣由分析以及解決辦法。 事實上爲了簡明期間,還有很是多有意思的問題還沒有展開,怕是寫的越多,本文的主題就凸顯不出來,有機會再對此還沒有展開的問題繼續進行分析。 補充一點:事實上真要是測試的話,任何一點點小小的改變, 好比查詢語句中BusinessId排序改成DESC,甚至沒有BusinessId上的索引,或者彙集索引創建在其餘列上 均可以免TOP 1比不加TOP 1慢的問題,這裏的目的是爲了重現TOP 1比不加TOP 1慢的現象條件和緣由,以及不改變外因的狀況下如何解決這一問題 謝謝。