应该将WITH子句中的Getdate()声明为变量吗?

时间:2018-08-03 12:37:23

标签: sql-server tsql

我知道通常将当前日期声明为变量很有用:

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函数的作用以及它是否具有令人惊讶的行为。

2 个答案:

答案 0 :(得分:3)

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

下面的代码经过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;

因此,您认为只是set冒充为select,而真实 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;

答案 1 :(得分:2)

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

方法1

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

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

SQL SERVER Execution Plan