第三章 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 FROMSales.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 GOSales.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'
查询计划如图:
查询并没有使用ShipDate列非聚集索引,因为它不是一个覆盖索引,并且被执行时,查询优化器根据参数值结合统计信息预估的行数很多,使用IndexSeek和LookUp的组合成本
太高。再观察STATISTICS IO&TIME:
==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.
两者的纯CPU时间和IO是基本一样,因为前者需要处理的数据量多很多,所以CPU消耗时间长一些。接下来,调换两个执行SP的顺序,再执行:
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.
这次两者性能差距就很明显了。参数探测导致优化器采用了适合少量数据的KeyLookUp操作,而第二次查询重用了此查询计划,但是实际它需要处理大量数据,
这时KeyLookUp就导致了明显的性能问题,需要额外的IO和CPU资源。
根据具体的环境和SQL Server版本,有多种处理参数探测的方法:
1. 跟踪标志4136
此跟踪标志使得SQLServer实例不再使用参数探测,而是使用列平均重复个数(=总行数/列的非重复值个数)来估算受影响行数。 这样的估算值是不精确的。
启用此标志将会使得那些正确的参数探测的情况,变得不准确,带来负面影响。所以应该做为最后的手段。
适用于SQL Server 2008 SP1 CU7,SQL Server 2008 R2 CU2,SQL Server 2005 in SP3 CU9。
2. 使用OPTIMIZE FOR查询提示 SQLServer 2005及后续版本中,可以为查询优化器编译查询计划时指定参数的值。如:
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
在2008中还能OPTIMIZE FOR UNKNOWN使得优化器不用参数探测,这个跟T-4136一样,只不过是语句级。
3. 重编译选项
在创建存储过程可以指定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重编译。如果可能尽量使用查询提示,减少重编译的影响范围和开销。