1. 程式人生 > >SQL Server新基數估量器

SQL Server新基數估量器

本系列屬於 SQL Server效能優化案例分享 專題


    當你使用SQL Server 2014及以上版本並且資料庫的相容級別為120或以上時,可能會有一個比較奇怪的現象,原本在SQL 2008/2012上執行正常的資料庫,可能因為遷移到SQL Server 2014版本,在新環境突然變慢了。
    一般來說,遷移/升級例項版本時,我們必不可少的工作有:備份、重建全庫索引、全庫更新統計資訊。但是如果這些都做了還是沒有效果,那麼要考慮一下是否因為SQL Server 新基數估量器在搗鬼。這個新的估量器從2014引入,並且一直在後續版本中存在。

    另外還有一個現象,就是在Where條件有多個列需要篩選時,其統計資訊可能會出現非預期情況。基於這些情況,我覺得有必要介紹一下新的基數估量器(Cardinality Estimator)

        

介紹

    統計資訊是描述表/索引資料分佈情況的系統元資料,通過統計資訊,優化器得到了優化的大部分前提資料並進行執行計劃的生成。為了得到高效的執行計劃,優化器必須得到每一步操作符的預估資訊,主要來自於統計資訊。SQL Server從2005~2012版本一直使用1998年釋出的基數預估模型。隨著時代的發展,顯然已經不夠合理。所以從SQL 2014版本開始,微軟引入了新的基數預估模型,但是從概念上來說,新舊模型是一樣的。

    舊模型有四個主要的“假定”:

  1. 均勻的:Uniformity,模型假定在缺少統計資訊的時候,資料分佈是均勻的。
  2. 獨立的:Independence,模型假定實體的屬性是互相獨立的,比如一個查詢有多個來自於同一個表的不同列的篩選條件,那麼這些列沒有任何關係。這個很重要,本人最近優化的一個案例中就出現這類問題。
  3. 簡單包含:Simple Containment,模型嘉定使用者查詢的資料都儲存在表中。比如在缺少統計資訊的情況下,當你關聯兩個表時,模型假設一個表的不同值“都”存在於另外一個表中。
  4. 包含:Inclusion,模型假設在將屬性(列)與常量進行比較時, 始終存在匹配項。

    雖然在很多情況下,這些假設是可以得到可接受的結果。不過很顯然這些假設不可能總是正確的。同時由於模型的重構難度很大,所以從SQL 2014開始,對模型進行了重新設計,注意兩個模型是並存的,並非簡單替代。新模型對假定進行了一些新的改進:

  1. 相互關聯:Correlation,其假定語句的謂詞是相互關聯的,這個更加符合現實環境。
  2. 基本包含:Base Containment,假定使用者可能所查詢的資料並不存在於表中。除了聯接謂詞的選擇性外, 它還將基表的直方圖作為聯接操作的因素。

    在SQL 2014~最新版本中(2014和2016又有稍微的區別),可以通過修改資料庫的相容級別來單獨控制每個庫所使用的模型,也可以使用跟蹤標記來控制伺服器、會話甚至語句級別的基數預估模型。執行計劃具體使用的模型可以檢視執行計劃根操作符(圖形中最左邊的那個)的屬性的“CardinalityEstimationModelVersion”值,圖中120代表使用SQL 2014:


    新舊模型的關鍵區別在於如何處理多語句表值函式。舊模型總是預期函式返回一行資料,而新模型預期為100行。雖然兩個模型都不合理,但是在大部分情況下,多語句表值函式返回100行會更加合理。


新舊模型對比


統計資訊未過時

    首先建立測試環境,然後產生一下測試資料並建立聚集索引和非聚集索引:

create database test
GO
USE TEST  
GO
--建立表
create table dbo.Test 
( 
    ID int not null, 
    TestDate date not null, 
    TestName char(10) 
); 
--建立測試資料
;with N1(C) as (select 0 union all select 0) -- 2 行 
,N2(C) as (select 0 from N1 as T1 cross join N1 as T2) -- 4 行 
,N3(C) as (select 0 from N2 as T1 cross join N2 as T2) -- 16 行 
,N4(C) as (select 0 from N3 as T1 cross join N3 as T2) -- 256 行 
,N5(C) as (select 0 from N4 as T1 cross join N4 as T2) -- 65,536 行 
,IDs(ID) as (select row_number() over (order by (select null)) from N5) 
insert into dbo.Test(ID,TestDate) 
select ID,dateadd(day,abs(checksum(newid())) % 365,'2018-03-20') from IDs; 
--建立索引
create unique clustered index IDX_Test_ID on dbo.Test(ID); 
create nonclustered index IDX_Test_ADate on dbo.Test(TestDate); 

    然後使用DBCC SHOW_STATISTICS('dbo.Test',IDX_Test_ADate)來檢視索引的統計資訊,會看到大概如下內容,不過由於日期是隨機生成,所以具體值不一定會一樣:


    如圖所示,表有65536行,然後以相容級別分別為110(2012)、120(2014)和130(2016)來對比一下在查詢條件屬於直方圖的值時模型的情況:

alter database test set compatibility_level =110--分別替換為120/130
GO
select id,TestDate,TestName
from dbo.test with (index=IDX_Test_ADate)
where TestDate='2018-03-26';




    三個結果一樣,SQL Server使用直方圖第四個值的EQ_ROWS列來估算影響行數。然後來試一下使用不在直方圖裡面的一個值'2018-03-21'(介於直方圖第1、2行之間)來測試:

    三個結果也一樣,他的估計行數為186,這個值來自於直方圖第二行的AVG_RANGE_ROWS。


    最後試一下用引數化查詢,使用本地變數來作為謂詞,這個使用SQL Server會使用索引的平均選擇度乘以索引鍵的唯一值總數來作為估計數量,即第一個圖的前兩個紅框相乘:0.002739726×65536=179.550683136≈179.551

alter database test set compatibility_level =110
GO
declare @D date = '2018-06-07'; 
select id,TestDate,TestName
from dbo.test with (index=IDX_Test_ADate)
where [email protected];
GO
alter database test set compatibility_level =120
GO
declare @D date = '2018-06-07'; 
select id,TestDate,TestName
from dbo.test with (index=IDX_Test_ADate)
where [email protected];
GO
alter database test set compatibility_level =130
GO
declare @D date = '2018-06-07'; 
select id,TestDate,TestName
from dbo.test with (index=IDX_Test_ADate)
where [email protected];

小結

    在統計資訊未過時的情況下,三種模型的結果是一樣的。


統計資訊已過時

    在真實環境下,統計資訊不過時是不現實的,那麼下面來演示一下,在增加10%的資料量即6554行新資料的情況下統計資訊的行為,為了避免系統自動更新,這裡也要把自動更新選項關閉。因為在SQL 2016(相容級別130)中會存在動態更新的特性。在演示完畢後,請重新開啟自動更新!

alter database Test set auto_update_statistics off  --關閉自動更新
go 
;with N1(C) as (select 0 union all select 0) -- 2 行 
,N2(C) as (select 0 from N1 as T1 cross join N1 as T2) -- 4 行 
,N3(C) as (select 0 from N2 as T1 cross join N2 as T2) -- 16 行 
,N4(C) as (select 0 from N3 as T1 cross join N3 as T2) -- 256 行 
,N5(C) as (select 0 from N4 as T1 cross join N4 as T2) -- 65,536 行 
,IDs(ID) as (select row_number() over (order by (select null)) from N5) 
insert into dbo.Test(ID,TestDate) 
select ID + 65536,dateadd(day,abs(checksum(newid())) % 365,'2018-06-01') 
from IDs 
where ID <= 6554; 
接下來我們重複上面的三個語句測試,可以看到所有模型的估計行數為194.701行,比之前的值多10%。


    再次執行DBCC SHOW_STATISTICS('dbo.Test',IDX_Test_ADate)可以看到SQL Server調整第四行的EQ_ROWS作為實際行數:


    接下來在執行第二個語句,即不在直方圖的日期,2018-03-21,可以發現相容級別是110的,其估計數量跟120及以上的不一樣:



    因為新模型(相容級別120+)會把10%的差異計算進去,而相容級別為110的依舊使用AVG_RANGE_ROWS。同樣的情況也會發生在引數化語句中:



小結

    這種差異有好有不好,新模型對於新資料均勻分佈在索引的情況中能夠得到更好的結果,在本例中是因為日期隨機生成,所以從統計學上來說是均勻分佈。

    但是,在舊資料分佈不變但是新資料不均勻分佈情況下,舊模型會更加好,其中一個典型的例子是索引鍵為自增。

    因此,目前的正式版本中依舊保留了舊模型可用,只是預設使用新模型,除非你的相容模式為120以下。記得恢復自動更新統計資訊!


自增索引鍵

    接下來演示一下,當值不在直方圖範圍中的情況,這種情況通常發生在索引鍵為自增的情況下(比如用identity或者sequence列作為索引鍵)。這次使用表中另外一個索引IDX_Test_ID(聚集索引),目前為止,索引統計資訊並沒有更新:


    同樣按照三種相容級別先查詢一下“不在”直方圖中的值,比如66000到67000:

alter database test set compatibility_level =110
GO
select top 10 ID, TestDate 
from dbo.Test 
where ID between 66000 and 67000 
order by TestName;
alter database test set compatibility_level =120
GO
select top 10 ID, TestDate 
from dbo.Test 
where ID between 66000 and 67000 
order by TestName;
alter database test set compatibility_level =130
GO
select top 10 ID, TestDate 
from dbo.Test 
where ID between 66000 and 67000 
order by TestName;


    可以看出,舊模型的估計行數只有1行,而新模型則基於索引的平均資料分佈(66000~67000範圍值為1001,注意between and是≥和≤)來預估。在這種索引自增情況下,新模型更加合適,至少避免了手動更新統計資訊。

表關聯

    接下來演示日常場景的另外一種使用場景,表關聯情況,先建立另外一個關聯表Test1,只有一個列ID,並與Test表有外來鍵關聯,然後把Test表的資料插入到這個Test1表(現在已經有72090行),並建立聚集索引在表上:

use test
GO
create table dbo.Test1
( 
    ID int not null 
     constraint FK_Test1_Test foreign key references dbo.Test(ID) 
); 
insert into dbo.Test1(ID) -- 72,090 行 
select ID from dbo.Test; 

create unique clustered index IDX_Test1_ID on dbo.Test1(ID); 

    第一步先關聯並查詢僅在Test1表中的資料,由於外來鍵約束能確保Test1的每行資料都能與Test表的資料關聯,所以SQL Server在這種情況下可以直接忽略關聯,注意也要加上三種相容級別:

alter database test set compatibility_level =110
GO
select d.ID 
from dbo.Test1 d join dbo.Test m on d.ID = m.ID 
GO
alter database test set compatibility_level =120
GO
select d.ID 
from dbo.Test1 d join dbo.Test m on d.ID = m.ID 
GO
alter database test set compatibility_level =130
GO
select d.ID 
from dbo.Test1 d join dbo.Test m on d.ID = m.ID 
GO
    讀者可以自行檢查,行數都是正確的。


多篩選條件

    前面提到,舊模式有一個主要假設就是獨立性,而新模型移除了這種假設,它認為實體的屬性之間存在某種層面上的關聯。所以在WHERE條件出現多個列篩選時,表現的行為跟舊模型有所區別。下面對Test表進行演示三種模型下的情況:

alter database test set compatibility_level =110
GO  
select ID, TestDate 
from dbo.Test 
where  ID between 20000 and 30000 and  TestDate between '2018-03-01' and '2018-04-01'
GO
alter database test set compatibility_level =120
GO
select ID, TestDate 
from dbo.Test 
where  ID between 20000 and 30000 and  TestDate between '2018-03-01' and '2018-04-01'
GO
alter database test set compatibility_level =130
GO
select ID, TestDate 
from dbo.Test 
where  ID between 20000 and 30000 and  TestDate between '2018-03-01' and '2018-04-01'
GO



    這一次可以看到,估計行數差異就比較大了。新舊模型由於在是否獨立的點上有出入,導致其計算公式也有不同:

  • 舊模型:(第一個條件的選擇度×第二個條件的選擇度)×(表總行數)=(第一個條件的預估行數×第二個條件的預估行數)/(表總行數),多個條件以此類推。
  • 新模型:(最佳選擇度的條件的選擇度)×平方根(次佳選擇度條件的選擇度)×(表總行數)

    兩者都是“合理”的,對於多條件之間確實沒有關聯時,舊模型會更好,反之,新模型更佳。

模型選擇

    前面演示了很多例子,從中可以發現新舊模型都有適用長期,那麼如何選擇模型來達到最佳的基數預估呢?通常情況下,新技術的出現是為了彌補甚至重構舊技術在某些方面的缺失,所以一般我們都會優先選擇新技術。但是如果環境是從相容級別較低(120以下)升級到120以上的情況,那麼就要小心處理,因為很有可能會出現統計資訊的差異導致效能突降。


總結

    本文演示了很多不同相容級別(相容級別導致了基數預估模型的不同)下新舊模型的差異。舊模型(相容級別120以下)和新模型(120以上)在基數預估上具有很大差異。而SQL 2014(120)和SQL 2016(130)則差異不明顯,130主要以增強為主,更好地應對自增索引和多列統計資訊的環境。

    如果讀者所使用的SQL Server是新專案,優先選擇相容級別120甚至130以上的版本,如果是120以下的環境,需要升級到以上相容級別,那建議對核心功能進行測試。

    另外可以參考一下聯機叢書的內容:高效使用統計資訊的查詢