Ich habe eine [UserActivity]
Basistabelle, die ein ActivityTypeId
Per UserId
und das, ActivityDate
an dem die Aktivität stattgefunden hat , erfasst .
Ich schreibe eine Abfrage / gespeicherte Prozedur, die die Eingabe der @UserId
, @ForTypeId
sowie der @DurationInterval
und @DurationIncrement
die dynamische Rückgabe von Ergebnissen basierend auf N Anzahl von Sekunden / Minuten / Stunden / Tagen / Monaten / Jahren ermöglicht. Da das datepart
Argument innerhalb DATEADD/DATEDIFF
keine Parameter zulässt, musste ich auf einige Tricks zurückgreifen, um die gewünschten Ergebnisse innerhalb der WHERE
Klausel zu erzielen.
Anfangs schrieb ich die Abfrage mit DATEDIFF
, aber unmittelbar nachdem ich den Ausführungsplan geschrieben und einen Blick darauf geworfen hatte, fiel mir ein, dass es sich nicht um eine SARGable-Funktion handelt (zusammen mit der Tatsache, dass die Genauigkeitsstufen für einige Daten in einem Schaltjahr angeboten werden könnten). Daher habe ich die Abfrage neu geschrieben, um den DATEPART
Gedanken zu nutzen , dass ich anstelle eines Index-Scans eine Indexsuche durchführen und im Allgemeinen eine bessere Leistung erzielen würde.
Leider habe ich festgestellt, dass das Schreiben der Abfrage als DATEADD
die gleichen Ergebnisse liefert: Es wird ein Index-Scan durchgeführt, und das Abfrageoptimierungsprogramm nutzt den nicht gruppierten Index nicht für [ActivityDate]
.
Ich las Aaron Bertrands Blog-Post "Performance Surprises and Assumptions: DATEADD" und implementierte die Änderungen, die er an CONVERT
dem DATEADD
Teil beschrieben hatte, in die entsprechende Spaltendefinition , datetime2
da seltsame Tricks damit verbunden waren datetime2
. Das Problem war jedoch auch danach noch vorhanden.
Zur besseren Veranschaulichung des Szenarios finden Sie hier eine vergleichbare Tabellendefinition.
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
Füllen Sie die Tabelle rekursiv mit Dummy-Daten für 5 verschiedene Benutzer mit einem Zufall ActivityTypeId
zwischen 1 und 10 mit einem neuen ActivityDate
alle 4 Minuten.
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;
Unten ist die erste Abfrage, mit der ich geschrieben habe DATEDIFF
. Hinweis: Ich schließe die @UserId
und @ForTypeId
Prädikate absichtlich aus, um diese Schlüsselsuche zu vermeiden und das Rauschen in den beigefügten Plänen zu reduzieren.
Wie Sie in PasteThePlan für diese Abfrage finden , führt es erwartungsgemäß einen DATEDIFF
Indexscan durch, der nicht SARGable ist.
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
Unten ist die DATEADD
Abfrage. PasteThePlan hier. Leider findet keine Indexsuche statt. Dies mag meinerseits eine falsche Annahme sein, aber ich bin ratlos darüber, warum sie überhaupt nicht auftritt.
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())))
)
Was ist die Ursache dafür? Ist das Verhalten, das ich sehe, darauf zurückzuführen, dass ich das OR
Potenzial negiert habe, überhaupt den Index zu verwenden? Übersehe ich hier etwas akribisch Offensichtliches?
UPDATE: Meine zweite Frage oben veranlasste mich, eine Abfrage vor den OR
Operationen durchzuführen. Die Abfrage hat die Indexsuche durchgeführt, sodass bei diesen Vergleichen etwas auftritt, das SQL Server nicht gefällt. PasteThePlan hier.
DECLARE @DurationIncrement int = 1
SELECT
COUNT(UA.[UserActivityId]) AS 'ActivityTypeCount'
FROM
[dbo].[UserActivity] UA
WHERE
UA.[ActivityDate] > CONVERT(datetime2(0), DATEADD(HOUR, -@DurationIncrement, GETDATE()))
UPDATE: Lösung hier geteilt.
WHERE
Klausel als solche wird der nicht gruppierte Index entsprechend getroffen. Ich habe mein OP mit der richtigen Abfrage aktualisiert. Danke mein Herr.