【问题标题】:Should Getdate() in WITH clause be declared as variable?WITH 子句中的 Getdate() 是否应该声明为变量?
【发布时间】:2018-08-03 12:37:23
【问题描述】:

我了解通常将当前日期声明为变量是有用的:

DECLARE @CurrentDate as DateTime
SET @CurrentDate = Getdate()

我的理解是,这会对当前日期/时间进行一次采样并将其记录为静态变量。这有助于确保“当前时间”在查询期间保持不变,并且还可以避免重复使用比获取变量值更昂贵的 Getdate()。

我的问题是....在递归 WITH 子句的情况下是否需要这样做,该子句似乎是一次获取日期?

例如,考虑以下代码:

WITH CalendarSequence as(
  SELECT Getdate() as RollingDate
  UNION ALL
  SELECT DateAdd(month, -1, RollingDate) as RollingDate
  FROM CalendarSequence
  WHERE DateAdd(month, -1, RollingDate) > Convert(date, '2016-01-01')
)
SELECT 
Year(CalendarSequence.RollingDate)*100+Month(CalendarSequence.RollingDate) as MissingYearMonth
FROM CalendarSequence
LEFT OUTER JOIN TableName
ON Year(CalendarSequence.RollingDate)*100+Month(CalendarSequence.RollingDate) = TableName.YearMonthField
WHERE TableName.YearMonthField is NULL

此查询生成一个临时日期表,并将其与任意表进行比较,以突出显示没有活动/数据的各个月份。

就这段特定的代码而言,Getdate() 是否被多次使用并不重要(除非在查询执行期间月份发生了变化!)但对于类似的查询而言,这并非普遍适用。我提出这个问题的部分动机是为了更好地了解 WITH 函数正在做什么以及它是否有任何令人惊讶的行为。

【问题讨论】:

  • 不,但您可能应该使用日历表。一个完整的索引表,例如 50 年,其中包含年、月、日、ISO 和非 ISO 周数、标签、各种日期表示等字段,而不是每次都重建它不会占用很多空间。加入它会比使用生成的日期范围快很多
  • 这是一个答案,但是,我认为它不能完全解决您的问题,所以我在评论。通常,您不需要将GETDATE 分配给变量。如果这样做,很可能在整个批次中对GETDATE 使用相同的值。例如,如果您在一个批次中有多个语句,则这 2 个语句可能具有不同的 GETDATE 值。如果它们需要相同,则首先将值分配给变量,然后引用该变量。如果他们不需要(或者没关系),那么你可以简单地使用GETDATE
  • 在查询中,GETDATE() 对于所有行始终只有一个值,因此无论您的 WITH 处理多少行,或者在处理过程中时间是否发生变化,都无关紧要。 (对于一个更有说服力的例子,试试SELECT SYSUTCDATETIME() FROM hugetable。)这通常适用于 T-SQL 函数,即使是像 RAND() 这样的非确定性函数,但值得注意的例外是 NEWSEQUENTIALID()NEWID(),它们确实适用于-row 值(以及明显设计为每行更改的函数,如ROW_NUMBER()NEXT VALUE FOR)。说了这么多……似乎没有记录。
  • 虽然 GetDate() 由于优化而出现固定,并且正如有人说 newid() 不固定,我确实在这里看到一个查询 newid() 出现固定 - 所以如果你想保证 getdate( ) 是固定的,使用变量,它肯定只能“一样好或更好”,永远不会更糟
  • 啊,这里有一些more 在这个问题上应该真正说服你使用一个变量:多个独立的GETDATE()s 保证在其中具有相同的值相同的查询,优化器不会统一这些值。我很确定您仍然无法仅使用单个GETDATE() 来显示差异,因为我想不出一种方法可以使优化器生成一个计划,在该计划中它的评估就像它在不同的列中一样,但依赖它似乎很不明智。

标签: sql-server tsql


【解决方案1】:

我在 SQL Server 2016 中运行了这两种方法。我没有发现任何区别。但是,正如 @Cato 在 cmets 中提到的,最好声明为变量并传递变量,而不是等待优化器处理它。

方法一

DECLARE @currentDate DATE = GETDATE()
;WITH CalendarSequence as(
  SELECT @currentDate as RollingDate
  UNION ALL
  SELECT DateAdd(month, -1, RollingDate) as RollingDate
  FROM CalendarSequence
  WHERE DateAdd(month, -1, RollingDate) > Convert(date, '2018-01-01')
)
SELECT * FROM CalendarSequence

方法2

;WITH CalendarSequence as(
  SELECT Getdate() as RollingDate
  UNION ALL
  SELECT DateAdd(month, -1, RollingDate) as RollingDate
  FROM CalendarSequence
  WHERE DateAdd(month, -1, RollingDate) > Convert(date, '2018-01-01')
)
SELECT * FROM CalendarSequence

它们都有相同的执行计划。完全没有变化。

【讨论】:

    【解决方案2】:

    执行摘要:如果您想要一个日期/时间的值,请将其捕获在一个变量中并根据需要使用它。它使您的意图清晰,并避免软件更新和其他细微之处可能出现的问题。

    以下代码使用 SQL Server 2008 R2 进行测试,证明并非所有 select 语句都相同。虽然GetDate() 似乎是每个实例(也可能是所有实例)的runtime constant,但问题比人们想象的要微妙。

    GetDate() 可以在所有列和行中保持不变。

    -- Constant across all columns and rows.
    with Murphy as (
      select GetDate() as A, GetDate() as B, 1 as Rows
      union all
      select GetDate(), GetDate(), Rows + 1
        from Murphy
        where A = B and Rows < 1000000 )
      select Min( A ) as MinA, Max( A ) as MaxA, Min( B ) as MinB, Max( B ) as MaxB
        from Murphy
        option ( MaxRecursion 0 );
    

    更高效的版本使用cross join 而不是递归。

    -- Constant across all columns and rows.
    declare @Limit as Int = 100000;
    with Ten ( Number ) as
      ( select * from ( values (0), (1), (2), (3), (4), (5), (6), (7), (8), (9) ) as Digits( Number ) ),
      TenUp2 ( Number ) as ( select 42 from Ten as L cross join Ten as R ),
      TenUp4 ( Number ) as ( select 42 from TenUp2 as L cross join TenUp2 as R ),
      TenUp8 ( Number ) as ( select 42 from TenUp4 as L cross join TenUp4 as R ),
      Numbers ( Number, A, B ) as ( select top (@Limit) Row_Number() over ( order by ( select NULL ) ),
        GetDate(), GetDate() from TenUp8 )
      select Min( A ) as MinA, Max( A ) as MaxA, Min( B ) as MinB, Max( B ) as MaxB
        from Numbers;
    

    再一次,也许不同的实例会分开。

    -- Fails (and does not generate an execution plan).
    declare @A as DateTime = GetDate();
    declare @B as DateTime = @A;
    declare @Trials as Int = 0;
    
    while @A = @B
      begin
      select @A = GetDate(), @B = GetDate(), @Trials += 1;
      if @Trials % 1000 = 0
        print @Trials;
      end
    
    select @A as A, @B as B, @Trials as Trials;
    

    所以你认为这只是一个冒充selectset 和一个真正的 select,生成执行计划的工作方式会有所不同。

    -- Fails.
    declare @A as DateTime = GetDate();
    declare @B as DateTime = @A;
    declare @Trials as Int = 0;
    
    while @A = @B
      begin
      select @A = GetDate(), @B = GetDate(), @Trials += 1
        from ( values ( 42 ) ) as PH( A );
      if @Trials % 1000 = 0
        print @Trials;
      end
    
    select @A as A, @B as B, @Trials as Trials;
    

    如果值来自表值构造函数怎么办?

    -- Fails.
    declare @A as DateTime = GetDate();
    declare @B as DateTime = @A;
    declare @Trials as Int = 0;
    
    select @A = GetDate(), @B = @A;
    while @A = @B
      begin
      select @A = A, @B = B, @Trials += 1
        from ( values ( GetDate(), GetDate() ) ) as PH( A, B )
      if @Trials % 1000 = 0
        print @Trials;
      end
    
    select @A as A, @B as B, @Trials as Trials;
    

    嗯,所有的失败都来自为变量赋值的select 语句。让我们通过向表中插入行并稍后检查它们来消除这种情况。 (注意:此示例在带有 SQL Server 2017 的 SQL Fiddle 上运行时不会失败。)

    -- Fails on SQL Server 2008 R2, but not on SQL Server 2017.
    declare @Samples as Table ( A DateTime, B DateTime );
    declare @Trials as Int = 0;
    while @Trials < 100000
      begin
      insert into @Samples ( A, B ) values ( GetDate(), GetDate() )
      set @Trials += 1;
      end
    
    select A, B
      from @Samples
      where A != B;
    select Min( A ) as MinA, Max( A ) as MaxA, Min( B ) as MinB, Max( B ) as MaxB
      from @Samples;
    

    【讨论】:

    • 您的最后一个示例在我的 SQL Server 2017 实例(RTM CU7)上也失败了,所以如果它不在 SQL Fiddle 上,那只不过是一种好奇心。长话短说:所有当前版本的 SQL Server 都将单个 GETDATE() 表达式视为查询期间的常量,但是 1)这没有正式记录(尽管公认极不可能在新版本中更改)和 2)它们为每个表达式重新单独常量,因此很容易得到惊喜。当然,这些意外是否重要取决于查询。
    • 值得注意的是,您的第一个查询(本应是安全的)并非如此。执行计划有两个GETDATE() 评估,它们可能不同。由于涉及的行数,让它失败只是乏味的。将其重写为循环中的检查并将从1000000 生成的行减少到10 使其在几秒钟内失败。每个单独的 query 失败的机会很低(因为每个查询只评估一次表达式),但执行足够多次,只要有多个 GETDATE() 在中,它们就可以全部失败在那里。
    • 鉴于GETDATE()几乎不变,你不得不怀疑MS是否愿意在未来的版本中一路走下去并使所有GETDATE() 评估解析为在查询开始时确定的单个值,而不是像目前看来的那样每个GETDATE() 表达式执行一次。这应该几乎不会影响现有的查询,除了让它们在几千次运行中不会每次都有不同的行为,我认为这只是一个净加分。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-09-06
    • 1970-01-01
    • 1970-01-01
    • 2012-02-17
    • 2020-02-12
    相关资源
    最近更新 更多