SQL Server统计信息:问题和解决方式

时间:2022-11-23 22:33:16

在网上看到一篇介绍使用统计信息出现的问题已经解决方式,感觉写的很全面。

在自己看的过程中顺便做了翻译。

因为本人英文水平有限,可能中间有一些错误。

假设有哪里有问题欢迎大家批评指正。建议英文好的直接看原文: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。你不会看到关于表变量的警告,所以小心表变量的表扫描和行预计。

 SQL Server统计信息:问题和解决方式

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

 SQL Server统计信息:问题和解决方式

解决的方法:

假设可能的话,通过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 Server统计信息:问题和解决方式

其次,你能够考虑用动态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显示。

 SQL Server统计信息:问题和解决方式

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中看一下上面语句真实的运行过程

SQL Server统计信息:问题和解决方式

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)

SQL Server统计信息:问题和解决方式

Picture 6: Correlated columns and Clustered Index Scan

 

为什么这里我们的索引没有被使用?统计信息是最新的。由于我们在插入100000行后显示运行了UPDATE STATISTICS 命令。查询返回0行。因此索引的选择性应该被使用,对吗?看一下索引的预计行数会给我们答案。图7显示了聚集索引扫描的操作符信息。

SQL Server统计信息:问题和解决方式

Picture 7: Wrong row-count estimations for correlated columns

 

看看预计和实际行数存在巨大的偏差。

优化器预计会返回16000行数据。从这个角度看,使用聚集索引扫描是全然能够理解的。

 

因此,这个奇怪的行为的原因是什么?为了弄清楚答案。我们须要检查统计信息。

假设你在对象管理器内打开“统计信息“目录。你可能注意的第一件事情是自己主动为非聚集索引DailyRate生成的统计信息。依据条件”DailyRate<90“,你能够依据统计信息的直方图轻松计算预期的返回行数。

SQL Server统计信息:问题和解决方式

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直方图)

SQL Server统计信息:问题和解决方式

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种你能够看到完美的基数估记和完美的运行计划。

SQL Server统计信息:问题和解决方式

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显示了运行计划:

SQL Server统计信息:问题和解决方式

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/比較上使用表达式。