第三章 High CPU Utilization.
CPU使用率過高的常見原因
查詢優化器會盡量從CPU,IO和內存資源成本最小的角度,找到最高效的數據訪問方式。如果沒有正確的索引,或者寫的語句本身就會忽略索引,
又或者不准確的統計信息等情況下,查詢計划可能不是最優的。
有些查詢計划可能對只對某種條件下的查詢是高效,而不是所有條件下都是。
缺失索引
索引的缺失,會導致查詢處理的行數大大超出必要的行數,從而加重CPU和IO的負載。簡單的例子:
SELECT per .FirstName , per.LastName , p.Name , p.ProductNumber , OrderDate , LineTotal , soh.TotalDue FROM Sales.SalesOrderHeader AS soh INNER JOIN Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID INNER JOIN Production.Product AS p ON sod.ProductID = p.ProductID INNER JOIN Sales.Customer AS c ON soh.CustomerID = c.CustomerID INNER JOIN Person.Person AS per ON c.PersonID = per.BusinessEntityID WHERE LineTotal > 25000 SQL Server parse and compile time: CPU time = 0 ms, elapsed time = 0 ms. SQL Server Execution Times: CPU time = 452 ms, elapsed time = 458 ms.
上面的查詢使用AdventureWorks2008數據庫,字段LineTotal上沒有索引,會導致SalesOrderDetail全表掃描。然后創建如下索引后,改善很明顯:
CREATENONCLUSTEREDINDEX idx_SalesOrde ON Sales.SalesOrderDetail (LineTotal) SQL Server parse and compile time: CPU time = 0 ms, elapsed time = 0 ms. SQL Server Execution Times: CPU time = 0 ms, elapsed time = 8 ms.
過期的統計信息
查詢優化器使用統計信息計算各種查詢操作的基數(開銷)。查詢操作的成本(cost)又決定了查詢計划的成本。過期的統計信息會導致生成非最優的查詢計划,
如預估成本很低,但實際成本很高的計划。
最常見就是預估行數很少,並選擇了那些適合少量數據的操作(如嵌套循環,LookUp),但當實際執行時要處理的行數卻很多,查詢效率就變得很低。
可以通過SSMS或者set statistics profile on為索引查找和掃描操作,返回實際行數與預估行數做比較。如果兩者差異較大,就很有可能統計信息過期了。
過期時,可以使用update statistics tableName更新表上所有的統計信息,update statistics tableName statisticsName更新指定統計信息。
為了防止統計信息過期的問題,有如下三種方法:
a. 開啟數據庫的Auto_Update_Statistics選項或者用定時作業更新全庫的統計信息。
b. 如果某些索引的自動更新統計信息被禁用,則需要指定STATISTICS_NORECOMPUTE=OFF重建索引開啟。
c. 對於某些經常因為統計信息過期而導致性能問題的統計信息,可以創建定時作業頻繁地更新它們。
非SAGR謂詞
SAGR=Search Agrument.簡單說就是能夠使用索引查找的謂詞。列應該直接與表達式進行比較則符合SAGR,如WHERE SomeFunction(Column) = @Value就符合,
WHERE Column = SomeOtherFunction(@Value) 則符合。注意LIKE和BETWEEN也是SAGR謂詞。
非SAGR會導致表或者索引掃描,它的影響跟缺失索引類似。使得CPU處理大量非必需的數據行。下面查詢會導致索引掃描:
SELECT soh .SalesOrderID , OrderDate , DueDate , ShipDate , Status , SubTotal , TaxAmt , Freight , TotalDue FROM Sales.SalesOrderheader AS soh INNER JOIN Sales.SalesOrderDetail AS sod ON soh.SalesOrderID = sod.SalesOrderID WHERE CONVERT(VARCHAR(10), sod.ModifiedDate , 101) = '01/01/2010'
改寫成如下則會使用索引查找:
SELECT soh .SalesOrderID , OrderDate , DueDate , ShipDate , Status , SubTotal , TaxAmt , Freight , TotalDue FROM Sales.SalesOrderheader AS soh INNER JOIN Sales.SalesOrderDetail AS sod ON soh.SalesOrderID = sod.SalesOrderID WHERE sod.ModifiedDate >= '2010/01/01'
AND sod.ModifiedDate < '2010/01/02'
UPPER,LOWER,LTRIM,RTRIM,ISNULL這些經常會被濫用,甚至用於WHERE和JOIN條件中。
在不區分大小寫排序規則中,大小寫被視為相等的,像UPPER,LOWER這種拖累性能的函數就不必要用了。
SQL中字符串比較會忽略末尾空格,所以RTRIM也沒必要用。
下面兩個過濾條件,前者,字段NULL值轉換成0從而被排除;后者中,其實NULL值與任何值比較操作都不會返回TURE,而被排除。
NULL值只在IS NULL或者IS NOT NULL檢查時才可能返回TRUE。所以是等效的,但后者才能使用索引查找。
WHERE ISNULL(SomeCol,0) > 0
WHERE SomeCol > 0
隱式轉換
隱式轉換發生在比較兩個不同數據類型時。SQL不能對不同類型數據進行比較,所以查詢優化器會在比較操作前把低優先級的數據類型轉換成高優先級的數據類型再比較。
這跟非SARG謂詞一樣,將不能使用Index Seek,從而處理很多不必要的數據行,增加CPU開銷。最常見例子是使用NVARCHAR類型的參數與VARCHAR類型的列進行比較。如:
SELECTp .FirstName , p.LastName , c.AccountNumber FROM Sales.Customer ASc
INNER JOINPerson.Person AS p ON c.PersonID = p.BusinessEntityID WHERE AccountNumber = N'AW00029594'
上面的查詢導致一個非聚集索引掃描,在Filter操作中會看有一個COVERT_IMPLICIT。
為了避免隱式轉換:
1. JOIN的列,數據類型盡量相同
2. 與列比較時,任何參數,變量和常量的類型要和列的類型相同
3. 當參數,變量或常量的類型與要比較的列不同時,斟酌地使用類型轉換函數,使其與列類型相同
4. 有些數據訪問組件和開發框架會把字符串類型默認地設置為NVARCHAR
參數探測(Parameter Sniffing)
參數探測是SQL Server為存儲過程,函數和參數化查詢創建查詢計划時用到的處理方式。當首次編譯查詢計划時,SQL Server會檢測或者探測輸入參數的值並結合統計信
息,預估受影響的行數,
並以之估算查詢計划成本。當根據傳入的參數值創建查詢計划,得到的受影響行數不是典型的情況時,就產生問題了。參數探測只出現在編譯和重編譯時,之后的存儲過程,函數和
參數化查詢,
會重用此查詢計划。最初編譯時只有輸入參數的值會被探測到,本地變量是沒有值的。如果批處理中的語句被重編譯,則參數和變量將會被賦值並探測到。示例如下:
CREATEPROCEDUREuser_GetCustomerShipDates ( @ShipDateStart DATETIME , @ShipDateEnd DATETIME ) AS
SELECT CustomerID , SalesOrderNumber FROM Sales.SalesOrderHeader WHERE ShipDate BETWEEN @ShipDateStart AND @ShipDateEnd
GO
Sales.SalesOrderHeader表的ShipDate字段范圍是2004-08-07~2011-08-07,並創建非聚集索引:
CREATENONCLUSTEREDINDEX IDX_ShipDate_ASC ON Sales.SalesOrderHeader (ShipDate ) GO 首先我們執行兩次SP,並用DBCC FREEPROCCACHE在運行前清空計划緩存: DBCC FREEPROCCACHE EXEC user_GetCustomerShipDates '2001/07/08', '2004/01/01'
EXEC user_GetCustomerShipDates '2001/07/10', '2001/07/20'

==FIRST EXECUTION (LARGE DATE RANGE)=== (Table 'SalesOrderHeader'. Scan count 1, logical reads 686, physical reads 0. SQL Server Execution Times: CPU time = 16 ms, elapsed time = 197 ms. SQL Server Execution Times: CPU time = 16 ms, elapsed time = 197 ms. ==SECOND EXECUTION (SMALL DATE RANGE)===
Table 'SalesOrderHeader'. Scan count 1, logical reads 686, physical reads 0. SQL Server Execution Times: CPU time = 15 ms, elapsed time = 5 ms. SQL Server Execution Times: CPU time = 15 ms, elapsed time = 5 ms.
DBCC FREEPROCCACHE EXEC user_GetCustomerShipDates '2001/07/10', '2001/07/20'
EXEC user_GetCustomerShipDates '2001/07/08', '2004/01/01'

==FIRST EXECUTION (SMALL DATE RANGE)===
Table 'SalesOrderHeader'. Scan count 1, logica ahead reads 0, lob logical reads 0, lob physic SQL Server Execution Times: CPU time = 0 ms, elapsed time = 0 ms. ==SECOND EXECUTION (LARGE DATE RANGE)===
Table 'SalesOrderHeader'. Scan count 1, logica SQL Server Execution Times: CPU time = 47 ms, elapsed time = 182 ms.
CREATE PROCEDURE user_GetCustomerShipDates ( @ShipDateStart DATETIME , @ShipDateEnd DATETIME ) AS
SELECT CustomerID , SalesOrderNumber FROM Sales.SalesOrderHeader WHERE ShipDate BETWEEN @ShipDateStart AND @ShipDateEnd
OPTION ( OPTIMIZE FOR ( @ShipDateStart = '2001/07/08' , @ShipDateEnd = '2004/01/01' ) ) GO
在創建存儲過程可以指定WITH RECOMPILE重編譯選項。指定后SP每次執行時會基於當前參數值重新編譯,同時也不緩存執行計划。但是這樣會增加執行處理時間。
CREATE PROCEDURE user_GetCustomerShipDates ( @ShipDateStart DATETIME , @ShipDateEnd DATETIME ) WITH RECOMPILE AS
SELECT CustomerID ,SalesOrderNumber FROM Sales.SalesOrderHeader WHERE ShipDate BETWEEN @ShipDateStart AND @ShipDateEnd
當SP中的多個語句,只是某個語句會產生參數探測問題,則可以對這個語句使用OPTION(RECOMPILE)查詢提示。這樣每次執行時只會對這個語句重編譯,
而不像WITH RECOMPILE對整個SP重編譯。如果可能盡量使用查詢提示,減少重編譯的影響范圍和開銷。
CREATE PROCEDURE user_GetCustomerShipDates ( @ShipDateStart DATETIME , @ShipDateEnd DATETIME ) AS
SELECT CustomerID , SalesOrderNumber FROM Sales.SalesOrderHeader WHERE ShipDate BETWEEN @ShipDateStart AND @ShipDateEnd
OPTION ( RECOMPILE )
即席非參數化查詢
即席查詢不能重用執行計划,每次執行時都會被編譯,消耗大量資源(特別是CPU)。像下面的查詢,每次因為WHERE條件中參數值不同而產生不同的執行計划。
雖然SQL Server有簡單參數化(Simple Parameterization)的技術,但是此語句相對”太復雜”了。
SELECT soh .SalesOrderNumber , sod.ProductID FROM Sales.SalesOrderHeader AS soh INNER JOIN Sales.SalesOrderDetail AS sod ON soh.SalesOrderID = sod.SalesOrderID WHERE soh.SalesOrderNumber = 'SO43662'
非參數化查詢主要有兩方面的影響:
1. 即席查詢產生的一次性的查詢計划會填滿計划緩存。由此帶來的內存壓力,會讓那些本可以重用的計划迫於內存壓力而被清除掉等等。
2. 編譯這些一次性的查詢計划浪費了大量的CPU資源。
可以用下面的計數器來判斷即席非參數化查詢的影響程度:
SQLServer: SQL Statistics: SQL Compilations/Sec
SQLServer: SQL Statistics: Auto-Param Attempts/Sec
SQLServer: SQL Statistics: Failed Auto-Param/Sec
解決的方法有:
1. 修改應用程序代碼,使發送到SQL Server語句盡量被參數化。
2. 在SQL Server 2005及以上版本中,能在數據庫級別設定強制參數化。但可能會帶來類型參數探測一樣的問題。
ALTER DATABASE AdventureWorks SET PARAMETERIZATION FORCED
3. SQL Server 2008及以上版本中,啟用實例級別的optimize for ad hoc workloads。
啟用后當即席查詢第一次執行時只保存查詢計划的一個“存根”,第二次執行時則緩存執行計划。“存根”使用很少的內存,
這樣就減少了那些本可以重用的執行計划,因內存壓力而被清除掉的機率。
EXEC sp_configure 'show advanced options',1
RECONFIGURE
EXEC sp_configure 'optimize for ad hoc workloads',1
RECONFIGURE
不恰當的並行
並行查詢是把一個查詢的工作分解成多個線程執行,每一個線程使用單獨的計划程序。查詢並行發生在操作符(Operator)級別。
查詢優化器在編譯執行計划時總是會讓其盡可能的快。如果執行計划的預估成本連續超過cost threshold of prillelism,同時SQL Server可用
CPU個數多於1個,並且max degree of prallelism為0或大於1,則產生的執行計划將會包括並行。
並行查詢通過水平分割輸入數據,然后分布到多個邏輯CPU上同時執行,從而減少執行時間。對於數據倉庫和報表系統會有好處,對於OLTP系統
則並行會占用過多的CPU資源,而其它的請求不得不等待CPU資源。
SQL Server有兩個控制並行執行的sp_configure選項:
cost threshold of prillelism:控制優化器為查詢使用並行執行的閥值
max degree of prallelism:避免單個查詢用完所有可用的處理器內核
cost threshold of prallelism的默認值為5秒。在較大的數據庫上,默認閥值可能太低了,會導致並行執行的資源爭用。
可以通過下面的查詢獲取存於計划緩存中的並行執行計划,做為調整cost threshold of prillelism的重要參考依據:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED ; WITH XMLNAMESPACES (DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan') SELECT query_plan AS CompleteQueryPlan , n.value ('(@StatementText)[1]', 'VARCHAR(4000)' ) AS StatementText , n.value ('(@StatementOptmLevel)[1]', 'VARCHAR(25)') AS StatementOptimizationLevel , n.value ('(@StatementSubTreeCost)[1]', 'VARCHAR(128)') AS StatementSubTreeCost , n.query ('.' ) AS ParallelSubTreeXML , ecp.usecounts , ecp.size_in_bytes FROM sys .dm_exec_cached_plans AS ecp CROSS APPLY sys .dm_exec_query_plan(plan_handle) AS eqp CROSS APPLY query_plan.nodes ('/ShowPlanXML/BatchSequence/Batch/Statements/StmtSimple') AS qn ( n ) WHERE n.query ('.' ).exist ('//RelOp[@PhysicalOp="Parallelism"]' ) = 1
並行中使用到處理器數是取自后面三者中的最小值:max degree of prallelis的值,可用的處理器數和MAXDOP查詢提示(會覆蓋max degree of prallelis指定的值)。
合適的max degree of prallelis值取決於工作負載類型和硬件資源支撐並行開銷的能力。很多磚家推薦將其設定為1,前提是你的系統是真正的單獨的OLTP系統,
只有大量的並發的小事務。對於NUMA和SMP系統的設定是不同的。SQL Server 2008及以上版本利用resource governor能將max degree of prallelis綁定到特定的查詢組。
當系統出現並行性能問題時,通常會CXPACKET等待會比較明顯,其實它很冤,它只是個表面現象。根本原因還是要查看產生CXPACKET等待的子線程的等待類型。
如果伴隨着如IO_COMPLETION,ASYNC_IO_COMPLETION,PAGEIOLATCH_*的等待,則要提高IO性能;
如果伴隨着LATCH_* and SOS_SCHEDULER_YIELD,則表示並行本身導致了性能問題,如果此時還有ACCESS_METHODS_DATASET_PARENT等待,則執行的並行度是根本原因。
出現這些問題時首先應該優化並行執行的語句,其次才是結合max degree of prallelis和cost threshold of prillelism兩者進行限制,再次是提升硬件。
在硬件暫時無法提升時,只能對並行做一定限制,做折中和權衡考量。
在SQL Server 2005,有個關於TokenAndPermUserStore的CPU問題。當數據庫用戶很多,adhoc和動態查詢很多且非AWE內存空間很多時,
可能會出現CMEMTHREAD大量等待和TokenAndPermUserStore使用過量並持續增長。微軟已經推出的解決方案http://support.microsoft.com/kb/927396。
長期的解決方案是修改程序架構,盡量減少可能導致此問題的adhoc和動態查詢的使用。
短期解決方案是:
1. 給應用程序賬戶提升為sysadmin,從而規避權限檢查而極大的減少TokenAndPermUserStore的緩存使用量。這是有安全風險的
2. 使用DBCC FREESYSTEMCACHE ('TokenAndPermUserStore'),定期清理這一部分緩存。
3. SQL Server 2005 SP2及以上版本,可以使用Trace Flag 4618&4610.4610限制緩存條目為1024,兩者都啟用則限制緩存條目為8192.
4. SQL Server 2005 SP3及以上版本,使用Trace Flag 4621,可以設定緩存配額。 http://support.microsoft.com/kb/959823。
5. SQL Server 2008有access check cache bucket count & access check cache quota兩個sp_configure項,
用於設置TokenAndPermUserStore的hash bucket數量和緩存條目數。
關於超線程和BIOS節能控制選項
很多磚家推薦SQL Server服務器不宜開啟超線程,根據作者的研究,這種推薦做法在過去是對的,現在不一定是對的。
在過去,超線出來的CPU會一起共享板載緩存,而這個緩存是KB量級的。而且windows 2000本身不支持超線程,“超”出來的它會認為是物理CPU。“CPU”一多就會造成緩存命中率低下,進而影響性能。
而很多DBA管理的OLTP&DSS的混合環境,那么超線程對於那些DSS型查詢的提升,在混合環境中很不明顯,甚至有時會導致需要迅速響應OLTP弄查詢等待。
現在,硬件和軟件進步了,現在板載緩存量級都是MB級的了,CPU緩存命中率不再是問題。而windows 2003也支持超線程了。而且現在OLTP,OLAP,DW等系統一般會隔離使用。
當然是否開啟還是需要經過測試才能最終決定。
綠色節能現在也是硬件和服務器一種標准了,自動降低系統中某些暫時未用到的硬件能耗和CPU頻率。BioS中可以設定為硬件自己控制或者OS控制。
windows 2008&r2默認設定電源計划為“平衡”,以允許其切換到節能模式。有時在windows 2008 or R2的新服務器升級系統一段時間后,性能下降明顯,這就是電源管理造成的CPU降頻導致的。
windows的性能計數器 % Processor Usage 是已使用CPU頻率除以可用CPU頻率得到的。如果CPU被降頻了,那么這個計算器會較高,會讓人誤會成工作負載很高。
可以使用CPU-Z來檢測CPU的狀態。
建議將windows的電源計划調定為“高性能”,並且檢查BIOS的電源控制選項,設定為OS Control.
總結:
1. SQL SERVER CPU調整的地方很少,很多時候還是因為語句性能低下導致CPU使用過高。
2. 解決參數嗅探問題,還有拼動態語句,使用plan_guide等方法。