1. 程式人生 > >全廢話SQL Server統計信息(2)——統計信息基礎

全廢話SQL Server統計信息(2)——統計信息基礎

position amp 要去 fault href 過程 字符串 最大 實用

接上文:http://blog.csdn.net/dba_huangzj/article/details/52835958

我想在大地上畫滿窗子,讓全部習慣黑暗的眼睛都習慣光明——顧城《我是一個任性的孩子》



這一節主要介紹一些理論層面的東西,主要針對SQL Server,為後面的做鋪墊。假設從實操層面考慮能夠跳過,可是我強烈建議還是要找時間看一下這節。本節的內容例如以下:

  1. SQL Server統計信息
  2. 列級統計信息
  3. 統計信息與運行計劃
  4. 統計信息與內存分配
  5. 開銷預估模型


SQL Server統計信息


說到統計信息,就一定要提到查詢優化器,主流關系型數據庫管理系統的查詢優化器都是基於開銷的優化(cost-based optimizer, CBO),而優化器是生成運行計劃的組件。所以運行計劃的質量直接依賴於開銷預估的準確性,相同。運行計劃的預估開銷又基於算法/操作符的使用和基數預估。所以,為了讓優化器得到準確的預估開銷,優化器須要盡可能準確地預估制定查詢要返回的記錄數。

在查詢被優化的過程中。SQLServer會分析非常多候選運行計劃。並預估它們的相對開銷,然後選擇最高效的那個運行計劃。因此,不準確的基數和開銷預估會引起優化器選擇不高效的運行計劃從而影響數據庫性能。

這裏提到的開銷、基數等概念,在用戶層面看來,就是統計信息。或者說。對於用戶來說。這些信息中可控部分主要是統計信息。

SQL Server的統計信息包括三個主要部分:直方圖(histogram)、密度信息(density information)和字符串統計信息(string statistics)。這三個部分在基數預估過程中分別協助不同的部分。

提醒:SQL Server在統計信息中存儲了一個額外的針對字符串值的信息,稱為Trie Trees(直譯叫字典樹或前綴樹),這個信息能夠針對字符串鍵值提供更好基數預估。可是這部分屬於“未公開功能”,所以不在這裏介紹。

SQL Server 創建和維護統計信息,通過提供基數預估幫助優化器分析。而基數預估是對一個查詢,“假設”使用了某些篩選條件、JOIN聯接或GROUP BY 操作之後。會返回的記錄數。而還有一個常見術語選擇度(Selectivity)的概念和基數預估非常類似,它計算滿足謂詞的行在表中的百分比。選擇度越高,返回的結果越小。提醒一下。選擇度是索引鍵值選擇的重要指標之中的一個。

最後。我們來回答一下一個一直沒有正式回答的問題:為什麽我們須要統計信息?答案事實上非常easy,可是可能須要有過一定的經歷,才會深有體會,這個答案就是統計信息降低了在優化過程中必須分析的數據量。假設優化器每次優化都要訪問實體表/索引的話。分析過程會變得非常低效。

所以優化器會使用實際數據的樣本(也就是統計信息)來做分析,統計信息的量通常來說會遠低於原數據。所以分析和生成運行計劃的速度會快得多。可是正如我一直在非常多文章中說到的一樣。沒有什麽功能是絕對的好或者絕對的壞,統計信息也有缺點。這個缺點就是維護成本,對於大型數據庫的統計信息創建和維護(實時更新)會消耗非常多資源和時間。另外由於統計信息是數據表/索引的取樣結果,所以對於超大型的表來說,準確程度不可能太高。


統計信息的樣子:


以下我們來看看上一節創建的演示庫中統計信息的樣子,先用以下腳本創建數據庫環境:


use StatisticsTest;
go
-- Create a test table 
if (object_id(‘T0‘, ‘U‘) is not null) 
  drop table T0;
go 
create table T0(c1 int not null, c2 nchar(200) not null default ‘#‘)
go 
-- Insert 100000 rows. All rows contain the value 1000 for column c1 
insert T0(c1)
select 1000 from Numbers 
where n <= 100000 
go 
-- Now insert only one row with value 2000 
insert T0(c1) values(2000)
go 
--create a nonclustered index on column c1 
create nonclustered index ix_T0_1 on T0(c1) 


首先看看圖形化的統計信息,我們能夠在SSMS的這個地方找到統計信息:


技術分享圖片


在環境創建完之後,能夠發現統計信息這個文件夾以下是沒有東西的,由於表沒有“被使用”,所以優化器不會對這個表創建不論什麽統計信息。可是當第一次使用或者創建索引(實際上也是對數據進行使用)時,就會創建統計信息。我們能夠嘗試兩個操作。第一個是運行一個簡單的SELECT語句,優化器會對上面用到的列創建統計信息:

技術分享圖片

須要註意要帶上WHERE條件,當中豎框部分的1代表表創建時的第一列也就是x,而_WA_Sys代表由SQL Server自己主動創建的統計信息。WA傳說是SQL Server開發組所在地華盛頓(Washington)的縮寫。

以下再來創建一個索引。即前面腳本中凝視掉的那段:

技術分享圖片

能夠看到又多了一個統計信息。而且這個統計信息是和索引名一樣。這個能夠說是SQL Server自己創建的(由於你沒有顯式編寫命令單獨創建統計信息)。也能夠說是用戶操作導致的。為了和前面_WA這個做差別。我們通常把它定義為非SQL Server自己主動創建的統計信息。

SQL Server統計信息元數據


以下我們來看看怎樣查詢統計信息。統計信息是獨立於實體表/索引的實際存儲的信息,我們能夠從一些元數據中獲取它們。SQL Server 2005開始引入了文件夾視圖、動態管理對象(DMO)等替代2000時代的系統表,降低對系統表的誤操作所帶來的系統故障,同一時候這些視圖也加入了非常多具體信息供興許使用。關於統計信息,我們首先用到的文件夾視圖是:sys.stats,註意這部分的元數據存儲在相應的數據庫中。所以也須要切換到相應的數據庫下運行:

use StatisticsTest
GO
SELECT *
FROM sys.stats
WHERE object_id = object_id(‘dbo.T1‘)

技術分享圖片

能夠看到每一個統計信息都單獨存在一行中。能夠使用DBCC SHOW_STATISTICS命令來對某個統計信息進行具體展示:

技術分享圖片



假設要查詢的統計信息不存在(或者拼錯),會得到以下錯誤:

消息 2767,級別16,狀態 1,第 8 行

無法在系統文件夾中找到統計信息 ‘a‘。

DBCC 運行完成。

假設 DBCC 輸出了錯誤信息,請與系統管理員聯系。


技術分享圖片

在這裏是由於我們從創建開始就沒有使用過a這個列,所以僅僅要我們運行一個使用到它的語句。然後就能夠查詢:

技術分享圖片


回到DBCC SHOW_STATISTICS命令得到的結果,前面提到了統計信息主要有三個部分,從上圖看到也確實有三部分,這三部分分別叫做頭信息、密度信息和直方圖。


技術分享圖片


頭信息:

技術分享圖片
以下來看看每一個列的簡要說明:

  • Name:_WA_Sys_00000002_108B795B。這是統計信息的名字。所以自己主動創建的統計信息都以_WA_Sys開頭,跟著是一個值,標識為統計信息是基於哪一列創建的,註意自己主動創建的統計信息僅僅會在單列上,對於多列組合的統計信息必須手動創建,同一時候這個列順序是表創建時候的順序,能夠通過sys.columns文件夾視圖查看。接下來是一個十六進制的值。代表表的object_id,能夠用Windows自帶的計算器反計算。

    然後使用Object_Name()函數得出表名:

技術分享圖片 技術分享圖片

  • Updated:09 17 2016 5:13PM 這個值是統計信息創建或最後一次更新的時間。
  • Rows:100000。表示統計信息創建或最後一次更新的時間。
  • Rows Sampled:100000,表示統計信息創建或近期一次更新時的取樣行數。
  • Steps:109,直方圖的步數。接下來會介紹。
  • Density:0.03002139,密度值,這個值在新版(最晚在SQL 2008開始)SQL Server中僅用於向後兼容。對優化器沒實用處。
  • Average key length:3.62263,統計信息所針對的列的平均字節數。這個值能夠通過這樣計算出來。盡管沒有多大研究價值:

技術分享圖片

即把列中每行的字節數(註意datalength函數返回字節數,len()函數返回字符數)加起來再除以總行數就可以。

  • String Index:YES。這個值表示統計信息是否包括字符串信息,僅僅有YES或者NO可選。這個值能夠對LIKE條件提供預估支持,而且字符串統計信息僅僅對第一列創建且必須為字符串類型(單列、多列統計信息都一樣),由於這裏的統計信息是建在A列,而這列是字符串類型,所以這裏的值為YES。

  • FilterExpression和UnfilteredRows:這兩個值僅僅在統計信息創建在過濾索引(filter index)上才出現非null的值,後面會介紹。

密度信息:

技術分享圖片

本例中比較簡單,是單列統計信息,所以密度信息這部分比較少。後面會演示多列統計信息。從名字來看,就三列:

  • All density:0.0003333333,它是對於這列(多列的先無論)。1/唯一值的個數。

技術分享圖片

  • Average Length:3.62263,表示唯一值的平均長度。

  • Columns:a ,明顯代表這個密度是包括哪些列。不羅嗦。

那麽這部分內容有啥用呢?大部分情況下,這部分的信息能夠對語句中的GROUP BY 和ON條件中的未知值(比方本地變量)提供信息給優化器。

再次看看這個表的統計信息情況:

DBCC SHOW_STATISTICS(‘Sales.SalesOrderDetail‘, IX_SalesOrderDetail_ProductID)
技術分享圖片

這裏表示了這個索引包括了3列,每種組合情況下的密度及平均長度信息。以下來看看這個語句:

USE AdventureWorks2014
GO
 
SELECT ProductID
FROM Sales.SalesOrderDetail
GROUP BY ProductID

優化器在編譯這個語句時,由於ProductID列上有統計信息,不須要遍歷整表,直接從密度信息中就能夠獲取唯一值(Group By本質就是去重)的預估行數。

技術分享圖片

這個值。依據定義,是1/相應的密度。由於僅僅計算ProductID,所以密度就是

技術分享圖片

結果大家能夠自行算一下。是265.99996。

大家也能夠自己算一下GROUP BY ProductID,SalesOrderID及SalesOrderDetailID,也就是上面的另外兩行。

那麽在使用本地變量的情況下會怎樣 ?

USE AdventureWorks2014
GO
 
DECLARE @ProductID INT
 
SET @ProductID = 921
 
SELECT ProductID
FROM Sales.SalesOrderDetail
WHERE ProductID = @ProductID

技術分享圖片

由於本地變量在統計信息的行為上有點特殊。所以這裏要特意拿來說一下,優化器在終於實際運行之前沒辦法知道提交的SQL語句中參數會是什麽值,可是它又必須產生一個預估運行計劃,同一時候接下來會解釋。此時也沒辦法使用直方圖。所以優化器僅僅能借助密度信息來獲取預估行數。此時的預估行數由表總數乘以密度信息得出。也就是:0.003759399 * 121317≈456.079。

另外一個情況下。WHERE條件不是使用“等於”符號,而是“不等於”,

技術分享圖片

這個時候,參數的具體值已經沒所謂了。大家能夠試一下隨便填一個值。此時優化器連密度信息都不能用了,可是又必須給點東西,怎麽辦?使用一個標準假設(表總數的30%作為選擇度),也就是說在這樣的不等操作符中。預估行數總是表總數的30%。即121317*30%≈36395.1。

這個30%的值有時候卻會帶來非常大的性能影響,特別是對超大表,30%的量還是非常大的。可是可能實際上語句僅僅須要極其少量的數據。基於這個原因,在查詢中。盡可能避免本地變量,而使用參數化或者字符串形式。由於此時優化器能夠使用直方圖來協助預估。從而提供更準確的預估行數。

直方圖:

在SQL Server中,僅僅會對統計對象的第一列創建直方圖。而且壓縮在這些列中的分布值的信息到一系列子集中。這些子集稱為桶(buckets)或者步(steps)。為了創建直方圖,SQL Server須要先在首列中查找唯一值,而且使用最大偏差算法嘗試獲取那些最經常使用的變化值。以便把最顯著的統計信息保存下來。

最大偏差是當中一個用於精確地表達關系數據庫中數據分布情況的算法。

在SQL Server中,我們能夠使用DBCC SHOW_STATISTICS命令查看直方圖,比方在微軟演示樣例數據庫AdventureWorks2014中顯示Sales.SalesOrderDetail表的IX_SalesOrderDetail_ProductID索引的當前統計信息:

USE AdventureWorks2014
GO
DBCC SHOW_STATISTICS (‘Sales.SalesOrderDetail‘,IX_SalesOrderDetail_ProductID)

前面提到,直方圖僅僅對統計對象的首列進行創建。所以在這個統計信息中,僅僅對ProductID列創建統計信息。以下來看看這個語句產生的關於直方圖方面的結果:

技術分享圖片

  • RANGE_HI_KEY:直方圖中每一步的最高邊界(上限值),比方上圖第21行相應的738。和第22行相應的741。這兩行意味著第22行中的統計信息邊界從739~741。
  • RANGE_ROWS:是一個預估行數。表示不包括上限值在內的該步長中的行數。

    在截圖中,也就是意味著第22行的值應該是關於739~740的行數。

  • EQ_ROWS:相同是一個預估值,預估在這列的相應步長中,跟RANGE_HI_KEY相同的值有多少,拿22行做樣例:最高值是741。然後直方圖預估等於741這個值的數有97個。
  • DISTINCT_RANGE_ROWS:在該步長範圍內,除去上限值的其它值中。唯一值的數量。
  • AVG_RANGE_ROWS:該步長中,每一個非上限值的唯一值的平均行數,計算公式為:RANGE_ROWS/DISTINCT_RANGE_ROWS。

以下來大概計算一下截圖中的值怎麽來的:

我們針對圖中第22行。使用以下語句能夠得出:

技術分享圖片

  • RANGE_HI_KEY:由SQL Server自己主動標註,我們幹預不了。
  • RANGE_ROWS:表示了不包括上限值的行數,即ProductID=739的行數。這裏算出是167,跟直方圖的RANGE_ROWS相同。

  • EQ_ROWS:上限值,即741的行數,這裏非常顯然是94行。
  • DISTINCT_RANGE_ROWS:這個步長有兩個唯一值。741和739,出去上限741之外。僅僅有739這個唯一值,所以相應的值為1。

  • AVG_RANGE_ROWS:RANGE_ROWS/DISTINCT_RANGE_ROWS=167/1=167。

通過定義,我們把直方圖中的值都計算出來了,大家能夠用這樣的方法去驗證其它值。

所謂知其然知其所以然。除了知道解釋之外,更重要的是知道為什麽要計算這些值。存在必定有意義,我們大部分情況下都不須要去研究這些意義。可是一旦出現一些深入問題或者你想做深入研究的時候,那就成了必要了。以下我們來看個非常普通的語句:

USE AdventureWorks2014
GO
 
SELECT *
FROM sales.SalesOrderDetail
WHERE ProductID = 741

然後看看實際運行計劃:

技術分享圖片

這裏的預估行數為94,由於這個樣例是ProductID=741,所以優化器直接使用直方圖中RANGE_HI_KEY的值來替代,剛好是94。然後把741換成該步長中的另外一個值739,看看相應的部分:

技術分享圖片

由於739不是這個步長的上限值。所以不能使用RANGE_HI_KEY來直接獲取。所以會使用AVG_RANGE_ROWS。

以下來一個不等於的操作:

技術分享圖片

這個13223怎麽來的?累加!,把直方圖中,RANGE_HI_KEY小於714(註意不是等於)的EQ_ROWS加起來。大家能夠自己算一下。

以下把樣例擴展一下,把WHERE條件加一個AND謂詞:

技術分享圖片

技術分享圖片

這個語句實際上使用了兩個統計信息。前面提到過00000004代表表定義的第四列。SQL Server使用相應的統計信息對象來分別評估兩個條件。得出相應的預估值之後。取兩者交集作為終於的預估行數。所以這個過程須要乘以兩個的選擇度。

大家能夠自己嘗試。ProductID=870的值有4688行,對於這個有121317行的表。相應的選擇度為4688/121317。也就是大概0.0386425645。

同理,OrderQty=1的有74954行(不同版本號的AdventureWorks庫行數可能不一樣,所以最好自己算一次),相應的選擇度為0.6178359175。

為了獲取交集,須要把這兩個選擇度相乘:0.0386425645*0.6178359175≈0.02387476429241042875。最後,用這個值乘以表的總數,得出終於的預估影響行數:0.02387476429241042875*121317≈2896.41。可是圖中的數據是2656.97。由於默認情況下數據庫是啟用自己主動創建統計信息功能的。在運行語句的時候。優化器會自己創建統計信息(在OrderQty列上)。而由於這個列的統計信息取樣不準確(沒索引),所以會出現跟計算不一樣的情況。

假設你用一個原始庫還原。然後先關閉自己主動創建統計信息功能再運行。可能會出現:

技術分享圖片

當你啟用再運行的時候,數值可能變成了:

技術分享圖片

可是從技術上來說,預估的計算公式就是上面提到的。以下來更加復雜化一點:OR

技術分享圖片

上面的語句就是取兩個條件的預估行數,而且除去反復值的並集。

在我這個庫中,ProductID=870的有4688行,OrderQty=1 的有行。

前一個樣例我們已經算出AND的結果(也就是反復值)為2896.41 。那麽OR的結果就是4688+74954-2896.41=76745.59,跟圖上的差異比較大,可是還是前面的原因,自己主動創建的統計信息不準確。事實上僅僅要我們建一個索引再檢查。能夠發現就是我們算出來的數值:

use AdventureWorks2014
go
create index IX_SalesOrderDetail_OrderQty on
Sales.SalesOrderDetail(OrderQty)
技術分享圖片

為避免興許影響。盡可能保持原有數據庫狀態。把索引刪掉:

use AdventureWorks2014
go
drop index IX_SalesOrderDetail_OrderQty on
Sales.SalesOrderDetail
 


列級統計信息

我們一般討論的統計信息,有兩種:1.索引上的統計信息。2. SQL Server自己主動創建或人工創建的統計信息。

這裏討論的列級統計信息算是另外一種。而且多為SQL Server自己主動創建。

以下在AdventureWorks2014庫中演示一下,先創建一個Customers表:

USE AdventureWorks2014
GO
--建表
CREATE TABLE dbo.Customers(
    CustomerId INT NOT NULL identity(1, 1),
    FirstName NVARCHAR(64) NOT NULL,
    LastName NVARCHAR(128) NOT NULL,
    Phone VARCHAR(32) NULL,
    Placeholder CHAR(200) NULL
    );
 
CREATE UNIQUE CLUSTERED INDEX IDX_Customers_CustomerId ON dbo.Customers(CustomerId)
GO
 
-- 通過Crossjoin產生測試數據
-- 使用GO50循環50次
    ;
 
WITH FirstNames(FirstName)
AS (
    SELECT Names.NAME
    FROM (
        VALUES (‘Andrew‘),(‘Andy‘),(‘Anton‘),(‘Ashley‘),
            (‘Boris‘),(‘Brian‘),
            (‘Cristopher‘),(‘Cathy‘),
            (‘Daniel‘),(‘Donny‘),
            (‘Edward‘),(‘Eddy‘),(‘Emy‘),
            (‘Frank‘),(‘George‘),
            (‘Harry‘),(‘Henry‘),
            (‘Ida‘),(‘John‘),
            (‘Jimmy‘),(‘Jenny‘),
            (‘Jack‘),(‘Kathy‘),
            (‘Kim‘),(‘Larry‘),
            (‘Mary‘),(‘Max‘),
            (‘Nancy‘),(‘Olivia‘),
            (‘Olga‘),(‘Peter‘),
            (‘Patrick‘),(‘Robert‘),
            (‘Ron‘),(‘Steve‘),
            (‘Shawn‘),(‘Tom‘),
            (‘Timothy‘),(‘Uri‘),(‘Vincent‘)
        ) Names(NAME)
    ),
LastNames(LastName)
AS (
    SELECT Names.NAME
    FROM (
        VALUES (‘Smith‘),(‘Johnson‘),(‘Williams‘),(‘Jones‘),
            (‘Brown‘),(‘Davis‘),(‘Miller‘),(‘Wilson‘),
            (‘Moore‘),(‘Taylor‘),(‘Anderson‘),
            (‘Jackson‘),(‘White‘),(‘Harris‘)
        ) Names(NAME)
    )
--插入數據
INSERT INTO dbo.Customers(
    LastName,
    FirstName
    )
SELECT LastName,
    FirstName
FROM FirstNames
CROSS JOIN LastNames
GO 50 --循環50次
 
--額外插入一行數據
INSERT INTO dbo.Customers(
    LastName,
    FirstName
    )
VALUES (‘Isakov‘,‘Victor‘ )
GO
--創建非聚集索引
CREATE NONCLUSTERED INDEXIDX_Customers_LastName_FirstName ON dbo.Customers(
    LastName,
    FirstName
    );


註意索引的定義順序,LastName是首列。以下使用一個非SARG寫法:


技術分享圖片

對於第一個語句。SQLServer使用了聚集索引掃描。返回700行數據。第二個語句使用了非聚集索引掃描,返回1行數據。

接下來檢查一下統計信息:

select  stats_id, name, auto_created
from sys.stats
where object_id= object_id(N‘dbo.Customers‘)

技術分享圖片

前兩行是聚集索引和非聚集索引創建時SQL Server自己主動創建的統計信息。而第三個以“_WA”為前綴的,就是列級統計信息,由SQL Server在語句優化過程中自己主動創建的。然後再細化檢查一下這個統計信息的情況:

技術分享圖片

從數據能夠看出,它是基於FirstName創建的統計信息,由於在語句優化過程中發現FirstName沒有統計信息支持,所以默認情況下由SQL Server自己主動創建。有了這個統計信息。優化器就能夠對第二個語句進行優化,從而得出了不同的運行計劃。

除了由SQL Server自己創建(自己主動創建僅僅基於單列),還能夠使用CREATE STATISTICS命令手動創建基於單列或多列(多列必須手動創建)的統計信息。

可是統計信息天生帶有維護開銷。盡管相對於索引帶來的統計信息而言。通常列級統計信息會小非常多。可是在頻繁更新的情況下,還是會帶來客觀的開銷。所以對於非常少運行的語句。這樣的統計信息的創建會比額外創建索引更加好,畢竟索引在主鍵更新時也會被迫更新。相反。對於使用極其頻繁的語句,創建索引會比創建列級統計信息對優化器的支持更好。由於索引不僅有預估信息,還能對數據進行組織和高速定位。

統計信息與運行計劃

前面大量的樣例說明,統計信息影響了優化器的語句優化,而優化器語句優化的產物就是預估運行計劃。預估運行計劃又直接影響了語句的運行性能,所以體現出統計信息影響了預估運行計劃從而影響語句性能。

默認情況下,SQL Server自己主動創建和更新統計信息。在數據庫級別有兩個選項能夠控制這個行為:

  1. 自己主動創建統計信息:控制SQL Server是否自己主動創建列級統計信息。這個選項不影響索引自帶的統計信息。由於索引總是帶有統計信息(能夠使用STATISTICS_NORECOMPUTE選項來關閉索引上統計信息是否自己主動創建)。默認情況下這個選項是開啟的。
  2. 自己主動更新統計信息:當SQL Server在編譯或運行查詢時發現統計信息過時時。這個選項用於控制是否隨之更新統計信息,默認也是開啟的。

是否創建統計信息非常好理解,那麽是否更新怎樣推斷呢?SQL Server會基於統計信息列上的INSERT/UPDATE/DELETE/MERGE語句的影響行數來推斷,技術上成為統計信息更新閾值(statistics update thresholds),有時也叫做統計信息重編譯閾值(statistics recompilation thresholds)。推斷條件例如以下:

  1. 當表為空時,一旦插入數據即覺得統計信息過時。
  2. 當表小於500行時,在統計信息列的每500次變更時覺得過時。

    註意是500次,不是500行,比方對同一行數據更新100次。會覺得是100次變更而不是1次。

  3. 當表多余500行數據時。統計信息列興許每500次更新而且影響表上總行數的20%時,被覺得過時。

這三個條件得出一個結論:對於越大的表而言,統計信息的自己主動更新頻率越低。

比方對於10億行數據。要到2億次更新後才覺得統計信息過時從而觸發統計信息更新。

這部分在興許演示。

統計信息與內存分配

關系數據庫在運行時候都嚴重依賴於內存。不同的操作符須要不同的內存資源。

比方。索引掃描操作符須要一行接一行地提取數據,並不須要在內存中存儲多行數據。而其它某些操作如排序,須要在進行排序前訪問整個結果集,所以須要在內存中盡可能保留整個數據集。

可是SQL Server並不會任意分配內存,它會嘗試評估某個查詢及當中基於預估行數的操作符所需內存(memory grant)。

簡單來說就是運行某個查詢。SQL Server會預估每一個部分所需的內存。

這一步非常重要,過高或過低的評估都會對總體性能帶來嚴重影響。多高會浪費SQL Server的內存。而且在負荷非常大的server中,分配大量內存也會相對久非常多。

評估多低。會導致實際運行過程中,某些操作符由於內存不足而失敗或等待。

比方排序操作。當沒有得到足夠的內存時,SQL Server會把溢出的結果集放到TempDB中進行排序。盡管TempDB也是在沒辦法的情況下才使用,可是相對於在內存中的性能,TempDB一般會明顯低效非常多。以下來演示一下:

USE AdventureWorks2014
GO
CREATE TABLE dbo.MemoryGrantDemo(
    ID INT NOT NULL,
    Col INT NOT NULL,
    Placeholder CHAR(8000)
    );
 
CREATE UNIQUE CLUSTERED INDEX IDX_MemoryGrantDemo_ID ONdbo.MemoryGrantDemo(ID);
--創建一個MemoryGrantDemo表,然後產生65536行數據,當中Col列存儲0~99的值,大概每655或656行一個值。

;WITH N1(C) AS ( SELECT 0 UNION ALL SELECT 0 ) -- 2 rows , N2(C) AS ( SELECT 0 FROM N1 AS T1 CROSS JOIN N1 AS T2 ) -- 4 rows , N3(C) AS ( SELECT 0 FROM N2 AS T1 CROSS JOIN N2 AS T2 ) -- 16 rows , N4(C) AS ( SELECT 0 FROM N3 AS T1 CROSS JOIN N3 AS T2 ) -- 256 rows , N5(C) AS ( SELECT 0 FROM N4 AS T1 CROSS JOIN N4 AS T2 ) -- 65,536 rows , IDs(ID) AS ( SELECT row_number() OVER ( ORDER BY ( SELECT NULL ) ) FROM N5 ) INSERT INTO dbo.MemoryGrantDemo( ID, Col, Placeholder ) SELECT ID, ID % 100, convert(CHAR(100), ID) FROM IDs; --創建一個在Col列上的非聚集索引 CREATE NONCLUSTERED INDEXIDX_MemoryGrantDemo_Col ON dbo.MemoryGrantDemo(Col);



接下來加入656行新數據,而且指定Col為1000。這個數量是表總數的1%,依據前面說的統計信息更新閾值來說,是不被覺得過時的。因此不更新統計信息,從而直方圖也不會有Col=1000的這個值的數據分布情況:

USE AdventureWorks2014
GO
;with N1(C) as (select 0 union all select 0) -- 2 rows
,N2(C) as (select 0 from N1 as T1 CROSS JOIN N1 as T2) -- 4 rows
,N3(C) as (select 0 from N2 as T1 CROSS JOIN N2 as T2) -- 16 rows
,N4(C) as (select 0 from N3 as T1 CROSS JOIN N3 as T2) -- 256 rows
,N5(C) as (select 0 from N4 as T1 CROSS JOIN N2 as T2) -- 1,024 rows
,IDs(ID) as (select row_number() over (order by (select NULL)) from N5)
insert into dbo.MemoryGrantDemo(ID,Col,Placeholder)
        select 100000 + ID, 1000, convert(char(100),ID)
        from IDs
        where ID <= 656;

產生了數據之後,以下來測試一下運行情況,分別對Col列為1和1000的查詢並排序:

USE AdventureWorks2014
GO
 
DECLARE @Dummy INT
 
SET STATISTICS TIME ON
 
SELECT @Dummy = ID
FROM dbo.MemoryGrantDemo
WHERE Col = 1
ORDER BY Placeholder
 
SELECT @Dummy = ID
FROM dbo.MemoryGrantDemo
WHERE Col = 1000
ORDER BY Placeholder
 
SET STATISTICS TIME OFF


運行計劃概覽:

技術分享圖片


技術分享圖片

Col=1000的實際及預計行數、內存分配情況:

技術分享圖片

盡管從運行計劃的形狀看上去非常類似,可是對照基數預估和內存分配就能夠知道差異非常明顯,另外一個就是對於第二個語句,在排序操作符(Sort)右下角有個黃色感嘆號。鼠標移到上面就能夠看到:

技術分享圖片

最後。對照一下運行時間,能夠看到Col=1000的比Col=1的大非常多,一般來說表示性能較差:

技術分享圖片

假設你使用Profiler等工具跟蹤的話,能夠使用Sort Warning和Hash Warning事件。

開銷預估模型

前面一直提到,關系數據庫都是基於開銷的優化。那開銷是怎樣來的?以下簡要介紹一下,可是畢竟SQL Server不是開源的,非常多信息屬於機密,不公開,所以僅僅能從有限的資料上找。

優化器產生運行計劃的運行域開銷預估的準確程度有“直接的關系”。優化器在優化語句的過程中,優化器會分析非常多候選運行計劃,預估它們各自開銷。然後選擇最佳的那個作為預估運行計劃。

所以,不僅基數預估要準確,開銷預估也要高效。

開銷是對部分或完整的運行計劃的預估,計算每一個操作符的開銷值。然後把運行計劃中的全部操作符的開銷加起來作為總體開銷。

每一個操作符的開銷依賴於自身算法復雜度和影響行數。有些比方排序操作符。還與server的內存情況有關。

從能獲得的信息來看,每一個操作符會關聯一個CPU開銷。還會可能有一些I/O開銷。比方聚集索引掃描(Clustered Index Scan)會包括CPU和I/O開銷,而其它一些如流聚合(Stream Aggregate)。僅僅有CPU開銷。只是還是那句,我們沒辦法得知具體公式和開銷值,所以以下的演示僅僅有一些基礎的信息。

我們使用演示樣例數據庫AdventureWorks2014。運行以下語句,看一下聚集索引掃描操作的開銷情況:

USE AdventureWorks2014
GO
 
SELECT *
FROM Sales.SalesOrderDetail
WHERE LineTotal = 35


技術分享圖片

在較為舊的SQL Server版本號中,開銷以秒為單位,也和特定硬件配置有關。可是在較新的版本號(最晚從SQL 2008開始),開銷已經跟“單位”沒關系,僅僅是一個數值。

如上圖,聚集索引掃描操作符在我本機的I/O預估開銷為0.915718,CPU預估開銷0.133606。,聚集索引掃描操作符中。對於第一行的CPU開銷為0.0001581。興許每行為0.0000011。由於表中有121317行,所以總CPU開銷為:0.0001581+0.0000011*(121317-1)≈0.133606。相同,對於第一個數據庫頁,最小的I/O開銷為0.003125。然後每一個頁為0.00074074。

由於是全表掃描(聚集索引等同整表),用以下語句檢查實際頁數:

USE AdventureWorks2014
GO
 
SELECTin_row_data_page_count,
    row_count
FROM sys.dm_db_partition_stats
WHERE object_id = object_id(‘Sales.SalesOrderDetail‘)
    AND index_id = 1
 


本機得到的數據為:1233,所以預估I/O開銷為:0.003125 + 0.00074074 * (1233 – 1)=0.91571668≈0.915718。最後,總開銷為0.133606+0.916458=1.05006,就是這個操作符的預估開銷。然後把每一個操作符的總開銷加起來就是這個運行計劃的預估總開銷。

關於統計信息的基礎內容先講到這裏,下一節介紹關於“統計信息常見問題及應對方案”

全廢話SQL Server統計信息(2)——統計信息基礎