SQL Server統計信息:問題和解決方式


在網上看到一篇介紹使用統計信息出現的問題已經解決方式,感覺寫的很全面。

在自己看的過程中順便做了翻譯。

因為本人英文水平有限,可能中間有一些錯誤。

假設有哪里有問題歡迎大家批評指正。建議英文好的直接看原文:SQL Server Statistics: Problems and Solutions

 

正文:

 

SQL Server統計信息協助查詢優化器計算執行查詢的最優方式. Holger描寫敘述了常見的統計信息出錯的事情,而且怎樣改善

 

通常你不須要太操心運行SQL查詢的方式.他們被傳送到查詢優化器,首先檢查是否有可用的運行計划.假設沒有,就編譯一個計划.為了做的有效率,他須要可以從各種替代策略的結果中評估中間行數. 數據庫引擎保存了表中每一個索引鍵值的分布統計信息,而且用這些統計信息確定哪些索引用於編譯運行計划.然而假設這些統計信息存在問題,性能就受到影響.統計信息會出什么問題?怎樣修正?我們將通過最常見的問題解釋它怎樣發生和怎樣處理。

 

本章包括的內容:

l   沒有統計信息

l   關閉自己主動更新統計信息選項

l   表變量

l   XML 和空間數據

l   遠程查詢

l   數據庫僅僅讀

l   統計信息存在,可是沒有正確使用

l   在SQL 腳本中使用本地變量

l   推斷中使用表達式

l   參數化問題

l   統計信息不准

l   樣本不足

l   統計信息力度太大

l   過時統計信息

l   沒有自己主動為多列生成統計信息

l   統計信息不支持相關列

l   更新統計信息是有代價的

l   內存分配問題

l   內存需求預計過高

l   內存需求預計過低

l   Best practices 最佳實踐

 

There is no statistics object at all 沒有統計信息

 

假設沒有統計信息。查詢優化器僅僅能猜行數而不能預計他們,相信我這不是你要的。

 

有幾種方法能夠在預計和實際的運行計划中找到查詢優化器是否丟失了統計信息。在這樣的情況下在計划中會看到警告。圖形顯示的運行計划會中有一個感嘆號,而且在擴展的屬性有警告如圖1。你不會看到關於表變量的警告,所以小心表變量的表掃描和行預計。

 

Picture 1: Missing statistics warning

 

假設你想查看當前語句的運行計划,能夠用DMV sys.dm_exec_cached_plans。

SQL Server profiler提供了第二種選項。

假設你在Profiler中使用Event Errors/Warings/Missing Column Statists等事件。當優化器檢測到丟失統計信息,你能夠觀察到日志。

注意假設數據庫或者表啟用了AUTO CREATE STATISTICS選項,這個事件不會被觸發。

 

有幾種情況。你會經歷丟失統計信息:

 

自己主動創建統計信息關閉

 

問題:

 

假設你關閉了 AUTO CREATE STATISTICS OFF選項,而且忽視了手工創建統計信息,優化器會遭受丟失統計信息之苦。

 

解決方式:

 

依賴自己主動創建統計信息,將AUTO CREATE STATISTICS選項設置為ON。

 

表變量

 

問題:

對於表變量,從來不維護統計信息。記住:關於表變量沒有統計信息。當從表變量查詢。預計的行數實始終是1。除非斷定的求值結果為false和表變量沒有關系(比方where 1=0),在這樣的情況下。預計返回的行數為0.

 

解決的方法:

 

假設暫時表包括多行數據。不要指望表變量為暫時表。作為一個經驗法則。對於超過100行的暫時表使用暫時表(#為表名稱的第一個字符)而不是表變量。

 

XML和空間數據

 

問題:

SQL Server不維護XML和空間數據的統計信息,這是一個事實而不是問題。所以不要嘗試找到這些列的統計信息,由於他們不存在。

解決的方法:

假設使用的查詢在搜索XML數據或者過濾空間列時遇到性能問題,XML或者空間索引可能會有幫助。可是這是另外一個故事超出了本文的范圍。

 

Remote queries 遠程查詢

問題:

如果你通過Linked Server從Oracle數據庫查詢一張表並跟本地表關聯。SQL Server並不知道遠程表返回的行數,這是能夠全然理解的,由於這些數據存在Oralce數據庫。

如果你使用OPENROWSET或者OPENQUERY遠程數據訪問,也可能發生。

 

可是你使用DMV的時候可能也遇到這個問題。一定數量的SQL Server DMV僅僅只是是從內部表中查詢數據的殼。

你可能在2005中看到遠程掃描操作或者2008中看到表值函數操作符。

這些操作默認綁定基數預計(取決於server版本號和操作符,他們是1,1000,10000)

 

看一下以下的查詢:

select * from sys.dm_tran_current_transaction


 

 

這是BOL的聲明: “返回一行顯示當前回話事務的狀態信息”

 

可是看圖2的運行計划,預計行數不是1。

顯然優化器在生成計划的時候沒有考慮BOL的內容。

你能夠看到內部表DM_TRAN_CURRENT_TRANSACTION通過OPENROWSET被調用並且預計行數為1000,遠離事實。

Picture 2: Row-count estimation for TVF

 

解決的方法:

假設可能的話,通過TOP(N)字句制定返回行數給優化器一些支持。它僅僅會在N 小於預計行數的情況下有效果。比方 1000在我們的樣例。因此假設你能推測Oracle查詢返回多少行,僅僅須要在OPENQUERY語句加入TOP(n)。

這將有助於更好的基數預計和你使用OPENROWSET結果集進一步關連或者過濾。但這樣的做法有點危急。假設你指定的n值過低,就會使結果集丟失行,這將是一場災難。

 

我們使用sys.dm_tran_current_transaction的樣例,能夠寫出更好的查詢例如以下:

 

select top 1 * from sys.dm_tran_current_transaction


 

 

通常來說這不是必要的,由於簡單的查詢單獨使用足夠快。可是假設你進一步的處理結果集,在連接中使用。那么TOP 1是實用的。

 

假設你發現你是在更復雜的查詢中使用OPENQUERY的結果集進行連接或者過濾操作,先將OPENQUERY的結果導入暫時表是明智的。統計信息和索引在本地表能夠適當的維護,所以優化器有足夠的信息評估行數。

 

數據庫僅僅讀

 

問題:

假設你的數據庫設置為僅僅讀,優化器不能添加丟失的統計信息即使AUTO CREATE STATISTICS被開啟,由於制度數據庫不同意更改。當心有一種特殊用途的僅僅讀數據庫。是的。我說的是快照。假設優化器丟失了統計信息,在數據庫快照中無法自己主動創建。這有可能發生快照被應用於報表應用。我常常讀到建議為報表目的創建快照一邊避免在底層的OLTP系統中執行長時間的資源密集性的報表查詢。在一定程度上可能好一點。可是報表查詢時高度不可預測的通常不同於正常的OLTP查詢。

因此。你的報表查詢有機會由於丟失統計信息或者更糟糕的丟失索引而影響性能。

 

解決方法:

假設你將數據庫設置為僅僅讀,你須要在做之前手動創建統計信息。

 

有合適的統計信息,可是沒有被正確使用:

 

常常有這樣的可能性優化器無法使用統計信息,雖然統計信息存在而且是最新的.這可能是因為拙略的T-SQL代碼導致的。正如本節看到的:

 

在SQL代碼中使用本地變量

 

問題:

 

我們再看一下以下樣例中的查詢:

create table T0(C1INT,C2INT)

 

INSERT INTO T0VALUES(2000,2000)

INSERT INTO T0VALUES(1000,1000)

GO 100000

 

declare @x int

set @x = 2000

select c1,c2from T0

where c1 = @x 


 

圖片5運行計划的第一部分,揭示了實際和預計的行數存在非常大差異。造成這樣的差距的原因是什么?假設你熟悉查詢運行的各個步驟。你會意識到這個問題的答案.在查詢被運行前,計划須要被生成,而且在計划被編譯的時候,SQL Server不知道變量@x的值.當然在我們的樣例中,能夠非常easy斷定@x的值。可是可能有更復雜的表達式會阻止編譯期間計算@x的值.優化器沒有足夠的知識知道真實的@x值。因此沒有辦法從直方圖中合理評估基數.

 

可是等等。至少列C1是有統計信息的,所以假設優化器不能瀏覽直方圖,它可能轉向其它的更一般的數量。正如這個樣例中發生的。假設優化器不能利用統計直方圖,基數的預測能夠通過檢查平均密度。表的總行數也可能和謂詞操作符。假設你看樣例的第一部分,你能夠看到我們插入了100001條記錄到測試表,在c1值2000僅僅有一行,一個1000的值100000行。你能夠通過運行DBCC SHOW_STATISTICS查看統計信息平均密度,可是記住值計算式1/不同數量的值,在我們樣例中計算結果為1/2=0.5。因此優化器為每一個不同值計算的平均行數為100001行*0.5=50000.5。有了這個值,謂詞操作符進場。在我們的樣例中就是“=”。

為准確比較,優化器如果返回C1一個值的平均行數。因此預期是50000.5(再次看圖5的第一部分)

 

其它運算符可能導致不同的選擇預計,平均密度可能會或者不會被考慮。如果“大於”或者“小於”運算符被應用。它僅僅會如果返回30%的表數據。你能夠非常easy的通過我們的測試腳本驗證。

解決方法

假設可能的話。避免在TSQL腳本中使用本地變量。由於這並不總是可行的,有其它選項可用。

 

首先,你能夠考慮引入存儲過程。

他們被完美的設計為通過參數嗅探技術使用參數。首次調用存儲過程,優化器將會找出不論什么提供的參數而且依據這些參數調整生成計划(當然還有基數預計)。雖然你可能面臨其它問題(見下文)。在我們這個樣例中是完美的解決方式。

create procedure getT0Values(@xint)as

select c1,c2from T0

where c1 = @x


 

 

然后通過程序調用運行這個存儲過程

 

exec getT0Values2000


 

 

這個運行計划將會顯示索引查找。

這是由於優化器必須為@x=2000的值產生計划。

 

圖3顯示了這個運行計划,將這個跟圖5中原始計划的一部分比較。

Picture 3: Execution plan of stored procedure

 

其次,你能夠考慮用動態SQL解決問題。好的僅僅是為了澄清一下,我不建議通常廣泛使用動態SQL。這樣絕不是合適的。

動態SQL已經有一些副作用比方可能計划緩存污染。easy遭到SQL注入攻擊,可能添加CPU和內存使用。可是看看這個:

declare @x int

,@cmd nvarchar(300)

set @x = 2000

set @cmd = 'select c1,c2 from T0 where c1=' 

+ cast(@xasnvarchar(8))

exec (@cmd)

 


 

這個運行計划如今是完美的(跟圖3一樣),由於他是在運行EXEC命令的時候創建而且提供的命令字符串被當作參數傳遞這個命令。其實,你不須要平衡結果決定是否使用動態SQL.可是你看到。僅僅要精心挑選選擇性的應用,動態SQL在全部情況下還是不錯的。總之非常easy:知道你要做什么。

 

通常擴展存儲過程sp_executesql能夠幫助你使用動態SQL。同一時候排除了動態SQL的一些弊端。

這是我們的樣例,這次用sp_executesql重寫:

exec sp_executesqlN'select c1,c2 from T0 where c1=@x'

,N'@x int'

,@x = 2000


 

 

再一次運行計划看起來想圖3展現的。  

 

在謂詞使用表達式。

 

問題:

在謂詞中使用表達式也會阻止優化器使用直方圖,看以下的樣例:

 

select c1,c2from T0

where sqrt(c1) = 100


 

 

運行計划在圖4顯示。

 

Picture 4: Bad cardinality estimation because of expression in predicate

 

因此,雖然字段C1的統計信息存在,可是優化器不知道怎樣在表達式POWER(c1,1)應用這些統計信息。因此僅僅能猜行數。這很類似我們上文提到的丟失統計索引的問題,由於表達式POWER(c1,1)根本沒有統計信息。

對優化器來說。POWER(c1,1)是一個non-foldable表達式。很多其它信息能夠參考這篇文章

 

解決的方法

假設有可能重寫SQL代碼以便比較僅僅在“純粹”列上做。比如不是指定:

where sqrt(c1) = 100


 

這樣寫更好:

where c1 = 10000


 

 

幸運的是優化器在評估表達式的時候足夠聰明,某些情況下會在內部重寫(看這篇文章獲得很多其它信息)

 

假設不能重寫查詢。我建議向其它同事同事尋求幫助。假設還是不行。你可能考慮計算列。計算列能夠解決問題,由於計算列維護統計信息。

此外你還能夠在計算列上創建索引,這些在表達式上無法實現。

 

 

參數化問題

 

問題: 

 

假設你使用參數化查詢比方前面事例的存儲過程,你可能面臨另外一個問題。你們記住查詢計划是在存儲過程第一次運行的時候產生而不是運行CREATE PROCEDURE語句。這是參數嗅探的工作方式。

 

計划的生成是利用了第一次調用時提供的參數值評估行數。

問題非常明顯。

假設第一次調用的參數值異常,基數評估會利用這些值生成運行計划並存儲到計划緩存。計划預計的行數比較差,因此興許計划重用使用通常的參數值可能導致性能不佳。

 

看一下以下的存儲過程:

exec getT0Values2000


 

 

假設這是第一次調用存儲過程,為過濾器生成的計划是WHERE c1=2000。由於僅僅有對於c1=2000僅僅有一行,所以索引查找被運行。假設我們像這樣第二次調用:

exec getT0Values1000


 

 

緩存的計划被重用,索引查找被運行。這是非常糟糕的選擇,由於這個查詢要返回100000行。預計和實際的行數有非常大差別。這里表掃描更有效。

 

使用參數有另外一個問題。很類似我們前寫過的在TSQL腳本中使用本地變量。考慮一下這個存儲過程:

create procedure getT0Values(@xint)as

set @x = @x* 2

select c1,c2from T0

where c1 = @x


 

 

改動@x的值不是理想的。參數嗅探技術不會跟蹤不論什么@x的改動,所以運行計划僅僅會依據提供的@x值調整。而不是使用查詢內部的實際值。

假設你像這樣調用上面的存儲過程:

exec getT0Values1000


 

 

然后運行計划為c=1000的過濾做優化而不是c1=2000.你通過這樣的做法愚弄了優化器,不愜意的基數預測非常可能發生。

 

解決的方法:

 

假設你遇到參數嗅探導致的問題,能夠考慮使用查詢提示比方OPTIMIZE FOR或者WITH RECOMPILE。這個話題超出了我們這批文章討論的范圍。

 

嘗試不要在存儲過程內改動參數的值。這樣將擊敗參數嗅探。假設為了進一步處理須要改動參數的值。你能夠考慮將存儲過程拆分成多個小的存儲過程,採用存儲過程的子程序。

在主存儲過程中改動參數的值,調用子程序時使用改變的值。

由於對於每一個存儲過程。會生成單獨的存儲過程:這樣的做法將規避這個問題。

 

統計信息不准確

 

通常我們必須接受一定數量的不確定性分布統計信息。畢竟。統計信息運行一些數據精簡,信息

損失不可避免。

可是假設我們損失的信息對優化器做出合適的基數預計至關重要,我們須要找到一些解決的方法。

統計對象怎樣會變成不精確無法使用呢?

 

樣本不足

 

問題: 

想像一張表有數百萬行。當SQL Server自己主動為這張表的一列自己主動創建或者更新統計信息,它不會考慮表的全部行。為了避免過多的資源消耗比方CPU和IO,通常僅僅有一些演示樣例行處理護統計信息。

這樣可能導致直方圖不能准確代表總體的數據分布。假設優化器預計基數,它可能無法獲得足夠的信息產生高效運行計划。

 

解決的方法:

一般的解決的方法非常easy。你將不得不通過手動更新或者創建索引干預自己主動創建非常更新。

記住你能夠指定樣本大小甚至運行CREATE STATISTICS或者UPDATE STATISTICS全掃描。記住索引重建總是迫使統計信息用全掃描產生。須要意識到,索引重建僅僅會影響到跟索引相關的統計信息不正確列統計有影響。

 

統計力度太廣泛

 

問題:

讓我們再次回到幾百萬行的表。盡管直方圖最大限制為200個條目。我們知道對於400W數據表僅僅有200 step預計行數。對於直方圖的每一個step平均40000000行/200 step=20000行。如列的值平均分布到各行。這不是一個問題。但假設不是呢?這些統計信息可能太粗導致錯誤的基數預計。

 

解決的方法:

表面上看添加直方圖會非常有幫助,可是我們不能這樣做。沒有辦法能夠擴大直方圖超過200條目。可是假設我們不能包括很多其它的列形成直方圖。簡單的使用多個直方圖怎么樣?當然通過篩選統計我們能夠做到。因為直方圖綁定單一的統計,對於相同列使用篩選統計,能夠有多個直方圖。

 

你須要手動創建那些篩選統計信息。同一時候要注意后果,我后面會做解釋。

 

讓我們回到之前的樣例,如果我們有一張表包括90%的歷史數據(從來不被更改)10%的活躍數據.我們樂意創建兩個篩選統計,通過應用時間列過濾將兩個統計對象分開.(你可能傾向於兩個篩選索引,對這個樣例,使用篩選索引或者篩選統計沒有關系).另外,我們對於僅僅展現的歷史數據(90%部分)禁用統計信息自己主動更新。由於僅僅有(10%)的活躍部分。統計信息須要要定期刷新。

 

如今我們遇到一個問題:通過設置CREATE AUTO STATISTICS ON,優化器會添加一個未過濾的統計對象。雖然這列的篩選統計信息已經存在。這個自己主動添加的統計信息也會啟動“自己主動更新”選項。

因此,雖然你看起來僅僅有兩個篩選統計信息,歷史和活躍部分,但終於你會有三個統計信息。另外一個(無過濾)會自己主動加入。

我們不僅有多余的未經過濾的統計信息,並且會無意義的自己主動更新。當然我們能夠對整個表禁用自己主動創建統計信息。可是這樣須要我們創建合適的統計信息。我很不喜歡這個主意。假設有一個自己主動的選項,為什么不依賴它。

 

我想克服這個問題的最佳辦法是手動創建篩選統計信息的時候指定NORECOMPUTE選項。

這將防止優化器加入這個統計對象而且自己主動更新。

 

以下是必須的步驟:

使用NORECOMPUTE選項創建一個未過濾的統計信息為了防止自己主動更新。這個統計知識為了“誤導”優化器。

為10%的活躍數據創建另外一個篩選統計信息,這次啟用自己主動更新。

假設須要的話,為90%的歷史數據創建還有一個篩選統計,禁用自己主動更新。

假設你遵守上面的步驟,表中活躍的部分會獲得相當好的統計信息。

但不幸的是這個解決方式不是免費的,下一章將展示。

 

過時的統計信息

 

我已經在前面提到。統計信息的同步一直落后於真實數據的更改。

因此在某種層面上每一個統計對象都是過時的。在大部分情況下這樣的行為全然能夠接受,但也有情況源數據和統計信息偏差太大。

 

問題:

我們都知道,對於超過500行的表。僅僅有超過20%的列數據被更改,與之相關的統計信息才會無效。所以這些統計信息在下次被使用時才接收更新。

在一些情況下,這個“至少20%“的門檻可能太大了。

通過另外一個樣例能夠最好的解釋:

 

如果我們有一個產品表例如以下:

 

if (object_id('Product','U')is not null)

drop table Product

go

create table Product

( 

ProductId int identity(1,1)notnull

,ListPrice decimal(8,2)notnull 

,LastUpdate date not nulldefaultcurrent_timestamp

,filler nchar(500)notnull default '#'

)

Go

alter table Productaddconstraint PK_Product

primary key clustered (ProductId)


 

包括主鍵和其它一些列,有一個列記錄最后產品的改動時間。

之后,我們搜索單個時間或者時間區間的產品。因此我們在LastUpdate列上創建非聚集索引。

 

create nonclusteredindex ix_Product_LastUpdateon Product(LastUpdate)


 

如今我們添加500000產品到表:

insert Product(LastUpdate, ListPrice)

select dateadd(day,abs(checksum(newid()))% 3250,'20000101')

,0.01*(abs(checksum(newid()))% 20000)

from Numbers where n <= 500000

go

update statistics Productwithfullscan


 

 

我們使用了一些隨機值生成LastUpdate和ListPrice,而且在插入完畢后更新了全部統計信息。

 

精彩的一天。經過艱苦談判,我們高興的宣布在2010年1月之前收購我們的主要競爭對手。非常高興,我們要加入他們的100000產品。

insert Product(LastUpdate,ListPrice)

select '20100101', 100from Numberswhere n<= 100000


 

 

確認一下,那些產品被加入。我們通過以下的語句檢查全部新插入的行:

select * from Product where LastUpdate = '20100101'


 

 

在圖5中看一下上面語句真實的運行過程

Picture 5: Execution plan created by use of stale statistics

因為過時的統計信息,真實行數和預測行數有非常大差異。

我們增加的100000數據沒有超過20%的改動閥值。也就意味着自己主動更新沒有運行。使用的索引查找對於獲取100000行數據不是最好的選擇,除此之外對於索引查找返回的值還要鍵查找。

這個語句在我的PC機上話費了大概300000邏輯讀。表掃描或者聚集索引掃描時一個更好的選擇。

 

解決的方法:

 

當然我們有機會提供知識給優化器通過查詢提示。

在我們的樣例中,假設我們知道聚集索引掃描是最好的選擇,我們能夠指定一個查詢提示。例如以下:

 

select *from Productwith (index=0)

where LastUpdate = '20100101'


 

 

雖然查詢提示能夠做。可是指定查詢提示存在風險。有可能查詢參數被更改或者底層數據被更改。

當這些發生了,你之前實用的查詢提示可能對性能有負面影響。

運行更新統計信息室一個更好的選擇:

 

update statistics Productwithfullscan


 

 

之后。聚集索引掃描被運行。我們能夠看到唯獨大概86000邏輯讀。所以,不要只依賴自己主動更新。

開啟自己主動更新,可是准備着通過手動更新支持自己主動更新,非常可能在你維護窗體的非高峰時間。對於持續增長的列比方IDENTIY列統計信息尤其重要。每加入一行都高於直方圖的最大值,造成優化器非常難或者不可能獲得合適的預計行數。通常你須要更頻繁的更新這些列上的統計信息。而不是等到20%的數據更改。

 

問題

 

篩選統計信息自己主動更新造成兩種特別的問題。

 

首先不論什么數據的改動改變了過濾器的選擇性,不會考慮現有統計信息的有效性。

 

第二也是最重要的,“20%規則“被應用到表的全部行。不僅僅是過濾的數據集。這一事實能夠使你的篩選統計信息高速過時。

讓我再次回到我們的樣例里面有10%的活躍數據和一個篩選統計。假設全部的統計數據集被缸蓋了,雖然對過濾結果集是100%(活躍數據),可是對於整個表僅僅有10%。

即使我們再次改動全部10%的部分,對這個表來說也僅僅有20%的數據更改。

我們篩選統計信息還是過時的。雖然我們已經改動了200%的數據!

請記住,這個也相同適用於篩選索引的篩選統計信息。

 

Solution 解決方式

 

對於眼下大部分我提到的問題,一個合理的解決方式包設計手動更新或者創建統計信息。

假設你採用了篩選索引或者篩選統計,那么手動更新變得更重要。

你不應該只依賴於篩選統計的自己主動更新,而是應該更頻繁的額外運行手動更新。

 

多列統計信息不會自己主動生成

 

問題:

假設我們依賴自己主動創建統計信息,你須要記住那些統計信息都是單列的統計數據。在非常多情況下。假設多列統計信息存在。優化器可以利用多列統計信息獲得更准確的行預計。

 

解決方式:

你必須手動加入多列統計。作為一些查詢分析的結果。假設你懷疑到多列統計信息會幫助添加他們。找到支持的多列統計信息可能很困難。可是數據庫優化顧問(DTA)能夠幫助你完畢這個任務。

 

統計信息不支持相關列

有一個特定的變化統計信息並不像預期的那樣工作。由於他們的目的不是:相關列。相關列我們指列包括的數據相關。有時候你會遇到兩個或多個列的值不是相互獨立的,這種樣例包括小孩的年齡和鞋碼或者性別和身高。

 

問題:

為了顯示為什么這可能導致一個問題,我們將嘗試一個簡單的實驗。讓我們創建以下的測試表,包括租賃車。

create table RentalCar

(

RentalCarID int not null identity(1,1)

primary key clustered

,CarType nvarchar(20)notnull

,DailyRate decimal(6,2)

,MoreColumns nchar(200)notnull default '#'

)


 

這張表有兩列,一個是車型另外一輛是每日租金,以及一些其它的數據跟我們這次的實驗沒特殊關系。

 

我們知道應用程序要查詢車型和日租金,所以最好在這兩列創建索引:

create nonclusteredindex Ix_RentalCar_CarType_DailyRate

on RentalCar(CarType, DailyRate)


 

 

如今我們加入一些測試數據。我們會包括四個不同的車型與每日租金,當然車越好租金越貴。用以下的腳步實現:

with CarTypes(minRate, maxRate, carType)as

(

select 20, 39,'Compact'

union allselect 40, 59,'Medium'

union allselect 60, 89,'FullSize'

union allselect 90, 140,'Luxory'

)

insert RentalCar(CarType, DailyRate)

select carType, minRate+abs(checksum(newid()))%(maxRate-minRate)

from CarTypes

inner join Numberson n<= 25000

go

update statistics RentalCarwithfullscan


 

 

如你所見,豪華車日租金結余90美元和140美元。小型車介於20和39美元,我們加入100000行到表中。

 

如今如果客戶想要一輛豪華車。由於客戶要求價格很地,它不想這輛車日花費超過90美元。以下是查詢:

select *from RentalCar

where CarType='Luxory'

and DailyRate < 90


 

 

我們知道在我們數據庫中沒有這種車,所以查詢會返回0行。可是看一下實際的運行計划(圖6)

Picture 6: Correlated columns and Clustered Index Scan

 

為什么這里我們的索引沒有被使用?統計信息是最新的。由於我們在插入100000行后顯示運行了UPDATE STATISTICS 命令。查詢返回0行。因此索引的選擇性應該被使用,對嗎?看一下索引的預計行數會給我們答案。圖7顯示了聚集索引掃描的操作符信息。

Picture 7: Wrong row-count estimations for correlated columns

 

看看預計和實際行數存在巨大的偏差。

優化器預計會返回16000行數據。從這個角度看,使用聚集索引掃描是全然能夠理解的。

 

因此,這個奇怪的行為的原因是什么?為了弄清楚答案。我們須要檢查統計信息。

假設你在對象管理器內打開“統計信息“目錄。你可能注意的第一件事情是自己主動為非聚集索引DailyRate生成的統計信息。依據條件”DailyRate<90“,你能夠依據統計信息的直方圖輕松計算預期的返回行數。

Picture 8: Excerpt from the statistics for the DailyRate column

 

僅僅須要匯總RANGE_HI_KEY < 90的RANGE_ROWS和 EQ_ROWS的值,就能夠得到‘DailyRate < 90’的行數。

實際上,對於‘DailyRate=89’須要一些特殊的對待,由於這個值沒有被包括在直方圖的step中。因此計算值不會百分百的准確。可是它會讓你了解優化器使用直方圖的方式。因此。我們可能簡單計算行數通過以下的查詢:

 

select count(*)from RentalCarwhere DailyRate< 90


 

 

 

看一下運行計划中預計的行數。在我這里是62949.8(你的數字可能不同,由於我為日租金添加了隨機值)。

 

如今我們對索引列CarType的統計信息做相同的事情(看圖片9直方圖)

Picture 9: Histogram for the CarType column

 

顯然,預計在我們的表中有25378.9豪華車。(這是一個有趣的統計數據。他們使用兩位小數位顯示預計值)

 

以下是優化器怎樣為我們的SELECT語句計算基數:DailyRate<90預計62949.8行,表中總共100000行,對這個過濾計算的密度是62949.8/100000=0.629498.

 

對第二個過濾條件CarType=’Luxory’應用相同的計算。這次,預計的密度為25378.9/100000=0.253789.

 

為了確定總體過濾條件返回的總行數,須要兩個密度相乘。

這樣做。我們得到0.629498*0.253789=0.15976。

查詢優化器將這個值與表總行數結合確定預計的行數,最后計算得到0.15976*100000=15976.這正是圖7顯示的值。

 

要理解這個問題,你須要回顧一些學校的數學。查詢優化器如果參與的兩列值相互獨立,僅通過兩個不同密度計算相乘,這顯然並不是如此。一個列的值對於第二個列的值不是均勻分布的。因此僅僅是對兩列密度相乘在數學上是不對的。它錯誤的判斷‘DailyRate<90’的總密度對於CarType列的全部值和“CarType=’Luxory’ 相同合理。

眼下,優化器不考慮這種依賴關系。可是我們有一些選項來處理類似問題。你將會看到下面解決方式:

 

解決方式

1)使用索引提示

 

當然。我們知道運行計划不愜意,並且非常easy證明假設我們強制優化器使用現有的索引(CarType, DailyRate).。我們能夠簡單的通過加入一個查詢提演示樣例如以下:

select *from RentalCarwith (index=Ix_RentalCar_CarType_DailyRate)

where CarType='Luxory'

and DailyRate < 90


 

 

運行計划如今顯示索引查找。只是注意預計的行數沒有改變。

由於運行計划生成在查詢運行之前。在兩個實驗中基數預計是一樣的。

 

然后,必要的讀此時(通過SET STATISTICS IO ON監控)已經大幅降低。在我的環境中聚集索引掃描須要5578邏輯讀,假設索引查找被使用,僅僅須要四個邏輯讀。

這個改進系數達到1400.

 

當然這個解決方式也暴漏了一個缺點。雖然其實索引提示在這樣的情況下很實用,可是你通常應該避免使用索引提示(或者查詢提示)。索引提示降低優化器的潛在選項。當數據被更改,索引已經不再實用時,能夠導致不愜意的運行計划。更糟糕的是,查詢可能變的無效。假設該索引已經被刪除或者重命名。

有更優秀的解決方法。在以下的段落中提出。

 

2) 使用篩選索引

 

在SQL Server 2008我們有機會使用篩選索引,在我們這個查詢非常適合。由於對於CarType列僅僅有四個不同值,我們能夠創建四個不同的篩選索引相應每一個車型。CarType=’Luxory’的特殊索引看起來例如以下:

create nonclusteredindex Ix_RentalCar_LuxoryCar_DailyRate

on RentalCar(DailyRate)

where CarType='Luxory'


 

 

其余的三個值我們調整過濾條件創建同樣的索引。

 

我們終於得到四個索引和四個統計信息,適應我們的查詢。在圖10種你能夠看到完美的基數估記和完美的運行計划。

Picture 10: Improved execution plan with filtered indexes

 

篩選索引對不超過兩列的關聯列提供了優雅的解決方式。對於當中起作用的列僅僅有少量數據機。以防你須要多列或者你的列值相差很大而且不可預見。你在檢測正確的過濾條件時會遇到困難。同一時候,你須要確保過濾的條件不能重疊。這可能給優化器造成難題。

 

3) Using filtered statistics 使用過濾的統計信息。

 

我想讓考慮上一節。通過創建篩選索引,查詢優化去可以創建一個最佳運行計划。最后它就是這樣做的。由於我們為它提供了非常多改進的基數預計。可是等等。

基數預計不是從索引中獲得。

實際上,跟索引關聯的統計信息做了預計。

 

所以,為什么我們不離開原始的索引而創建篩選統計信息?其實我們正要這樣做。

我們刪除之前的篩選統計索引創建篩選統計信息作為一種替代方法:

drop index Ix_RentalCar_LuxoryCar_DailyRateon RentalCar

go

create statistics sfon RentalCar(DailyRate)

where CarType='Luxory'


 

 

假設我們為CarType列其它三個值做相同的動作,這樣會產生四個不同的直方圖。每個相應這個列的不同值。

再次運行我們上面的測試語句,我們能夠看到運行計划正如圖10展示。

 

請注意面對相同的障礙,就像上一節提到的關於篩選索引,你可能在決定合適的過濾條件遇到問題。

 

4)使用覆蓋索引

 

我已經提到在決定最佳的篩選索引或者過濾你可能遇到問題。顯示情況可能不會像我們樣例那么easy,假設你無法找到微妙的篩選表達式。有一個其它的選項,你能夠考慮:覆蓋索引。

 

假設優化器發現索引包括全部須要的列和行不須要關聯相關表,這個索引覆蓋了查詢。那么索引會被使用。不考慮基數估記。

 

讓我們構建一個索引。

首先移除篩選統計信息確保優化器不依賴它。

之后我們構造一個覆蓋索引。優化器從中受益:

 

drop statistics RentalCar.sf

go

create index IxRentalCar_Covering

on RentalCar(CarType, DailyRate)

include(MoreColumns)

 


 

假設再次運行測試查詢,你會看到覆蓋索引被使用。圖11顯示了運行計划:

Picture 11: Index Seek with covering index

 

你可能會問為什么不包括RentalCarID列。原因是我門已經在聚集索引中包括這列,所以她會被包括在每一個非聚集索引).

 

注意雖然行預測跟真事偏差非常大。

行預測在這里並不重要。由於我們使用了索引,查詢在第一列使用了搜索而且索引覆蓋了查詢。

 

也請注意覆蓋索引也有特殊性。我說的是每一個表都能夠有(實際應該有)聚集索引.假設你的查詢設計為搜索聚集索引的前面列,那么會使用聚集索引,而不考慮行預計。

 

更新統計信息也有代價

 

問題:

實際上這不是一個問題,僅僅是一個事實,你要在規划數據庫維護計划考慮:對於500萬的表運行OLTP操作期間,最好避免開啟自己主動更新統計信息。

 

解決方式

 

相同,自己主動更新的補充方案是手動更新。

你應該加入手動更新任務到你的數據庫維護計划任務列表。記住,更新統計信息會導致緩存的秩序計划又一次編譯,所以你不能更新的太頻繁。假設你仍然遇到在正常操作時自己主動更新代價昂貴的問題,你能夠切換到異步更新。

 

內存分配問題

每一個查詢都須要一定的內存運行。

優化器評估行數和行的大小計算和申請內存大小。

假設這兩個信息都是錯的。優化器會過多或過少預計內存。對於排序和HASH連接是一個問題。內存分配問題能夠分為下面兩個問題:

 

內存需求預計過高

 

問題:

預計的行數太大,分配的內存會太高。

這僅僅是浪費內存,由於分配的這部分內存在查詢期間不會被用到。假設系統已經遇到內存分配競爭。這回導致等待時間添加。

 

解決方式:

當然最好的辦法是調整統計信息或重寫代碼。假設都不可能,你能夠使用查詢提是(比方OPTIMIZE FOR)告訴優化器更好的基數預計。

 

內存需求低估

 

問題:

 

這個問題更嚴重。假設須要的內存被低估,在運行期間不能馬上獲得額外內醋。記過,查詢將中間結果交換到tempdb導致性能減少8-10倍。

 

解決的方法:  

 

每次查詢或者Hash連接使用tempdb,SQL Server profiler事件 Errors and Warnings/Sort Warnings and Errors and Warnings/Hash Warnings可以知道。

當你看到這一點,可能值得進一步調查。

假設你懷疑行預計造成tempdb交換,更新統計信息或者檢查改動代碼會有幫助。假設不能。考慮查詢提示解決問題。

另外,你也能夠添加查詢最低內存分配。通過調整min memory per query (KB)。默認的是1MB。請確保這是在沒有其它解決方式之前。每次改動選項,最好知道你在做什么。

改動配置選項應該是你解決這個問題的最后一個選擇。

 

最佳實踐

 

眼下我提取全部給出的建議,並將它們放入以下的最佳實踐列表。

你能夠覺得這是一個特殊的總結:

 

做懶人。假設有自己主動創建和更新統計信息的進程,使用它們。

讓SQL Server做大部分的工作。

在大部分情況下。自己主動創建和更新統計信息工作的非常好。

 

假設你遇到一個查詢性能問題。這一般是因為過時或者質量差的查詢統計信息導致。在大部分情況下。你沒有時間做更深層次的分析,所以簡單的直線一下統計信息更新。一定不要更新全部表的統計信息,僅僅是更新參與的表或者索引。假設這沒有幫助,你可能找個時間使用full scan更新統計信息。注意運行計划中預計和實際的行數差異。

優化器應該預計而不是推測。假設你看到相當大的差異,這一般是不良的統計信息或者差的TSQL 代碼導致。

 

使用內置的自己主動機制。可是不只依賴這些。對於更新尤其如此。假設須要。能夠通過手動更新支持自己主動更新。

必要時重建碎片索引。這樣能夠使用full scan更新與索引關聯的統計信息.千萬不要在重建索引之后再去更新這些索引的統計信息。

這不僅是不必要的。甚至會減少統計信息的質量,假設默認的採樣被使用。

細致檢查,假設你的查詢能夠使用到多列統計。假設是這樣,手動創建他們。你能夠利用數據庫引擎優化顧問(DTA)進行相關分析。

用篩選統計假設你須要不止200直方圖條目。

當引入篩選統計。你須要運行手動更新。否則你的篩選統計信息會非常快變得過時。

 

 

不要再一列上創建多個統計信息除非他們是篩選統計。

SQL Server不會阻止你在一列上創建多列統計信息。相同你也能夠在一列上有相同的索引。這不光添加維護工作,也添加了優化器的負載。此外,由於優化器始終使用一個特定的統計信息作基數預計。它須要選擇當中的一個。

他通過評價這些統計信息選擇最好的一個。這可能是最新更新的或者具有更大樣本。最后可是不最重要的:提升你的TSQL代碼。

避免在TSQL代碼使用本地變量或者在存儲過程中覆蓋參數。不要在where /join/比較上使用表達式。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM