Pic*_*llo 7 performance index sql-server execution-plan query-performance
我有一个基本[UserActivity]表,它捕获了一个ActivityTypeIdperUserId和ActivityDate发生 Activity 的时间。
我写一个查询/存储过程中允许的输入@UserId,@ForTypeId,还有@DurationInterval和@DurationIncrement基于动态返回的结果ň若干秒/分钟/小时/天/月/年。鉴于其中的datepart参数DATEADD/DATEDIFF不允许使用参数,为了在WHERE子句中获得所需的结果,我不得不重新使用一些技巧。
最初我使用 编写查询DATEDIFF,但在编写并查看执行计划后,我立即想起它不是 SARGable 函数(以及精度级别可以为闰年下降的某些日期提供的事实)。因此,我重新编写了查询以利用DATEPART我会命中索引查找而不是索引扫描并且通常性能更好的想法。
不幸的是,我发现将查询编写为DATEADD提供了相同的结果:正在执行索引扫描,并且查询优化器没有针对[ActivityDate].
我读阿龙贝特朗的博客文章,“业绩惊喜和假设: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
Run Code Online (Sandbox Code Playgroud)
使用ActivityTypeId1 到 10 之间的随机数为 5 个不同用户递归填充虚拟数据表,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;
Run Code Online (Sandbox Code Playgroud)
下面是我用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
Run Code Online (Sandbox Code Playgroud)
下面是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())))
)
Run Code Online (Sandbox Code Playgroud)
这是什么原因?我看到的行为是否是由于我使用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()))
Run Code Online (Sandbox Code Playgroud)
更新: 此处共享解决方案。
该OR条件计算在编译时,而不是在运行时,这意味着你的WHERE条件不生成寻道。
为了清理代码,我重构了你的代码,CONVERT使代码更具可读性。
我会尝试将WHERE条款更改为:
UA.[ActivityDate]>CONVERT(datetime2(0), (CASE
WHEN @DurationInterval IN ('year', 'yy', 'yyyy') THEN DATEADD(year, -@DurationIncrement, GETDATE())
WHEN @DurationInterval IN ('month', 'mm', 'm') THEN DATEADD(month, -@DurationIncrement, GETDATE())
WHEN ...
END))
Run Code Online (Sandbox Code Playgroud)
我无权访问可以验证这一点的环境,但请告诉我是否可行。
在编译时,SQL Server 不知道 的值,@DurationInterval因此编译最适合为任何可能方案检索数据的计划。
您可以通过向WITH (FORCESEEK)查询添加一个选项来证明这一点,该选项显示,为了对给定查询执行索引查找,每个OR条件都将有一个单独的查找。
https://www.brentozar.com/pastetheplan/?id=HkE3lkuqf
与 6 次搜索相比,扫描被确定为检索数据的最佳方式。
@Daniel Hutmacher 提供了一个最佳解决方案,可以对IX_UserActivity_ActivityDate. 或者,您可以添加OPTION(RECOMPILE),尽管这会在每次运行查询时强制重新编译,可能弊大于利。
像这样的“厨房水槽”查询(多个不同的过滤子句,其中一个或多个根据输入的值使用)永远不会是 sargable,即使它的所有单个子句都是。
两个快速选项是将它们分解为单独的过程,并根据主过程的需要调用每个过程或使用临时 SQL。
有关描述此类查询/过程的许多选项的详细文章,请参阅http://www.sommarskog.se/dyn-search.html
| 归档时间: |
|
| 查看次数: |
757 次 |
| 最近记录: |