我有一个基本[UserActivity]
表,用于捕获活动发生的ActivityTypeId
时间UserId
和地点。ActivityDate
我正在编写一个查询/存储过程,允许输入@UserId
,@ForTypeId
以及@DurationInterval
和 并@DurationIncrement
根据N秒/分钟/小时/天/月/年的数量动态返回结果。鉴于datepart
内部DATEADD/DATEDIFF
参数不允许参数,我不得不恢复一些技巧才能在WHERE
子句中获得所需的结果。
最初我使用 编写查询DATEDIFF
,但在编写并查看执行计划后,我立即记起它不是 SARGable 函数(以及精度级别可以提供闰年下降的某些日期这一事实)。因此,我重新编写了查询,以利用DATEPART
我会命中索引查找而不是索引扫描的想法,并且通常表现得更好。
不幸的是,我发现编写查询DATEADD
提供了相同的结果:正在进行索引扫描,并且查询优化器没有利用针对[ActivityDate]
.
我阅读了Aaron Bertrand 的博客文章“性能惊喜和假设:DATEADD”,并CONVERT
由于. 但是,即使这样做了,问题仍然存在。DATEADD
datetime2
datetime2
为了更好地说明该场景,这里有一个可比较的表定义。
DROP TABLE IF EXISTS [dbo].[UserActivity]
IF OBJECT_ID('[dbo].[UserActivity]', 'U') IS NULL
BEGIN
CREATE TABLE [dbo].[UserActivity] (
[UserId] [int] NOT NULL
,[UserActivityId] [bigint] IDENTITY(1,1) NOT NULL
,[ActivityTypeId] [tinyint] NOT NULL
,[ActivityDate] [datetime2](0) NOT NULL CONSTRAINT [DF_UserActivity_ActivityDate] DEFAULT GETDATE()
,CONSTRAINT [PK_UserActivity] PRIMARY KEY CLUSTERED ([UserActivityId] ASC)
,INDEX [IX_UserActivity_UserId] NONCLUSTERED ([UserId] ASC)
,INDEX [IX_UserActivity_ActivityTypeId] NONCLUSTERED ([ActivityTypeId] ASC)
,INDEX [IX_UserActivity_ActivityDate] NONCLUSTERED ([ActivityDate] ASC)
)
END;
GO
为 5 个不同的用户递归地用虚拟数据填充表,随机数ActivityTypeId
在 1 到 10 之间,ActivityDate
每 4 分钟一个新的。
DECLARE @UserId int = (SELECT ISNULL((SELECT TOP (1) [UserId] + 1 FROM [dbo].[UserActivity] ORDER BY [UserId] DESC), 1))
;WITH [UserActivitySeed] AS (
SELECT
CONVERT(datetime2(0), '01/01/2018') AS 'ActivityDate'
UNION ALL
SELECT
DATEADD(minute, 4, [ActivityDate])
FROM
[UserActivitySeed]
WHERE
[ActivityDate] < '2018-04-01')
INSERT INTO [dbo].[UserActivity] ([UserId], [ActivityTypeId], [ActivityDate])
SELECT
@UserId
,ABS(CHECKSUM(NEWID()) % 9) + 1
,[ActivityDate]
FROM
[UserActivitySeed] OPTION (MAXRECURSION 32767);
GO 5
ALTER INDEX ALL ON [dbo].[UserActivity] REBUILD;
下面是我用 编写的第一个查询DATEDIFF
。请注意,我有意排除了@UserId
和@ForTypeId
谓词,以避免那些关键查找并减少附加计划中的噪音。
正如您将在PasteThePlan 上针对此查询发现的那样,它正在按预期执行索引扫描,因为它DATEDIFF
不是 SARGable。
DECLARE @UserId int = 1
DECLARE @ForTypeId int = 3
DECLARE @DurationInterval varchar(6) = 'hour'
DECLARE @DurationIncrement int = 1
SELECT
COUNT(UA.[UserActivityId]) AS 'ActivityTypeCount'
FROM
[dbo].[UserActivity] UA
WHERE
-- Exclude the @UserId and @ForTypeId predicates.
-- UA.[UserId] = @UserId
-- AND UA.[ActivityTypeId] = @ForTypeId
-- AND
CASE
WHEN @DurationInterval IN ('year', 'yy', 'yyyy') THEN DATEDIFF(SECOND, UA.[ActivityDate], GETDATE()) / 3600.0 / 24.0 / 365.25
WHEN @DurationInterval IN ('month', 'mm', 'm') THEN DATEDIFF(SECOND, UA.[ActivityDate], GETDATE()) / 3600.0 / 24.0 / 365.25 * 12
WHEN @DurationInterval IN ('day', 'dd', 'd') THEN DATEDIFF(SECOND, UA.[ActivityDate], GETDATE()) / 3600.0 / 24.0
WHEN @DurationInterval IN ('hour', 'hh') THEN DATEDIFF(SECOND, UA.[ActivityDate], GETDATE()) / 3600.0
WHEN @DurationInterval IN ('minute', 'mi', 'n') THEN DATEDIFF(SECOND, UA.[ActivityDate], GETDATE()) / 60.0
WHEN @DurationInterval IN ('second', 'ss', 's') THEN DATEDIFF(SECOND, UA.[ActivityDate], GETDATE())
END < @DurationIncrement
下面是DATEADD
查询。在此处粘贴计划。不幸的是,没有发生索引查找。这对我来说可能是一个错误的假设,但我很困惑为什么它根本没有发生。
DECLARE @UserId int = 1
DECLARE @ForTypeId int = 3
DECLARE @DurationInterval varchar(6) = 'hour'
DECLARE @DurationIncrement int = 1
SELECT
COUNT(UA.[UserActivityId]) AS 'ActivityTypeCount'
FROM
[dbo].[UserActivity] UA
WHERE
-- Exclude the @UserId and @ForTypeId predicates.
-- UA.[UserId] = @UserId
-- AND UA.[ActivityTypeId] = @ForTypeId
-- AND
(
(@DurationInterval IN ('year', 'yy', 'yyyy') AND UA.[ActivityDate] > CONVERT(datetime2(0), DATEADD(YEAR, -@DurationIncrement, GETDATE())))
OR
(@DurationInterval IN ('month', 'mm', 'm') AND UA.[ActivityDate] > CONVERT(datetime2(0), DATEADD(MONTH, -@DurationIncrement, GETDATE())))
OR
(@DurationInterval IN ('day', 'dd', 'd') AND UA.[ActivityDate] > CONVERT(datetime2(0), DATEADD(DAY, -@DurationIncrement, GETDATE())))
OR
(@DurationInterval IN ('hour', 'hh') AND UA.[ActivityDate] > CONVERT(datetime2(0), DATEADD(HOUR, -@DurationIncrement, GETDATE())))
OR
(@DurationInterval IN ('minute', 'mi', 'n') AND UA.[ActivityDate] > CONVERT(datetime2(0), DATEADD(MINUTE, -@DurationIncrement, GETDATE())))
OR
(@DurationInterval IN ('second', 'ss', 's') AND UA.[ActivityDate] > CONVERT(datetime2(0), DATEADD(SECOND, -@DurationIncrement, GETDATE())))
)
这是什么原因?我看到的行为是否是我使用OR
否定它甚至可以使用索引的任何可能性的结果?我是否忽略了这里非常明显的东西?
更新:我上面的第二个问题导致我在OR
操作之前执行查询。查询执行了索引查找,因此在这些比较过程中发生了 SQL Server 不喜欢的事情。在此处粘贴计划。
DECLARE @DurationIncrement int = 1
SELECT
COUNT(UA.[UserActivityId]) AS 'ActivityTypeCount'
FROM
[dbo].[UserActivity] UA
WHERE
UA.[ActivityDate] > CONVERT(datetime2(0), DATEADD(HOUR, -@DurationIncrement, GETDATE()))
更新: 解决方案在这里共享。
条件在编译时计算
OR
,而不是在运行时计算,这意味着您的WHERE
条件不会生成查找。为了清理代码,我重构了你的代码,
CONVERT
使代码更具可读性。我会尝试将
WHERE
条款更改为:我无法访问可以验证这一点的环境,但如果可行,请告诉我。
在编译时,SQL Server 不知道 的值,
@DurationInterval
因此会编译最适合检索任何可能情况的数据的计划。您可以通过向查询添加一个选项来证明这一点,该
WITH (FORCESEEK)
选项表明,为了对给定查询执行索引查找,每个OR
条件都会有一个单独的查找。https://www.brentozar.com/pastetheplan/?id=HkE3lkuqf
扫描被确定为比 6 次查找更优化的数据检索方式。
@Daniel Hutmacher 提供了一个最佳解决方案,可以在
IX_UserActivity_ActivityDate
. 或者,您可以添加一个OPTION(RECOMPILE)
,尽管这会在每次运行查询时强制重新编译,可能弊大于利。像这样的“厨房水槽”查询(多个不同的过滤子句,根据输入的值使用其中一个或多个)永远不会是可搜索的,即使它的所有单独的子句都是可搜索的。
两个快速选项是将它们分解为单独的过程,并根据需要由主过程调用每个过程或使用临时 SQL。
有关描述此类查询/过程的多个选项的详细文章,请参阅http://www.sommarskog.se/dyn-search.html
为了将来参考,这是我根据Daniel Hutmatcher 提出的答案得出的解决方案。