来源:《Pro SQL Server Internals, 2nd edition》CHAPTER 3 Statistics中的Introduction to SQL Server Statistics、Statistics and Execution Plans、Statistics Maintenance
Introduction to SQL Server Statistics
SQLServer统计信息是系统对象,这些对象在索引键值中包含有关数据分布的信息,有时在常规列值中也包含有关数据分布的信息。可以为支持比较操作的任何数据类型(如>;、<;、=等)创建统计信息。
让我们从上一章清单2-15中创建的dbo.Books表中查看IDX_BOOK_ISBN索引统计信息。你可以使用DBCCshow_STATISTICS(‘dbo.Books’,idx_book_isbn)命令来实现这一点。结果如图3-1所示。
如你所见,DBCCshow_STATISTICS命令返回三个结果集。第一个包含有关统计信息的一般元数据信息,如名称、更新日期、更新统计信息时索引中的行数等。第一个结果集中的“步数”列指示直方图中的步数/值(稍后详细介绍)。“密度”值不由查询优化器使用,仅为向后兼容的目的而显示。
第二个结果集称为“密度向量”(Density Vector),其中包含有关统计信息(索引)中关键字值组合的密度的信息。它是根据1/数量的不同值来计算的。公式,并指示每个键值组合的平均行数。尽管IDX_Books_ISBN索引只定义了一个键列ISBN,但它也包括一个聚集索引键作为索引行的一部分。我们的表中有1,252,500个唯一的ISBN值,ISBN列的密度为1.0/1,252,500=7.984032E-07。(ISBN、BookId)列的所有组合也都是唯一的,并且具有相同的密度。
最后一个结果集称为直方图。直方图中的每条记录(称为直方图步骤)都包括统计信息(索引)最左边一列中的采样键值,以及从前面的值到当前的Range_HI_Key值的值范围内的有关数据分布的信息。让我们更深入地检查直方图列。
让我们使用清单3-1所示的代码将一组重复的ISBN值插入到索引中。
Listing 3-1. Inserting duplicate ISBN values into the index .
;with Prefix(Prefix)
as ( select Num from (values(104),(104),(104),(104),(104)) Num(Num) )
,Postfix(Postfix)
as
(
select 100000001
union all
select Postfix + 1 from Postfix where Postfix < 100002500
)
insert into dbo.Books(ISBN, Title)
select
convert(char(3), Prefix) + '-0' + convert(char(9),Postfix)
,'Title for ISBN' + convert(char(3), Prefix) + '-0' + convert(char(9),Postfix)
from Prefix cross join Postfix
option (maxrecursion 0);
-- Updating the statistics
update statistics dbo.Books IDX_Books_ISBN with fullscan;
现在,如果您再次运行DBCSHOW_STATISTICS(‘dbo.Books’,idx_book_isBN)命令,您将看到如图3-2所示的结果。
前缀为104的ISBN值现在有重复的值,这会影响直方图。还值得一提的是,第二个结果集中的密度信息也被更改。具有重复值的ISBN的密度高于(ISBN,BookId)列的组合,后者仍是唯一的。
让我们运行SELECT BookId, Title FROM dbo.Books WHERE ISBN LIKE ‘114%’语句,并检查执行计划,如图3-3所示。
大多数执行计划操作符都有两个重要的属性。实际行数表示在运算符执行期间处理的行数。估计行数表示SQL Server在查询优化阶段估计的该操作符的行数。在我们的示例中,SQLServer估计有2,625行,其中ISBN从114开始。如果查看图2023-2所示的直方图,您将看到步骤10存储了有关ISBN间隔的数据分布的信息,其中包括您正在选择的值。即使使用线性近似,也可以估计接近SQLServer确定的行数。
关于统计,有两件非常重要的事情要记住。
1。直方图仅为最左侧的统计信息(索引)列存储有关数据分布的信息。统计中有关于键值的多列密度的信息,但仅此而已。直方图中的所有其他信息仅与最左侧统计信息列的数据分布有关。
2。SQL Server在直方图中最多保留200个步骤,而不管表的大小以及表是否已分区。每个直方图步骤所覆盖的间隔随着表的增长而增加。这导致在大表的情况下不太准确的统计数据。
对于复合索引,当索引中的所有列都用作所有查询中的谓词时,最好将具有较低密度/较高唯一值百分比的列定义为索引最左侧的列。这将允许SQL Server更好地利用统计信息中的数据分布信息。但是,您应该考虑谓词的SARGability。例如,如果所有查询在WHERE子句中都使用FirstName=@FirstName和LastName=@LastName谓词,则最好将LastName作为索引中最左边的列。尽管如此,像FirstName=@FirstName和LastName<;>;@LastName这样的谓词不是这样的,其中LastName不是SARGable。
Statistics and Execution Plans
默认情况下,SQLServer会自动创建和更新统计信息。数据库级别上有两个选项可控制此类行为:
1。自动创建统计信息控制优化器是否自动创建列级统计信息。此选项不影响始终创建的索引级统计信息。默认情况下,“自动创建统计数据库”选项处于启用状态。
2。启用“自动更新统计信息数据库”选项后,SQL Server会在每次编译或执行查询时检查统计信息是否过时,并根据需要进行更新。默认情况下,“自动更新统计信息数据库”选项也处于启用状态。
您可以使用STATISTICS_NORECOMPUTE索引选项控制索引级别上统计信息的自动更新行为。默认情况下,此选项设置为OFF,这意味着统计信息将自动更新。在索引或表级更改自动更新行为的另一种方法是使用sp_autostats系统存储过程。
SQLServer根据影响统计信息列的INSERT、UPDATE、DELETE和MERGE语句执行的更改数来确定统计信息是否过时。SQLServer计算统计信息列更改的次数,而不是更改的行数。例如,如果将同一行更改100次,它将被视为100个更改,而不是1个更改。
有三种不同的场景,称为统计信息更新阈值,有时也称为统计信息重新编译阈值,其中SQLServer将统计信息标记为过时。
1。当表为空时,在向表中添加数据时,SQLServer将超过统计信息。
2。当表的行数少于500行时,SQL Server将在统计信息列每更改500行后超过统计信息。
3。在SQL Server 2016之前和SQL Server 2016中,数据库兼容级别为<;130:当表包含500行或更多行时,SQL Server会在统计信息列每更改500次以上(占表中总行数的20%)之后超过统计信息。
在数据库兼容级别为130的SQL Server 2016中:大型表的统计信息更新阈值将变为动态的,并取决于表的大小。表的行数越多,阈值就越低。在包含数百万甚至数十亿行的大表上,统计信息更新阈值可能只占表中总行数的一小部分。也可以在SQLServer2008R2SP1及更高版本中使用跟踪标志T2371启用此行为。
表3-1总结了不同版本SQLServer中的统计信息更新阈值行为。
这使我们得出了一个非常重要的结论。使用静态统计信息更新阈值时,触发统计信息更新所需的统计信息列的更改数量与表的大小成正比。表越大,自动更新统计信息的频率就越低。例如,如果表有10亿行,则需要对统计信息列执行大约2亿次更改,以使统计信息过时。建议尽可能使用动态更新阈值。
让我们看看这种行为如何影响我们的系统和执行计划。此时,表dbo.Books有1,265,000行。让我们将250,000行添加到前缀为999的表中,如清单3-5所示。在此示例中,我使用的是未启用T2371的SQL Server 2012。如果在启用动态统计信息更新阈值的情况下运行它,您可以看到不同的结果。此外,SQL Server 2014中引入的新基数估计器也可以更改行为。我们将在本章后面讨论这个问题。
Listing 3-5. Adding rows to dbo.Books
;with Postfix(Postfix)
as
(
select 100000001
union all
select Postfix + 1
from Postfix
where Postfix < 100250000
)
insert into dbo.Books(ISBN, Title)
select
'999-0' + convert(char(9),Postfix)
,'Title for ISBN 999-0' + convert(char(9),Postfix)
from Postfix
option (maxrecursion 0);
现在,让我们运行SELECT * FROM dbo.Books WHERE ISBN LIKE '999%' 的查询,它选择所有带有这样一个前缀的行。
如果检查查询的执行计划(如图3-7所示),您将看到非聚集索引查找和键查找操作,即使在需要从表中选择几乎20%的行的情况下,它们也是低效的。
你还会在图3-7中注意到,IndexSeek操作符的估计行数和实际行数之间存在巨大差异。SQL Server估计,表中只有31.4行带有前缀999,即使有250,000行带有前缀999。因此,产生了一个效率极低的计划。
让我们通过运行DBCCshow_STATISTICS(‘dbo.Books’,idx_book_isBN)命令来查看IDX_BOOK_ISBN统计信息。输出结果如图3-8所示。如您所见,即使我们在表中插入了250,000行,统计信息也没有更新,并且在柱状图中没有前缀999的数据。第一个结果集中的行数与上次统计信息更新期间表中的行数相对应。它不包括刚刚插入的250,000行。
现在,让我们使用UPDATESTATISTICS dbo.Books IDX_Books_ISBN命令使用FULLSCAN命令更新统计信息,然后再次运行SELECT*FROM dbo.Books,其中ISBN类似于‘999%’查询。查询的执行计划如图3-9所示。现在估计的行数是正确的,SQL Server最终得到了一个效率更高的执行计划,该计划使用聚集索引扫描,其I/O读取次数大约是以前的17倍。
如你所见,不正确的基数估计可能导致执行计划效率极低。过时的统计数据可能是基数估计不正确的最常见原因之一。您可以通过检查执行计划中的估计行数和实际行数来精确定位其中的一些情况。这两个值之间的巨大差异常常表明统计数据是不正确的。更新统计信息可以解决这个问题,并生成更有效的执行计划。
Statistics Maintenance
如前所述,SQL Server默认情况下会自动更新统计信息。对于小型表,此行为通常是可以接受的;但是,如果大型表具有数百万或数十亿行,则不应依赖自动统计信息更新,除非您使用的是数据库兼容级别为130或启用跟踪标志T2371的SQL Server 2016。按20%的统计信息更新阈值触发统计信息更新所需的更改数量将非常高,因此,将不会经常触发更新。
在这种情况下,建议您手动更新统计信息。在选择最佳统计信息维护策略时,必须分析表的大小、数据修改模式和系统可用性。例如,如果系统在工作时间以外没有繁重的负载,则可以决定每晚更新关键表的统计信息。不要忘记,统计信息和/或索引维护为SQLServer添加了额外的负载。您必须分析它如何影响同一服务器和/或磁盘阵列上的其他数据库。
在设计统计信息维护策略时需要考虑的另一个重要因素是如何修改数据。对于键值不断增加或减少的索引,例如索引中最左边的列被定义为IDENTITY或填充序列对象时,您需要更频繁地更新统计信息。如您所见,如果特定键值在直方图之外,SQL Server会严重低估行数。此行为在SQL Server 2014到2016中可能有所不同,我们将在本章的后面部分介绍这一点。
你可以使用UPDATESTATISTICS命令更新统计信息。当SQLServer更新统计信息时,它读取数据的一个示例,而不是扫描整个索引。您可以使用FULLSCAN选项更改该行为,该选项强制SQLServer读取和分析索引中的所有数据。正如您可能猜测的那样,该选项提供了最准确的结果,尽管在大型表的情况下,它可能会引入大量的I/O活动。
SQLServer在重建索引时更新统计信息。我们将在第6章“索引碎片”中更详细地讨论索引维护。
可以使用sp_updatestats系统存储过程更新数据库中的所有统计信息。建议您在将数据库升级到SQLServer的新版本后,使用此存储过程并更新数据库中的所有统计信息。您应该将其与DBCCUPDATEUSAGE存储过程一起运行,该存储过程更正了目录视图中不正确的页数和行数信息。
有一个sys.dm_db_stats_properties DMV,它显示自上次统计信息更新以来对统计信息列所做的修改数量。使用该DMV的代码如清单3-9所示。
Listing 3-9. Using sys.dm_db_stats_properties
select
s.stats_id as [Stat ID], sc.name + '.' + t.name as [Table], s.name as [Statistics]
,p.last_updated, p.rows, p.rows_sampled, p.modification_counter as [Mod Count]
from
sys.stats s join sys.tables t on
s.object_id = t.object_id
join sys.schemas sc on
t.schema_id = sc.schema_id
outer apply
sys.dm_db_stats_properties(t.object_id,s.stats_id) p
where
sc.name = 'dbo' and t.name = 'Books';
查询结果(如图3-11所示)表明,自上次统计信息更新以来,对统计数据列进行了250,000次修改。你可以构建一个统计信息维护例程,定期检查sys.dm_db_stats_properties DMV,并使用较大的Modify_Counter值重建统计信息。
另一个与统计信息相关的数据库选项是异步自动更新统计信息。默认情况下,当SQLServer检测到统计信息过时,它会暂停查询执行,同步更新统计信息,并在统计信息更新完成后生成新的执行计划。对于异步统计信息更新,SQLServer使用基于过时统计信息的旧执行计划执行查询,同时在后台异步更新统计信息。建议您保持同步统计信息更新,除非系统具有非常短的查询超时时间,在这种情况下,同步统计信息更新可能会使查询超时。
最后,SQLServer在创建新索引时不会自动删除列级统计信息,你应该手动删除冗余的列级统计信息对象。