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.
quelle
WHERE
Klausel als solche wird der nicht gruppierte Index entsprechend getroffen. Ich habe mein OP mit der richtigen Abfrage aktualisiert. Danke mein Herr.Bei der Kompilierung kennt SQL Server den Wert von nicht
@DurationInterval
und kompiliert daher den Plan, der am besten zum Abrufen der Daten für ein mögliches Szenario geeignet ist.Sie können dies beweisen, indem Sie
WITH (FORCESEEK)
der Abfrage eine Option hinzufügen , aus der hervorgeht, dass für jedeOR
Bedingung eine individuelle Suche durchgeführt wird, um eine Indexsuche für die angegebene Abfrage durchzuführen .https://www.brentozar.com/pastetheplan/?id=HkE3lkuqf
Es wird festgestellt, dass der Scan eine optimalere Methode zum Abrufen der Daten darstellt als 6 Suchvorgänge.
@ Daniel Hutmacher bietet eine optimale Lösung, die eine einzelne Indexsuche durchführt
IX_UserActivity_ActivityDate
. Alternativ können Sie eine hinzufügenOPTION(RECOMPILE)
, obwohl dies jedes Mal, wenn die Abfrage ausgeführt wird, eine Neukompilierung erzwingen würde, was möglicherweise mehr Schaden als Nutzen verursachen könnte.quelle
Eine solche "Küchenspüle" -Abfrage (mehrere unterschiedliche Filterklauseln, von denen eine oder mehrere abhängig vom Wert einer Eingabe verwendet werden) wird niemals sarkierbar sein, selbst wenn alle ihre einzelnen Klauseln vorhanden sind.
Die beiden schnellen Optionen bestehen darin, sie in einzelne Prozeduren zu unterteilen und jede nach Bedarf von einer Master-Prozedur aufzurufen oder Ad-hoc-SQL zu verwenden.
Einen ausführlichen Artikel, der eine Reihe von Optionen für diese Art von Abfrage / Prozedur beschreibt, finden Sie unter http://www.sommarskog.se/dyn-search.html
quelle
Zum späteren Nachschlagen ist dies die Lösung, zu der ich auf der Grundlage der von Daniel Hutmatcher vorgeschlagenen Antwort gekommen bin.
quelle