SELECT TOP 1 比不加TOP 1 慢的緣由分析以及SELECT TOP 1語句執行計劃預估原理

 

本文出處: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慢的現象條件和緣由,以及不改變外因的狀況下如何解決這一問題    謝謝。

相關文章
相關標籤/搜索