SQL Server 中统计信息直方图中对于没有覆盖到谓词预估以及预估策略的变化(SQL2012-->SQL2014-->SQL2016)

时间:2023-12-13 19:07:08

本文出处:http://www.cnblogs.com/wy123/p/6770258.html

统计信息写过几篇了相关的文章了,感觉还是不过瘾,关于统计信息的问题,最近又踩坑了,该问题虽然不算很常见,但也比较有意思。
相对SQL Server 2012,发现在新的SQL Server版本(2014,2016)中都有一些明显的变化,下文将对此进行粗浅的分析。

SQL Server 2012中(包括之前的版本),因表中数据变化,但统计信息尚未更新的情况下,对于直方图中没有覆盖到的谓词过滤时,sqlserver总是预估为1行
SQL Server 2014和 Server 2016中这种估算方式都有所变化,从表现看,对于对于没有覆盖到的谓词过滤的预估,每个版本都是不同的。
本文简单测试一下此种情况在SQL Server 2012,SQL Server 2014,SQL Server 2016的不同表现,以及该问题可能造成的潜在影响。

下面涉及到的测试环境的数据库版本如下

SQL Server 中统计信息直方图中对于没有覆盖到谓词预估以及预估策略的变化(SQL2012-->SQL2014-->SQL2016)SQL Server 中统计信息直方图中对于没有覆盖到谓词预估以及预估策略的变化(SQL2012-->SQL2014-->SQL2016)SQL Server 中统计信息直方图中对于没有覆盖到谓词预估以及预估策略的变化(SQL2012-->SQL2014-->SQL2016)

测试环境准备

首先利用如下脚本,建一张测试表,写入测试数据,下面会解释测试数据的分布

create table A
(
IdentifierId int identity(1,1),
Id1 int,
Id2 int,
OtherCol CHAR(500)
)
GO begin tran
declare @i int = 1
while @i<=1000000
begin
insert into A values ((@i/50000)+1,@i,NEWID())
set @i = @i+1
if (@i%500000)=0
begin
if @@TRANCOUNT>0
begin
commit
begin tran
end
end
end
if @@TRANCOUNT>0
begin
commit
end
GO

插入的测试数据的分布如下,Id1是从1~20,每一个Id1对应50000个不同的Id2

SQL Server 中统计信息直方图中对于没有覆盖到谓词预估以及预估策略的变化(SQL2012-->SQL2014-->SQL2016)

  

统计信息直方图中覆盖到的谓词的预估

  测试:根据直方图中的任何一个Id来做查询,查询之前先创建相关列上的统计信息,发现预估行数是绝对准确的。

  SQL Server 中统计信息直方图中对于没有覆盖到谓词预估以及预估策略的变化(SQL2012-->SQL2014-->SQL2016)

  查看idx_1上的统计信息,上面预估的绝对准确就归结于统计信息100%的取样统计以及Rang_Hi_key的EQ_Rows,直方图中的Id1的分布是1~21

  SQL Server 中统计信息直方图中对于没有覆盖到谓词预估以及预估策略的变化(SQL2012-->SQL2014-->SQL2016)

统计信息直方图中未覆盖到的谓词的预估

  继续插入一个与上面Id2都不一样的数据,这里为50,因为此时插入的是50000行数据,同时又不足以触发统计信息更新,因此发生如下写入数据之后,统计信息并不会更新。
  因此这个插入完成之后,统计信息并没有更新。

   SQL Server 中统计信息直方图中对于没有覆盖到谓词预估以及预估策略的变化(SQL2012-->SQL2014-->SQL2016)

  因为统计信息没有更新,在idx_1的直方图中,是没有Id1=50的信息的,也就说Id1=50不存在于统计信息的直方图中,
  在SQL Server 2012中预估的结果:预估为1行,实际为50000行

  SQL Server 中统计信息直方图中对于没有覆盖到谓词预估以及预估策略的变化(SQL2012-->SQL2014-->SQL2016)

  重复以上测试代码,分别在SQL Server 2014和SQL Server 2016中测试,不重复截图了

  SQL Server 2014中测试如下:行预估为1024.7,实际为50000,
  这个值是通过什么方式计算出来的?暂时还没查到资料。

  SQL Server 中统计信息直方图中对于没有覆盖到谓词预估以及预估策略的变化(SQL2012-->SQL2014-->SQL2016)

  可以确定的是,对于类似情况的预估算法,也就是谓词没有包含在统计信息直方图中的情况下(one specifies a value which is out of range of the current statistics)
  在sqlserver 2014中,经测试,不同情况下预估是不一样的,不是固定的预估为1行,也不是固定预估为的0.1%,也不是简单的Rows Sampled*All density

  SQL Server 2016中测试如下: 预估为49880.8,实际为50000,基本上接近于真实值。
  相对于SQL Server 2012和2014的预估结果,这个预估的准确性看起来还是比较吊的。

  SQL Server 中统计信息直方图中对于没有覆盖到谓词预估以及预估策略的变化(SQL2012-->SQL2014-->SQL2016)

  为什么SQL Server 2016中预估的如此准确?
  因为在SQL Server 2016中,对于直方图中不存在的过滤谓词,在用这个谓词进行查询的时候,会自动更新相关的统计信息,然后再执行查询,
  这个特性,相对于SQL Server 2012和2014来说,是全新的,也是非常实用的。
  SQL Server 2014这个预估策略虽然在2012的基础上做出了一些改进,但是还是没有解决本质问题,以至于人仍旧要人为地干预统计信息的更新。
  在SQL Server 2016中,即便是当前表中改变的数据行还没有达到触统计信息更新阈值的条件(传统上所谓的阈值,500+rowcount*20%),
  统计信息依然会在查询的驱动下更新,通过索引上的统计信息可以看到,参考下图,直方图中生成了一个50的统计。

  SQL Server 中统计信息直方图中对于没有覆盖到谓词预估以及预估策略的变化(SQL2012-->SQL2014-->SQL2016)

  下面就是所谓触发统计信息更新阈值的条件(严格说是该规则仅对SQL Server 2016之前的版本有效,不适应于SQL Server 2016)
    1,表格从没有数据变成有大于等于1条数据。
    2,对于数据量小于500行的表格,当统计信息的第一个字段数据累计变化量大于500以后。
    3,对于数据量大于500行的表格,当统计信息的第一个字段数据累计变化量大于500 + (20%×表格数据总量)以后。
  这个说法,对于SQL Server 2016之前的版本是有效的,对于SQL Server 2016之后的版本是不成立的,我想这个还是值得注意的。

SQL Server 2016中统计信息更新策略相当于之前版本中开启了TraceFlag 2371,参考http://www.cnblogs.com/wy123/p/5748933.html
也即决定统计信息的变化值为动态的,不再拘泥于“数据累计变化量大于500 + (20%×表格数据总量)”这一限制。
除此之外,应该还要其他机制,比如这里的查询所触发的。

造成的问题

为什么微软会在SQL Server 2016中将统计信息的更新策略做出如此的改变,以及为什么笔者会来探究这个问题?
当然在实际业务中被这个问题坑的蛋疼。
问题很明显,类似于测试的场景,在SQL Server 2012(包括之前的版本),这种预估策略存在的严重的缺陷。
比如示例中:
因为没有当前过滤谓词的统计信息(或者说没有收集到当前谓词的统计信息),实际为5000行的情况下,预估为1行。
这种预估策略非常离谱,某种情况下会造成严重的性能问题,估计也很容易猜到,只是遇到的比较少罢了.
下面就简单具体说明,会造成什么问题,以及原因。

  上述问题在什么情况下会造成性能问题,以及影响又多严重,这里仅简单举例说明。下面这个测试是在SQL Server 2012下进行的。
  为演示这个问题,先来做另外一张测试表B,并写入测试数据。

create table B
(
IdentifierId int identity(1,1),
Id2 int,
OtherCol char(500)
)
GO begin tran
declare @i int = 1
while @i<=1000000
begin
insert into B values (@i,NEWID())
set @i = @i+1
if (@i%100000)=0
begin
if @@TRANCOUNT>0
begin
commit
begin tran
end
end
end
if @@TRANCOUNT>0
begin
commit
end
GO create index idx_2 on B(Id2)
GO

借助第二张表做一个测试,从而把错误预估行数造成的缺陷给放大,
执行下面两个SQL,分别查询A.Id1 = 5和A.Id1 = 50的信息,
由数据分布可知,查询总的结果总数会完全一样(截图受影响行数),
虽然A.Id1 = 5和A.Id1 = 50的数据量和分布也完全一样,但是后者的逻辑IO远远超出前者。
就是因为直方图中没有A.Id1 = 50的统计信息,A.Id1 = 50被错误地预估为1行造成的。

   SQL Server 中统计信息直方图中对于没有覆盖到谓词预估以及预估策略的变化(SQL2012-->SQL2014-->SQL2016)

  具体原因就很明了的,了解执行计划的同学应该很清楚。
  因为错误地预估了当前谓词过滤的行数,在A表上,采用索引查找的方式来查询数据,
  事实证明,当前情况下,这是比全表扫描更加低效的一种方式(看逻辑IO),这是其一。
  另外A表查询之后驱动B表的过程中,因为预估为一行,采用了Nested Loop的方式来驱动B表做连接,
  事实上当前情况下Nested Loop并非最好的,可以说是很不好的。
  这里也可以归结为统计信息的直方图中没有过滤谓词上的统计信息,在第一个阶段的预估中错误地估算为1行造成的。

  SQL Server 中统计信息直方图中对于没有覆盖到谓词预估以及预估策略的变化(SQL2012-->SQL2014-->SQL2016)

  这种问题更蛋疼的地方在于,检查Session或者缓存的执行计划的时候,会发现,表面上看,执行计划挺好的啊,都用到索引了。
  比如第二个SQL的执行计划,看起来似乎没问题,也容易直接忽略这个造成的问题,
  从而把重点转向其他地方,使得问题变得更加难以甄别。其实问题正是出在错误地使用了索引,不该使用索引的地方使用了索引。
  这就是执行计划第一步选择错误,造成后面每一步都错误的情况(一步错,步步错),实际情况中,SQL更加复杂,数据量也更大,造成的影响也更大。
  如果上述示例中在再多几张表join,会出现清一色的Nested Loop方式来驱动表连接,这样的话,SQL执行时间和逻辑IO是非常高的。

  附上一个在SQL Server 2016下的测试截图,可见在默认情况下,执行计划做出了正确的选择。

  SQL Server 中统计信息直方图中对于没有覆盖到谓词预估以及预估策略的变化(SQL2012-->SQL2014-->SQL2016)

  SQL Server 中统计信息直方图中对于没有覆盖到谓词预估以及预估策略的变化(SQL2012-->SQL2014-->SQL2016)

 最后: 

1,本文不是说索引的,关于索引的就不多说。
2,本文也的场景虽然不是太常见,稍显特殊,但也是实际遇到的,另外可以看出,微软也在从这个方面逐步改进SQL Server优化器更新统计信息的策略。
3,关于此场景下的预估,在不同版本下,还有不少有意思问题没有抛出来,有机会再说。
4,类似问题只有在数据量相对较大的情况下才能发生,如果是十万以下或者几十万的数据量,对数据库来说算是微小型数据量,类似问题对性能的影响完全体现不出来。
5,如果有人根据本文的测试验证的话,请注意一个细节:对于过滤谓词的预估,分如下两种情况,这两种情况在2012和2014(2016)中预估的方式也是不同的
  1,表中确实没有这个谓词的数据,并且统计信息没有更新,比如Id1 = 50的数据为0行的情况下的预估
  2,表中有这个谓词的数据,同样是统计信息没有更新,比如Id1 = 50的数据为50000行的情况下的预估

总结:

SQL Server 的预估对执行计划的生成有着至关重要的影响,而预估又依赖于统计信息,因此统计信息的更新以及准确性就显得尤为重要。鉴于此,SQL Server在每个版本中,对于统计信息的生成以及更新策略都有着比较大的变化,本文仅仅从一个较小的点出发,来验证SQL Server各个版本中统计信息预估以及更新的一些特点,从中发现类似问题可能产生的潜在的影响,以及SQL Server 2016中的一些改进。