从具有聚集索引的列中过滤计算为 DATEADD 的列时进行索引扫描

Index scan when filtering on column calculated as DATEADD from column with clustered index

我很困惑为什么我在使用简单 DATEADD 函数(应该是确定性的)的计算列上的查询上进行聚集索引搜索,而不是其他聚集索引的列。

我能否保留非具体化的计算列(因为这是针对现有的大量遗留数据)并能够按此列进行筛选并让 sql 命中索引?我需要在计算列上进行过滤(因为我无法更改查询并且它们需要使用适当的偏移量进行查询)。

简单的示例定义:

-- table with calculated column
CREATE TABLE [dbo].[_test](
    [Dt] [datetime] NOT NULL
) ON [PRIMARY]
GO

--clustered index
CREATE CLUSTERED INDEX [Idx] ON [dbo].[_test]
(
    [Dt] ASC
) ON [PRIMARY]
GO



--simulation of legacy data
DECLARE @rdate DATE
DECLARE @startLoopID INT = 1
DECLARE @endLoopID INT = 1000

WHILE @startLoopID <= @endLoopID
BEGIN
    SET @rdate = DATEADD(Hour, ABS(CHECKSUM(NEWID()) % (365 * 24) ), '2020-01-01');
    SET @startLoopID = @startLoopID + 1;

    INSERT INTO [_test] (Dt)
    VALUES (@rdate);
END

--adding the calculated column with proper offset
alter table _test ADD [Dt_2] AS  DATEADD(MINUTE, 300, Dt) 

执行示例: 声明 @EndTime DATETIME2(7) = '2020-07-10 00:00:00.000'

DECLARE @StartTime DATETIME2(7) = DATEADD(day, -12, @EndTime)

--select is performing seek
select * from _test
where 
 Dt > @StartTime AND Dt < @EndTime
 
--select is performing scan
select * from _test
where 
 Dt_2 > @StartTime AND Dt_2 < @EndTime

查询计划:

Dt 列上过滤时,我得到了预期的搜索。在 Dt_2 列上过滤时 - 通过确定性函数从 Dt 计算 - 我得到索引扫描。 在具有大量数据的真实场景中,这会导致巨大的性能损失。

您需要为计算列编制索引,以便 SQL 能够执行查找。计算列仅在 select 时评估,除非它被持久化或索引。即使标记为persisted,也仍然需要在没有索引的情况下进行扫描。表达式具有确定性和精确性这一事实意味着它 可以 被索引,但您仍然需要添加索引。

您在下面的评论表明您无法向计算列添加索引。但是在这种特殊情况下,您实际上并不需要一个来执行相同的查询,因为您的计算列正在向原始列添加一个常量偏移量。因此,可以将常量表达式移到查询本身比较操作的右侧,SQL仍然可以使用原来的聚簇索引。

也就是说,不是创建一个等于 Dt 加上某个常数的新列,而是可以从不等式的 RHS 中减去该常数。而不是:

alter table _test ADD [Dt_2] AS  DATEADD(MINUTE, 300, Dt);

DECLARE @EndTime DATETIME2(7) = '2020-07-10 00:00:00.000'
DECLARE @StartTime DATETIME2(7) = DATEADD(day, -12, @EndTime);

select * from _test
where 
 Dt_2 > @StartTime AND Dt_2 < @EndTime;

您可以使用:

--subtract 300 minutes from the @endTime parameter instead of adding 300 minutes to every value of Dt

DECLARE @EndTime DATETIME2(7) = dateadd(minute, -300, '2020-07-10 00:00:00.000');
DECLARE @StartTime DATETIME2(7) = DATEADD(day, -12, @EndTime);

select * from _test
where 
 Dt > @StartTime
AND Dt < @EndTime;