ActivityTypeId
ごとのUserId
およびアクティビティが発生したActivityDate
をキャプチャする基本的な[UserActivity]
テーブルがあります。
@UserId
、@ForTypeId
、および@DurationInterval
と@DurationIncrement
の入力を可能にするクエリ/ストアドプロシージャを作成して、[〜#〜] n [〜#〜]秒/分/時間/日/月/年の数。 DATEADD/DATEDIFF
内のdatepart
引数はパラメーターを許可しないため、WHERE
句内で目的の結果を得るには、少し工夫する必要がありました。
最初はDATEDIFF
を使用してクエリを記述しましたが、実行プランを記述して調べた直後に、それがSARGable関数ではないことを思い出しました(精度レベルによって、一部の日付が下がる可能性があるという事実とともに)うるう年)。そのため、DATEPART
を使用するようにクエリを書き直し、インデックススキャンではなくインデックスシークをヒットして、通常はパフォーマンスを向上させると考えました。
残念ながら、クエリをDATEADD
として記述しても同じ結果が得られることがわかりました。インデックススキャンが実行されており、クエリオプティマイザーが[ActivityDate]
に対して非クラスター化インデックスを利用していません。
Aaron Bertrandのブログ投稿 "Performance Surprises and Assumptions: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
4分ごとに新しいActivityTypeId
を使用して、1〜10のランダムなActivityDate
で5人の異なるユーザーのダミーデータを再帰的にテーブルに入力します。
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
がSARG可能でない場合、インデックススキャンは期待どおりに実行されます。
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
クエリです。 PasteThePlan here。 残念ながら、インデックスシークは行われていません。これは私の側では間違った仮定かもしれませんが、なぜそれがまったく起こらないのかについて私は困惑しています。
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
を使用した結果として見られる動作は、インデックスを使用する可能性さえも否定していますか?私はここで骨の折れるほど明白な何かを見落としていますか?
UPDATE:上記の2番目の質問により、OR
操作の前にクエリを実行するようになりました。クエリがインデックスシークを実行したため、これらの比較中にSQL Serverが気に入らない何かが発生しています。 PasteThePlanはここにあります。
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:ここで共有されるソリューション
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))
これを確認できる環境にアクセスできませんが、動作するかどうかをお知らせください。
コンパイル時に、SQL Serverは@DurationInterval
の値を認識しないため、考えられるシナリオのデータを取得するのに最適な計画をコンパイルします。
クエリにWITH (FORCESEEK)
オプションを追加することにより、特定のクエリに対してインデックスシークを行うために、各OR
に対して個別のシークが行われることを証明できます。状態。
https://www.brentozar.com/pastetheplan/?id=HkE3lkuqf
スキャンは、6回のシークよりもデータを取得するためのより最適な方法であると判断されます。
@Daniel Hutmacherは、IX_UserActivity_ActivityDate
で単一のインデックスシークを実行する最適なソリューションを提供します。または、OPTION(RECOMPILE)
を追加することもできますが、これにより、クエリが実行されるたびに再コンパイルが強制的に行われ、潜在的に害になる可能性があります。
そのような「キッチンシンク」クエリ(入力の値に応じて1つ以上が使用される複数の個別のフィルタリング句)は、その個々の句がすべてであっても、検索可能になることはありません。
2つのクイックオプションは、それらを個別のプロシージャに分割し、マスタープロシージャの必要に応じてそれぞれを呼び出すか、アドホックSQLを使用することです。
このタイプのクエリ/手順の多くのオプションを説明する詳細な記事については、 http://www.sommarskog.se/dyn-search.html を参照してください。
将来の参考のために、これは Daniel Hutmatcherの提案された回答に基づいて私が見つけたソリューションです
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
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 @DurationInterval IN ('day', 'dd', 'd') THEN DATEADD(DAY, -@DurationIncrement, GETDATE())
WHEN @DurationInterval IN ('hour', 'hh') THEN DATEADD(HOUR, -@DurationIncrement, GETDATE())
WHEN @DurationInterval IN ('minute', 'mi', 'n') THEN DATEADD(MINUTE, -@DurationIncrement, GETDATE())
WHEN @DurationInterval IN ('second', 'ss', 's') THEN DATEADD(SECOND, -@DurationIncrement, GETDATE())
END))