如何在DateAdd()约束下的索引中改进视图中1行的估计


8

使用Microsoft SQL Server 2012(SP3)(KB3072779)-11.0.6020.0(X64)。

给定一个表和索引:

create table [User].[Session] 
(
  SessionId int identity(1, 1) not null primary key
  CreatedUtc datetime2(7) not null default sysutcdatetime())
)

create nonclustered index [IX_User_Session_CreatedUtc]
on [User].[Session]([CreatedUtc]) include (SessionId)

以下每个查询的“实际行数”为3.1M,估计的行数显示为注释。

当这些查询在View中提供另一个查询,由于1行估算,优化器选择循环连接。 如何在此基础上改进估计,以避免覆盖父查询联接提示或求助于SP?

使用硬编码日期非常有用:

 select distinct SessionId from [User].Session -- 2.9M (great)
  where CreatedUtc > '04/08/2015'  -- but hardcoded

这些等效查询是视图兼容的,但全部估计1行:

select distinct SessionId from [User].Session -- 1
 where CreatedUtc > dateadd(day, -365, sysutcdatetime())         

select distinct SessionId from [User].Session  -- 1
 where dateadd(day, 365, CreatedUtc) > sysutcdatetime();          

select distinct SessionId from [User].Session s  -- 1
 inner loop join  (select dateadd(day, -365, sysutcdatetime()) as MinCreatedUtc) d
    on d.MinCreatedUtc < s.CreatedUtc    
    -- (also tried reversing join order, not shown, no change)

select distinct SessionId from [User].Session s -- 1
 cross apply (select dateadd(day, -365, sysutcdatetime()) as MinCreatedUtc) d
 where d.MinCreatedUtc < s.CreatedUtc
    -- (also tried reversing join order, not shown, no change)

尝试一些提示(但查看时不适用):

 select distinct SessionId from [User].Session -- 1
  where CreatedUtc > dateadd(day, -365, sysutcdatetime())
 option (recompile);

select distinct SessionId from [User].Session  -- 1
 where CreatedUtc > (select dateadd(day, -365, sysutcdatetime()))
 option (recompile, optimize for unknown);

select distinct SessionId                     -- 1
  from (select dateadd(day, -365, sysutcdatetime()) as MinCreatedUtc) d
 inner loop join [User].Session s    
    on s.CreatedUtc > d.MinCreatedUtc  
option (recompile);

尝试使用参数/提示(但对视图不可用):

declare
    @minDate datetime2(7) = dateadd(day, -365, sysutcdatetime());

select distinct SessionId from [User].Session  -- 1.2M (adequate)
 where CreatedUtc > @minDate;

select distinct SessionId from [User].Session  -- 2.96M (great)
 where CreatedUtc > @minDate
option (recompile);

select distinct SessionId from [User].Session  -- 1.2M (adequate)
 where CreatedUtc > @minDate
option (optimize for unknown);

估算与实际

统计信息是最新的。

DBCC SHOW_STATISTICS('user.Session', 'IX_User_Session_CreatedUtc') with histogram;

显示直方图的最后几行(总共189行):

在此处输入图片说明

Answers:


6

与Aaron相比,答案不那么全面,但核心问题是DATEADD使用datetime2类型时的基数估计错误:

连接:sysdatetime出现在dateadd()表达式中时,估计不正确

一种解决方法是使用GETUTCDATE(返回日期时间):

WHERE CreatedUtc > CONVERT(datetime2(7), DATEADD(DAY, -365, GETUTCDATE()))

请注意,为避免该错误,到datetime2的转换必须在之外DATEADD

1行基数估计的问题在我使用的70个模型基数估计器(包括2016 RC0)的所有SQL Server版本中都复制了。

Aaron Bertrand在SQLPerformance.com上写了一篇有关此的文章:


6

在某些情况下,SQL Server可能会对DATEADD/ 进行真正的估算DATEDIFF,具体取决于参数是什么以及实际数据是什么样。我DATEDIFF在处理本月初时写的有关此内容的文章,以及一些解决方法,请参见此处:

但是,我的典型建议是仅在where / join子句中停止使用DATEADD/ DATEDIFF

以下方法虽然在a年处于过滤范围内(在这种情况下将包括额外的一天)不是很准确,并且在四舍五入到一天的同时会得到更好的估计(但仍然不是很好!),就像您DATEDIFF对列方法不满意,并且仍然允许使用查找:

DECLARE @start date = DATEFROMPARTS
(
  YEAR(GETUTCDATE())-1, 
  MONTH(GETUTCDATE()), 
  DAY(GETUTCDATE())
);

SELECT ... WHERE CreatedUtc >= @start;

您可以操纵输入来DATEFROMPARTS避免发生leap日问题,使用它DATETIMEFROMPARTS来获得更高的精度而不是四舍五入,等等。这只是演示您可以使用过去的日期填充变量而无需使用DATEADD(这只是一个少做一些工作),从而避免估算错误的更严重问题(已在2014+版中修复)。

为了避免发生leap日错误,您可以改为从去年2月28日而不是29日开始:

DECLARE @start date = DATEFROMPARTS
(
  YEAR(GETUTCDATE())-1, 
  MONTH(GETUTCDATE()), 
  CASE WHEN DAY(GETUTCDATE()) = 29 AND MONTH(GETUTCDATE()) = 2 
    THEN 28 ELSE DAY(GETUTCDATE()) END
);

您还可以通过检查我们是否过了今年的say日来添加一天,如果是,则在开始时添加一天(有趣的是,使用DATEADD 此处仍然可以进行准确的估算):

DECLARE @base date = GETUTCDATE();
IF GETUTCDATE() >= DATEFROMPARTS(YEAR(GETUTCDATE()),3,1) AND 
  TRY_CONVERT(datetime, DATEFROMPARTS(YEAR(GETUTCDATE()),2,29)) IS NOT NULL
BEGIN
  SET @base = DATEADD(DAY, 1, GETUTCDATE());
END

DECLARE @start date = DATEFROMPARTS
(
  YEAR(@base)-1, 
  MONTH(@base),
  CASE WHEN DAY(@base) = 29 AND MONTH(@base) = 2 
    THEN 28 ELSE DAY(@base) END
);

SELECT ... WHERE CreatedUtc >= @start;

如果您需要比午夜的时间更准确,则可以在选择之前添加更多操作:

DECLARE @accurate_start datetime2(7) = DATETIME2FROMPARTS
(
  YEAR(@start), MONTH(@start), DAY(@start),
  DATEPART(HOUR,  SYSUTCDATETIME()), 
  DATEPART(MINUTE,SYSUTCDATETIME()),
  DATEPART(SECOND,SYSUTCDATETIME()), 
  0,0
);

SELECT ... WHERE CreatedUtc >= @accurate_start;

现在,您可以在视图中添加所有这些内容,并且它仍将使用搜寻和30%的估计值,而无需任何提示或跟踪标志,但这并不漂亮。嵌套的CTE只是为了使我不必键入SYSUTCDATETIME()一百次或重复使用重复的表达式-仍然可以对其进行多次评估。

CREATE VIEW dbo.v5 
AS
  WITH d(d) AS ( SELECT SYSUTCDATETIME() ),
  base(d) AS
  (
    SELECT DATEADD(DAY,CASE WHEN d >= DATEFROMPARTS(YEAR(d),3,1) 
      AND TRY_CONVERT(datetime,RTRIM(YEAR(d))+RIGHT('0'+RTRIM(MONTH(d)),2)
      +RIGHT('0'+RTRIM(DAY(d)),2)) IS NOT NULL THEN 1 ELSE 0 END, d)
    FROM d
  ),
  src(d) AS
  (
    SELECT DATETIME2FROMPARTS
    (
      YEAR(d)-1, 
      MONTH(d),
      CASE WHEN MONTH(d) = 2 AND DAY(d) = 29
        THEN 28 ELSE DAY(d) END,
      DATEPART(HOUR,d), 
      DATEPART(MINUTE,d),
      DATEPART(SECOND,d),
      10*DATEPART(MICROSECOND,d),
      7
    ) FROM base
  )
  SELECT DISTINCT SessionId FROM [User].[Session]
    WHERE CreatedUtc >= (SELECT d FROM src);

这比在DATEDIFF本专栏文章中要冗长得多,但是正如我在评论中提到的那样,这种方法不是可靠的,并且尽管必须要阅读大多数表,但它可能具有竞争优势,但是我怀疑它将成为一种负担。因为“去年”在表格中所占的比例较低。

另外,仅供参考,以下是我尝试复制时获得的一些指标:

在此处输入图片说明

我无法获得1行的估算值,因此我竭尽全力匹配您的分布(313万行,去年为289万行)。但是您可以看到:

  • 我们的两个解决方案都执行大致等效的读取。
  • 您的解决方案的准确性稍差一些,因为它只考虑了日期范围(这可能很好,我的观点可能不太精确,无法匹配)。
  • 4199 +重新编译并没有真正改变估计(或计划)。

不要从持续时间数据中得出太多信息-它们现在已经很接近了,但是随着表格的增长可能不会保持紧密(再次,我相信是因为即使寻求也仍然必须阅读表格的大部分内容)。

以下是v4(与列的datediff)和v5(我的版本)的计划:

在此处输入图片说明

在此处输入图片说明


总结,如您的博客所述。该答案提供了可用的基于估计和寻求的计划。@PaulWhite的答案给出了最佳估计。可能我得到的1行估算值(与1500相比)可能是由于该表在过去约24小时内没有任何行。
crokusek '16

@crokusek如果您说>= DATEADD(DAY, -365, SYSDATETIME())的错误是估计是基于>= SYSDATETIME()。因此,从技术上讲,估算是基于表CreatedUtc中将来有多少行。可能为0,但是SQL Server总是将0向上舍入为1的估计行。
亚伦·伯特兰

1

用datediff()替换dateadd()以获得足够的近似值(30%)。

 select distinct SessionId from [User].Session     -- 1.2M est, 3.0M act.
  where datediff(day, CreatedUtc, sysutcdatetime()) <= 365

这似乎是类似于MS Connect 630583的错误。

选项重新编译没有区别。

计划统计


2
请注意,将datediff应用于该列会使表达式不可更改,因此您必须进行扫描。当仍然需要读取表的90%以上时,这可能是可以的,但是随着表的变大,这将变得更加昂贵。
亚伦·贝特朗

好点。我以为它可以在内部进行转换。验证它正在执行扫描。
crokusek '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.