1. 程式人生 > >SQL Server 中統計資訊直方圖中對於沒有覆蓋到謂詞預估以及預估策略的變化(SQL2012-->SQL2014-->SQL2016)

SQL Server 中統計資訊直方圖中對於沒有覆蓋到謂詞預估以及預估策略的變化(SQL2012-->SQL2014-->SQL2016)

統計資訊寫過幾篇了相關的文章了,感覺還是不過癮,關於統計資訊的問題,最近又踩坑了,該問題雖然不算很常見,但也比較有意思。
相對SQL Server 2012,發現在新的SQL Server版本(2014,2016)中都有一些明顯的變化,下文將對此進行粗淺的分析。

SQL Server 2012中(包括之前的版本),因表中資料變化,但統計資訊尚未更新的情況下,對於直方圖中沒有覆蓋到的謂詞過濾時,sqlserver總是預估為1行
SQL Server 2014和 Server 2016中這種估算方式都有所變化,從表現看,對於對於沒有覆蓋到的謂詞過濾的預估,每個版本都是不同的。
本文簡單測試一下此種情況在SQL Server 2012,SQL Server 2014,SQL Server 2016的不同表現,以及該問題可能造成的潛在影響。

下面涉及到的測試環境的資料庫版本如下

測試環境準備

首先利用如下指令碼,建一張測試表,寫入測試資料,下面會解釋測試資料的分佈

create table A
(
    IdentifierId    int identity(1,1),
    Id1                int,
    Id2                int,
    OtherCol        CHAR(500)
)
GO

begin tran
declare @i int = 1
while @i<=1000000
begin
    insert into
A values ((@i/50000)+1,@i,NEWID()) set @i = @i+1 if (@i%500000)=0 begin if @@TRANCOUNT>0 begin commit begin tran end end end if @@TRANCOUNT>0 begin commit end GO

插入的測試資料的分佈如下,Id1是從1~20,每一個Id1對應50000個不同的Id2

統計資訊直方圖中覆蓋到的謂詞的預估

  測試:根據直方圖中的任何一個Id來做查詢,查詢之前先建立相關列上的統計資訊,發現預估行數是絕對準確的。

  

  檢視idx_1上的統計資訊,上面預估的絕對準確就歸結於統計資訊100%的取樣統計以及Rang_Hi_key的EQ_Rows,直方圖中的Id1的分佈是1~21

  

統計資訊直方圖中未覆蓋到的謂詞的預估

  繼續插入一個與上面Id2都不一樣的資料,這裡為50,因為此時插入的是50000行資料,同時又不足以觸發統計資訊更新,因此發生如下寫入資料之後,統計資訊並不會更新。
  因此這個插入完成之後,統計資訊並沒有更新。

    

  因為統計資訊沒有更新,在idx_1的直方圖中,是沒有Id1=50的資訊的,也就說Id1=50不存在於統計資訊的直方圖中,
  在SQL Server 2012中預估的結果:預估為1行,實際為50000行

  

  重複以上測試程式碼,分別在SQL Server 2014和SQL Server 2016中測試,不重複截圖了

  SQL Server 2014中測試如下:行預估為1024.7,實際為50000,
  這個值是通過什麼方式計算出來的?暫時還沒查到資料。

  

  可以確定的是,對於類似情況的預估演算法,也就是謂詞沒有包含在統計資訊直方圖中的情況下(one specifies a value which is out of range of the current statistics)
  在sqlserver 2014中,經測試,不同情況下預估是不一樣的,不是固定的預估為1行,也不是固定預估為的0.1%,也不是簡單的Rows Sampled*All density

  SQL Server 2016中測試如下: 預估為49880.8,實際為50000,基本上接近於真實值。
  相對於SQL Server 2012和2014的預估結果,這個預估的準確性看起來還是比較吊的。

  

  為什麼SQL Server 2016中預估的如此準確?
  因為在SQL Server 2016中,對於直方圖中不存在的過濾謂詞,在用這個謂詞進行查詢的時候,會自動更新相關的統計資訊,然後再執行查詢,
  這個特性,相對於SQL Server 2012和2014來說,是全新的,也是非常實用的。
  SQL Server 2014這個預估策略雖然在2012的基礎上做出了一些改進,但是還是沒有解決本質問題,以至於人仍舊要人為地干預統計資訊的更新。
  在SQL Server 2016中,即便是當前表中改變的資料行還沒有達到觸統計資訊更新閾值的條件(傳統上所謂的閾值,500+rowcount*20%),
  統計資訊依然會在查詢的驅動下更新,通過索引上的統計資訊可以看到,參考下圖,直方圖中生成了一個50的統計。

  

  下面就是所謂觸發統計資訊更新閾值的條件(嚴格說是該規則僅對SQL Server 2016之前的版本有效,不適應於SQL Server 2016)
    1,表格從沒有資料變成有大於等於1條資料。
    2,對於資料量小於500行的表格,當統計資訊的第一個欄位資料累計變化量大於500以後。
    3,對於資料量大於500行的表格,當統計資訊的第一個欄位資料累計變化量大於500 + (20%×表格資料總量)以後。
  這個說法,對於SQL Server 2016之前的版本是有效的,對於SQL Server 2016之後的版本是不成立的,我想這個還是值得注意的。

SQL Server 2016中統計資訊更新策略相當於之前版本中開啟了TraceFlag 2371,參考http://www.cnblogs.com/wy123/p/5748933.html
也即決定統計資訊的變化值為動態的,不再拘泥於“資料累計變化量大於500 + (20%×表格資料總量)”這一限制。
除此之外,應該還要其他機制,比如這裡的查詢所觸發的。

造成的問題

為什麼微軟會在SQL Server 2016中將統計資訊的更新策略做出如此的改變,以及為什麼筆者會來探究這個問題?
當然在實際業務中被這個問題坑的蛋疼。
問題很明顯,類似於測試的場景,在SQL Server 2012(包括之前的版本),這種預估策略存在的嚴重的缺陷。
比如示例中:
因為沒有當前過濾謂詞的統計資訊(或者說沒有收集到當前謂詞的統計資訊),實際為5000行的情況下,預估為1行。
這種預估策略非常離譜,某種情況下會造成嚴重的效能問題,估計也很容易猜到,只是遇到的比較少罷了.
下面就簡單具體說明,會造成什麼問題,以及原因。

  上述問題在什麼情況下會造成效能問題,以及影響又多嚴重,這裡僅簡單舉例說明。下面這個測試是在SQL Server 2012下進行的。
  為演示這個問題,先來做另外一張測試表B,並寫入測試資料。

create table B
(
    IdentifierId    int identity(1,1),
    Id2                int,
    OtherCol        char(500)
)
GO

begin tran
declare @i int = 1
while @i<=1000000
begin
    insert into B values (@i,NEWID())
    set @i = @i+1
    if (@i%100000)=0
    begin
        if @@TRANCOUNT>0
        begin
            commit
            begin tran
        end
    end
end
if @@TRANCOUNT>0
begin
    commit
end
GO

create index idx_2 on B(Id2)
GO

藉助第二張表做一個測試,從而把錯誤預估行數造成的缺陷給放大,
執行下面兩個SQL,分別查詢A.Id1 = 5和A.Id1 = 50的資訊,
由資料分佈可知,查詢總的結果總數會完全一樣(截圖受影響行數),
雖然A.Id1 = 5和A.Id1 = 50的資料量和分佈也完全一樣,但是後者的邏輯IO遠遠超出前者。
就是因為直方圖中沒有A.Id1 = 50的統計資訊,A.Id1 = 50被錯誤地預估為1行造成的。

    

  具體原因就很明瞭的,瞭解執行計劃的同學應該很清楚。
  因為錯誤地預估了當前謂詞過濾的行數,在A表上,採用索引查詢的方式來查詢資料,
  事實證明,當前情況下,這是比全表掃描更加低效的一種方式(看邏輯IO),這是其一。
  另外A表查詢之後驅動B表的過程中,因為預估為一行,採用了Nested Loop的方式來驅動B表做連線,
  事實上當前情況下Nested Loop並非最好的,可以說是很不好的。
  這裡也可以歸結為統計資訊的直方圖中沒有過濾謂詞上的統計資訊,在第一個階段的預估中錯誤地估算為1行造成的。

 

  這種問題更蛋疼的地方在於,檢查Session或者快取的執行計劃的時候,會發現,表面上看,執行計劃挺好的啊,都用到索引了。
  比如第二個SQL的執行計劃,看起來似乎沒問題,也容易直接忽略這個造成的問題,
  從而把重點轉向其他地方,使得問題變得更加難以甄別。其實問題正是出在錯誤地使用了索引,不該使用索引的地方使用了索引。
  這就是執行計劃第一步選擇錯誤,造成後面每一步都錯誤的情況(一步錯,步步錯),實際情況中,SQL更加複雜,資料量也更大,造成的影響也更大。
  如果上述示例中在再多幾張表join,會出現清一色的Nested Loop方式來驅動表連線,這樣的話,SQL執行時間和邏輯IO是非常高的。

  附上一個在SQL Server 2016下的測試截圖,可見在預設情況下,執行計劃做出了正確的選擇。

  

  

 最後: 

1,本文不是說索引的,關於索引的就不多說。
2,本文也的場景雖然不是太常見,稍顯特殊,但也是實際遇到的,另外可以看出,微軟也在從這個方面逐步改進SQL Server優化器更新統計資訊的策略。
3,關於此場景下的預估,在不同版本下,還有不少有意思問題沒有丟擲來,有機會再說。
4,類似問題只有在資料量相對較大的情況下才能發生,如果是十萬以下或者幾十萬的資料量,對資料庫來說算是微小型資料量,類似問題對效能的影響完全體現不出來。
5,如果有人根據本文的測試驗證的話,請注意一個細節:對於過濾謂詞的預估,分如下兩種情況,這兩種情況在2012和2014(2016)中預估的方式也是不同的
  1,表中確實沒有這個謂詞的資料,並且統計資訊沒有更新,比如Id1 = 50的資料為0行的情況下的預估
  2,表中有這個謂詞的資料,同樣是統計資訊沒有更新,比如Id1 = 50的資料為50000行的情況下的預估

總結:

SQL Server 的預估對執行計劃的生成有著至關重要的影響,而預估又依賴於統計資訊,因此統計資訊的更新以及準確性就顯得尤為重要。鑑於此,SQL Server在每個版本中,對於統計資訊的生成以及更新策略都有著比較大的變化,本文僅僅從一個較小的點出發,來驗證SQL Server各個版本中統計資訊預估以及更新的一些特點,從中發現類似問題可能產生的潛在的影響,以及SQL Server 2016中的一些改進。