一、 统计信息的优缺点
为什么需要统计信息?统计信息降低了在优化过程中必须分析的数据量,如果优化器每次都要访问、统计表、列、索引情况,分析过程会变得非常低效。优化器使用已保存的实际数据的样本(即统计信息)来做分析,样本的量通常会远低于原数据,分析和生成执行计划的速度会快得多。
当然,统计信息也有缺点——维护成本、阻塞与准确度。对于大型数据库,统计信息创建和维护会消耗很多资源和时间,维护期间还可能造成阻塞业务。另外由于统计信息通常是表、列、索引等的采样结果,对于超大型的表来说,准确程度不可能高,导致执行计划选择错误。
二、 统计信息主要组成部分
SQL Server的统计信息包含三个主要部分:
- 头信息
- 密度信息(density information)
- 直方图(histogram)
1. 查看统计信息(图形化)
表刚创建完时,统计信息这个文件夹下面是空的,因为表没有被使用,优化器不会对这个表创建任何统计信息。当第一次使用或者创建索引(实际上也是对数据进行使用)时,就会创建统计信息。
我们可以尝试两个操作,第一个是执行一个简单的SELECT语句,优化器会对上面用到的列创建统计信息:
需要注意要带上WHERE条件,竖框部分的1代表 表创建时的第一列也就是x,而_WA_Sys代表由SQL Server自动创建的统计信息,WA传说是SQL Server开发组所在地华盛顿(Washington)的缩写。
下面再来创建一个索引:
可以看到又多了一个统计信息,并且统计信息名和索引名一样,这个可以说是SQL Server自己创建的,也可以说是用户操作导致的。为了和前面_WA这个做区别,通常把它定义为非SQL Server自动创建的统计信息。
2. 查看统计信息(命令行)
首先找到统计信息名称(name字段):
use StatisticsTest;
SELECT * FROM sys.stats WHERE object_id = object_id('dbo.T1');
可以看到每个统计信息都单独存在一行中
使用DBCC SHOW_STATISTICS详细展示某个统计信息:
dbcc show_statistics('表名','统计信息名')[with <option>]
option有4个选项,4个选项可以都用上
NO_INFOMSGS
STAT_HEADER
DENSITY_VECTOR
HISTOGRAM
如果要查询的统计信息不存在,会得到以下错误:
这里是因为我们从没有使用过a这个列,只要我们执行一个使用到它的语句,就可以查询统计信息:
可以看到输出结果就对应了前面提到的统计信息的三个部分——头信息、密度信息和直方图。
3. 头信息
下面来看看每个列的含义:
- Name:统计信息名。所有自动创建的统计信息都以_WA_Sys开头,跟着的数字表示统计信息是基于表的第几列创建的,注意自动创建的统计信息只会在单列上,对于多列组合的统计信息必须手动创建,这个列顺序是表创建时的顺序,可以通过sys.columns查看,最后的十六进制值是表的object_id,可以转为10进制然后使用Object_Name()函数得出表名:
- Updated:统计信息创建或最后一次更新的时间。
- Rows:上次更新统计信息时表或索引中的总行数。如果有采样或与过滤索引相对应,则行数可能小于表中的行数。
- Rows Sampled:表示统计信息创建或最近一次更新时的取样行数。
- Steps:直方图数据采样步长。每个步长跨越一系列列值,后跟上限列值,最大步数为200。直方图部分会介绍。
- Density:统计信息列 1/唯一值个数(数据密度),不包括直方图边界值。该值是为与2008之前版本兼容,优化器实际不使用。
- Average key length:统计信息列的平均字节数。这个值可以通过把列中每行的字节数加起来再除以总行数计算出来
- String Index:统计信息是否包含字符串信息,只有YES或NO可选,这个值可以对LIKE条件提供预估支持。字符串统计信息只对第一列创建且必须为字符串类型,由于这里的统计信息是建在A列,且这列是字符串类型,所以这里的值为YES。
- FilterExpression和UnfilteredRows:这两个值只在统计信息创建在过滤索引上才出现非null的值,后面介绍。
查看统计信息采样百分比
其实就是上面的 rows sampled/rows,当然也可以用sql来查
SELECT sch.name + '.' + so.name AS table_name
, so.object_id
, ss.name AS stat_name
, ds.stats_id
, ds.last_updated
, ds.rows
, ds.rows_sampled
, ds.rows_sampled*1.0/ds.rows *100 AS sample_rate
, ds.steps
, ds.unfiltered_rows
, ds.modification_counter
FROM sys.stats ss
JOIN sys.objects so ON ss.object_id = so.object_id
JOIN sys.schemas sch ON so.schema_id = sch.schema_id
CROSS APPLY sys.dm_db_stats_properties(ss.object_id,ss.stats_id) ds
WHERE so.name = N'pbCutClothCost'
AND LEFT(ss.name, 4) != '_WA_';
查看整个数据库的所有用户表的采样比例,可以使用下面脚本
--适应于SQL Server 2016 (13.x) SP1 CU4之前的版本
SELECT sch.name + '.' + so.name AS table_name
, so.object_id
, ss.name AS stat_name
, ds.stats_id
, ds.last_updated
, ds.rows
, ds.rows_sampled
, ds.rows_sampled*1.0/ds.rows *100 AS sample_rate
, ds.steps
, ds.unfiltered_rows
--, ds.persisted_sample_percent
, ds.modification_counter
FROM sys.stats ss
JOIN sys.objects so ON ss.object_id = so.object_id
JOIN sys.schemas sch ON so.schema_id = sch.schema_id
CROSS APPLY sys.dm_db_stats_properties(ss.object_id,ss.stats_id) ds
WHERE so.is_ms_shipped = 0
AND so.object_id NOT IN (
SELECT major_id
FROM sys.extended_properties (NOLOCK)
WHERE name = N'microsoft_database_tools_support' );
4. 密度信息
本例中比较简单,是单列统计信息,所以密度信息这部分比较少,后面会演示多列统计信息。
- All density:统计信息列(多列先不管)1/唯一值的个数。
- Average Length:表示唯一值的平均长度。
- Columns:表示这个密度包含哪些列。
这部分内容有啥用呢?大部分情况下,它可以提供GROUP BY 和ON条件中的未知值(比如本地变量)信息给优化器。
下面看一个3列的统计信息密度
DBCC SHOW_STATISTICS('Sales.SalesOrderDetail', IX_SalesOrderDetail_ProductID)
可以看到这个索引包含了3列,每种组合情况下的密度及平均长度信息。
SELECT ProductID FROM Sales.SalesOrderDetail GROUP BY ProductID
优化器在编译上面这个语句时,由于ProductID列上有统计信息,不需要遍历整表,直接从密度信息中就可以获取唯一值(Group By本质就是去重)的预估行数。
密度=1/唯一值个数,只计算ProductID,所以结果就是265.99996。
那么在使用本地变量的情况下会如何?先看本地变量+等值查询
本地变量在统计信息的行为上有点特殊,优化器在实际运行之前没办法知道SQL语句中的参数会是什么值,但是它又必须产生一个预估执行计划,此时也没办法使用直方图,所以优化器只能借助密度信息来获取预估行数。
此时的预估行数=表总行数*密度信息。也就是:0.003759399 * 121317≈456.079。
另一种情况,使用本地变量+范围查询
这种情况下参数的具体值已经没所谓了,大家可以试一下随便填一个值。此时优化器连密度信息都不能用了,sqlserver会使用一个标准假设(表总数的30%作为选择度)。也就是说在这种不等操作符中,预估行数总是表总数的30%,即121317*30%≈36395.1。这个瞎蒙的30%有时会带来很大的性能影响,特别是对超大表,30%的量还是非常大的,但是可能实际上语句只需要极其少量的数据。因此,在查询中应尽可能避免本地变量,而使用参数化或者字符串形式。因为此时优化器可以使用直方图来协助预估,提供更准确的预估行数。
5. 直方图
在SQL Server中,只会对统计对象的第一列创建直方图,并且压缩列中的分布值的信息到一系列子集中,这些子集称为桶(buckets)或者步(steps)。为了创建直方图,SQL Server需要先在首列中查找唯一值,并且使用最大偏差算法尝试获取最常用的变化值,以便把最显著的统计信息保存下来。最大偏差是其中一个用于精确地表达数据库中数据分布情况的算法。
在SQL Server中,可以使用DBCC SHOW_STATISTICS命令查看直方图
DBCC SHOW_STATISTICS ('Sales.SalesOrderDetail',IX_SalesOrderDetail_ProductID)
前面提到,直方图只对统计对象的首列进行创建,所以在这个统计信息中,只对ProductID列创建统计信息。下面来看看这个语句产生的关于直方图方面的结果:
- RANGE_HI_KEY:采样步长最大值(直方图中范围边界上限)。比如上图第21行为738,第22行为741,表示第22行统计信息范围是(738,741]。
- RANGE_ROWS:步长范围中的预估行数。
- EQ_ROWS:采样步长最大值预估行数。即与RANGE_HI_KEY相同的值有多少,还是看22行,最大值是741,直方图预估等于741这个值的数有97个。
- DISTINCT_RANGE_ROWS:在此步长范围中(最大值除外)唯一值的个数。
- AVG_RANGE_ROWS:此步长范围中唯一值的平均行数(最大值除外),即RANGE_ROWS/DISTINCT_RANGE_ROWS
三、 统计信息设置
1. 四个重要参数
- Auto Create Statistics:是否自动创建统计信息,默认开启。
- Auto Update Statistics:是否自动更新统计信息,默认开启。
- Auto Update Statistics Asynchronously:是否采用异步方式更新统计信息,默认关闭。启用时,优化器将首先运行查询,然后更新过时的统计信息。否则优化器将在编译查询之前更新过时的统计信息,阻塞查询。此选项在OLTP中非常有用,但在OLAP中可能会产生负面影响。
- Auto Create Incremental Statistics:是否自动创建增量统计信息,2014新增选项,默认关闭。启用时,重建每个分区的统计信息。否则,SQL Server将删除统计信息树并重新计算统计信息。如果不支持每个分区的统计信息,则会报错。
UPDATE STATISTICS (Transact-SQL) - SQL Server | Microsoft Learn
2. 检查
检查数据库Model统计信息设置,新增数据库会以这个数据库为模版。
SELECT
database_name = name
,[IsAutoCreateStatistics?] =
CASE is_auto_create_stats_on
WHEN 1 THEN 'Yes'
ELSE 'No'
END
,[IsAutoUpdateStatistics?] =
CASE is_auto_update_stats_on
WHEN 1 THEN 'Yes'
ELSE 'No'
END
,[IsAutoUpdateStatsaAyncOn?] =
CASE is_auto_update_stats_async_on
WHEN 1 THEN 'Yes'
ELSE 'No'
END
,[IsAutoCreateStatisticsIncremental?] =
CASE is_auto_create_stats_incremental_on
WHEN 1 THEN 'Yes'
ELSE 'No'
END
FROM sys.databases
WHERE name = 'model'
还可以使用下面的sql查询:
SELECT
[IsAutoCreateStatistics?] =
CASE
WHEN DATABASEPROPERTYEX('AdventureWorks2008R2', 'IsAutoCreateStatistics') = 1
THEN 'Yes'
ELSE 'No'
END,
[IsAutoUpdateStatistics?] =
CASE
WHEN DATABASEPROPERTYEX('AdventureWorks2008R2', 'IsAutoUpdateStatistics') = 1
THEN 'Yes'
ELSE 'No'
END,
[IsAutoUpdateStatsaAyncOn?] =
CASE
WHEN DATABASEPROPERTYEX('AdventureWorks2008R2', 'Is_Auto_Update_stats_async_on') = 1
THEN 'Yes'
ELSE 'No'
END
,[IsAutoCreateStatisticsIncremental?] =
CASE
WHEN DATABASEPROPERTYEX('AdventureWorks2008R2', 'IsAutoCreateStatisticsIncremental') = 1
THEN 'Yes'
ELSE 'No'
END
GO
当然也可以使用SSMS查看:Right Click On Database => Properties => Options
3. 设置
- 图形界面:位置同上步,直接改即可
- 命令行
-- 启用
ALTER DATABASE YourDBName SET AUTO_CREATE_STATISTICS ON;
-- 关闭
ALTER DATABASE YourDBName SET AUTO_CREATE_STATISTICS OFF;
- 使用SP_DBOPTION
SP_DBOPTION DBMonitor, 'auto update statistics', 'ON';
SP_DBOPTION DBMonitor, 'auto update statistics', 'OFF;
四、 创建统计信息
1. 自动创建
其实最前面提到过,这里再概括一下
- 执行查询语句时,优化器会判断谓词中使用到的列统计信息是否可用,如果不可用则会单独对每列创建统计信息。
- 当手动创建索引时,SQL Server会为对应索引自动创建一个同名的统计信息。
2. 手动创建
还可以用CREATE STATISTICS语句手动创建统计信息。
USE AdventureWorks2008R2
GO
CREATE STATISTICS st_anotherID ON dbo.tb_TestStats(anotherID)
GO
五、 更新统计信息
1. 查询统计信息最近更新时间
- 方法1:用系统视图sys.stats和系统函数STATS_DATE
--查看统计信息的更新时间
DECLARE @TableName NVARCHAR(128);
SET @TableName = '[Maint].[JobHistoryDetails]';
SELECT @TableName AS Table_Name,
name AS Stats_Name ,
STATS_DATE(object_id, stats_id) AS Last_Stats_Update
FROM sys.stats
WHERE object_id = OBJECT_ID(@TableName)
ORDER BY 2 DESC;
-- 或者
DECLARE
@day_before int = 30;
SELECT
Object_name = OBJECT_NAME(object_id)
,Stats_Name = [name]
,Stats_Last_Updated = STATS_DATE([object_id], [stats_id])
FROM sys.stats WITH(NOLOCK)
WHERE STATS_DATE([object_id], [stats_id]) <= DATEADD(day, -@day_before, getdate());
- 方法2:sp_autostats
--查看统计信息的更新时间
EXEC sp_autostats '[Maint].[JobHistoryDetails]';
- 方法3:sys.dm_db_stats_properties(2008 R2 SP2后可用)
SELECT sch.name + '.' + so.name AS table_name
, so.object_id
, ss.name AS stat_name
, ds.stats_id
, ds.last_updated
, ds.rows
, ds.rows_sampled
, ds.rows_sampled*1.0/ds.rows *100 AS sample_rate
, ds.steps
, ds.unfiltered_rows
--, ds.persisted_sample_percent
, ds.modification_counter
FROM sys.stats ss
JOIN sys.objects so ON ss.object_id = so.object_id
JOIN sys.schemas sch ON so.schema_id = sch.schema_id
CROSS APPLY sys.dm_db_stats_properties(ss.object_id,ss.stats_id) ds
WHERE so.is_ms_shipped = 0
AND so.object_id NOT IN (
SELECT major_id
FROM sys.extended_properties (NOLOCK)
WHERE name = N'microsoft_database_tools_support' );
列名 | 数据类型 | Description |
object_id | int | 要返回统计信息对象属性的对象(表或索引视图)的 ID。 |
stats_id | int | 统计信息对象的 ID。 在表或索引视图中是唯一的。 有关详细信息,请参阅 sys.stats (Transact-SQL)。 |
last_updated | datetime2 | 上次更新统计信息对象的日期和时间。 有关详细信息,请参阅此页中的备注部分。 |
rows | bigint | 上次更新统计信息时表或索引视图中的总行数。 如果筛选统计信息或者统计信息与筛选索引对应,该行数可能小于表中的行数。 |
rows_sampled | bigint | 用于统计信息计算的抽样总行数。 |
Step | int | 直方图中的值范围数(步长)(Number of steps in the histogram)。 有关详细信息,请参阅 DBCC SHOW_STATISTICS (Transact-SQL)。 |
unfiltered_rows | bigint | 应用筛选表达式(用于筛选的统计信息)之前表中的总行数。 如果未筛选统计信息,则 unfiltered_rows 等于行列中返回的值。 |
modification_counter | bigint | 自上次更新统计信息以来前导统计信息列(构建直方图的列)的总修改次数。 |
persisted_sample_percent | float | 持久样本百分比用于未显式指定采样百分比的统计信息更新。 如果值为零,则不为此统计信息设置持久样本百分比。 |
2. 自动更新统计信息
sqlserver会自动更新“过期”统计信息,那什么样的统计信息算是过期?
1)默认自动统计信息更新策略:
普通表
- 表数据从0行变为>=1行
- 对<500行的表,当统计信息第一个字段数据累计变化量>500 以后
- 对>=500行的表,当统计信息第一个字段数据累计变化量> 500+20%*表行数 以后
临时表
- 可创建统计信息,维护策略与普通表基本相同
表变量
- 不能创建统计信息
2)会强制触发统计信息更新的语句(包括但不限于下面三种,别的没想起来):
- rebulid / reorg index
- 主动update statistics
- 数据库级别的sp_updatestats
3. 默认自动统计信息更新策略的问题
- 对于大表而言,触发统计信息更新的阈值太大,会导致某些统计信息长期无法更新
比如对于1000万行的表,数据变化要超过500+1000w*20%=2000500后才能触发统计信息的更新,大多数情况下这是无法接受的,由于统计信息导致的执行计划不合理的情况在实际业务中更是屡见不鲜。
- 默认取样比例无法准确描述大表数据分布
即使统计信息是最新的,对于大表也不一定可靠,因为统计信息中还有一个取样行数的问题。SQL Server默认的取样行数是有上限的,这个上限值在100W行左右,对于上亿行的表,这个比例其实非常低。比如下图超过3亿行的表,更新统计信息时候未指定取样百分比,默认取样才取了84万行)。
4. 能否降低自动统计信息更新的阈值
- SQL Server 2016之前
为解决大表自动统计信息更新阈值过高的问题,SQL Server 2008 R2 SP1中引入了跟踪标志2371,该标志覆盖了超过25,000行的表的自动更新统计功能的默认阈值。如果启用此跟踪标志,则对于具有大量行的表,更新统计信息的新阈值将更低。
DBCC TRACEON (2371,-1)
DBCC TRACEOFF (2371,-1)
- SQL Server 2016开始
SQL Server将忽略2371标志,因为默认已启用动态更新统计信息阈值。 表具有的行越多,阈值越低。通过这种方式,统计信息将更频繁地更新,并将保证更好的查询性能。关于降低的比例,大致可以参考下图:
5. 手动更新统计信息
当然我们也可以手动更新统计信息,包含以下三个级别:
- 更新索引、列级别统计信息
--update statistcis for a specify statistic
UPDATE STATISTICS dbo.tb_TestStats PK_tb_TestStats;
UPDATE STATISTICS dbo.tb_TestStats Ind_History WITH FULLSCAN;
- 更新单表级别统计信息(锁表)
--update statistcis for a specify table
UPDATE STATISTICS [表名]
UPDATE STATISTICS [表名] WITH FULLSCAN;
--可以自己设定行数
UPDATE STATISTICS [表名] WITH ROWCOUNT=1000000, PAGECOUNT=10000;
- 更新整个数据库统计信息(锁表)
--update statistcis for a specify database
USE AdventureWorks2008R2
GO
EXEC sys.sp_updatestats
GO
- 更新实例级别统计信息(少用)
可以使用系统存储过程sys.sp_updatestats和sys.sp_msforeachdb(非公开)来遍历更新整个实例级别统计信息。
参考
SQL Server · 特性介绍 · 统计信息
https://www.mssqltips.com/sqlservertip/2766/sql-server-auto-update-and-auto-create-statistics-options/
https://www.virtual-dba.com/sql-server-statistics-trace-flag-2371/
https://www.mssqltips.com/sqlservertip/4473/auto-update-statistics-enhancement-in-sql-server-2016/
SQL Server 查找统计信息的采样时间与采样比例