读懂索引的统计信息
索引只能帮助SQL Server找到符合条件的记录,它还需要知道每一种操作所要处理的数据量有多少,从而估算出复杂度,选取一个代价最小的执行计划。而如何获取准确的数据的分布信息,这就是索引的统计信息的作用了。
单列索引的统计信息
- 以 dbo.OrderHeader 为例,这张表保存的是每张订单的概要信息,一张订单只会有一条记录,所以 OrderID 是不会重复的,假设目前有 31474 条记录。
IF OBJECT_ID('dbo.OrderHeader') IS NOT NULL
DROP TABLE dbo.OrderHeader
GO
CREATE TABLE dbo.OrderHeader ( OrderID int, InsertTime datetime default getdate() )
GO
DECLARE @i INT = 43659
DECLARE @j INT = 75132
WHILE @i <= @j
BEGIN
INSERT INTO dbo.OrderHeader (OrderID) SELECT @i
SET @i = @i + 1
END
- 创建索引,运行 dbcc show_statistics 会返回统计信息的3部分内容:
create clustered index OrderHeader_CL on dbo.OrderHeader (OrderID)
dbcc show_statistics ("dbo.OrderHeader",OrderHeader_CL)
1. 统计头信息
- 统计信息的头信息包含的信息比较多,下面只讲几个重要的指标。
| 列名 | 说明 | 示例 |
|---|---|---|
| Name | 统计信息对象的名称 | OrderDetail_CL,也就是索引的名称 |
| Updated | 上一次更新统计信息的日期和时间 | 2026/05/13 3:05PM |
| Rows | 表中的行数 | 31474,完全正确地反映了当前表里数据量 |
| Rows Sampled | 统计信息的抽样行数 | 31474,说明上次更新统计信息的时候对索引列字段全部扫了一遍 |
| Steps | 在统计信息的第三部分,会把数据分成几组 | 这里为 3 则直方图会有3组数据。该值最大为 200 |
| Density | 第一个列前缀的选择性 | 查询优化器不使用此值,纯粹是为了向后兼容 |
| Average key length | 所有键列的每个值的平均字节数 | 示例索引只有1列,类型是int,所以长度为4 |
| String Index | 如果为 YES 则统计信息中包含字符串摘要索引 | 示例索引列类型是int,所以为 NO |
- Updated 上一次更新统计信息的时间非常重要,根据它能够判断统计信息是什么时候更新的,是不是在数据量发生变化以后,是不是存在统计信息不能反映当前数据分布特点的问题。
- 若 Rows Sampled < Rows 则直方图和密度信息是抽样估计的,若 Rows Sampled = Rows 则说明上次更新统计信息的时候,对所有记录的索引列字段都扫描了一遍,这样的统计信息一般都是很精确的。
2. 索引列的选择性
- All density 反映索引列的选择性(Selectivity),它反映了数据集里重复的数据量多少,如果一个字段的数据很少有重复,那么它的可选择性就比较高。
注:如果 All density 小于0.1(越小越好),一般讲这个索引的选择性比较高。如果大于0.1,它的选择性就不高了。
| 列名 | 说明 | 示例 |
|---|---|---|
| All density | 1/非重复值,反映索引列的选择性,若数据很少重复则该值越小 | 示例数据均唯一,1/31474 即 3.177226E-05 |
| Average Length | 索引列的平均长度 | 示例索引只有1列,类型是int,所以长度为4 |
| Columns | 索引列的名称 | 示例索引创建在 OrderID 字段上 |
3. 直方图
- 头信息中的 Steps 为 n,这里就有 n 组数据
- 每组数据的总行数 = RANGE_ROWS + EQ_ROWS
| 列名 | 说明 | 示例 |
|---|---|---|
| RANGE_HI_KEY | 每一组(step)数据的最大值 | 示例分为3组:0 - 43659、43660 – 75131、75132 - 75132 |
| RANGE_ROWS | 每组数据区间的总行数,上限值除外 | 第1、第3组只有1个最大值,排除之后为0, 第2组则有31472行,排除最大值则为31471 |
| EQ_ROWS | 表中值与该组数据上限值相等的行数目 | 示例数据无重复,所以每组只有1个上限值 |
| DISTINCT_RANGE_ROWS | 每组数据区间非重复值的数目,上限值除外 | 示例数据无重复,所以非重复的行数 = RANGE_ROWS |
| AVG_RANGE_ROWS | 每组数据区间重复值的平均数目,上限值除外 | 示例为1,计算公式=RANGE_ROWS/DISTINCT_RANGE_ROWS |
- 根据这个直方图可知:在 OrderID 字段上,最小值是 43659、最大值是 75132、共 31471+1+1+1 个值、且没有重复值
注:如果查询 OrderID 的条件在 43659 - 75132,那么 SQL Server 知道会返回一行,如果不在这个区间,就不会有返回,而且返回的每一行长度都是 4。所以通过统计信息,SQL Server 就能较好地预测返回的结果集的行数和长度。
组合索引的统计信息
- 上面看的索引 OrderHeader_CL 数据分布比较简单,下面来看一个复杂的组合索引 SalesOrderDetail_test_NCL (SalesOrderID,SalesOrderDetailID)
注:SalesOrderDetail_test 的数据比较特别,前10%的数据,属于编号43659到75124这三万多条订单,而后90%的数据,平均属于75124到75132这9张订单
根据此索引的统计信息,分析如下:
- 1、这里的数据分组(step)有 190 个,要详细很多
- 2、在第二部分 Density,不但有索引列 SalesOrderID 的选择性值,还有 SalesOrderID + SalesOrderDetailID 合并起来的选择性值
注:可以看出如果同时使用两个字段进行过滤,其选择性(8.24E-07)会比只使用SalesOrderID(3.18E-05)还要高。
- 3、SQL Server 会维护组合索引中所有字段的选择性信息,但直方图只有组合索引第一个字段 SalesOrderID 的信息,没有 SalesOrderDetailID 的信息
- 4、从直方图的各项值分布情况,可以清楚地看出 SalesOrderDetail_test 这张表的数据分布特点。SQL Server能够根据带入的SalesOrderID值,推断出是只有几条、几十条、还是会有121317条记录返回,所以下面这两句话虽然结构一模一样,但是因为参数值不同,SQL Server选择了不同的执行计划。
场景1:当 SalesOrderID 在 43659 到 75123 之间,SQL Server “猜”到会返回 3 行(EstimateRows)。
select b.SalesOrderID, b.OrderDate, a.*
from dbo.SalesOrderDetail_test a
inner join dbo.SalesOrderHeader_test b on a.SalesOrderID = b.SalesOrderID
where b.SalesOrderID = 72642
场景2:当 SalesOrderID 在 75124 到 75132 之间,SQL Server “猜”到会返回 121317 行(EstimateRows)。
select b.SalesOrderID, b.OrderDate, a.*
from dbo.SalesOrderDetail_test a
inner join dbo.SalesOrderHeader_test b on a.SalesOrderID = b.SalesOrderID
where b.SalesOrderID = 75127






