DATEADD không tạo ra kỳ vọng SARGable về tìm kiếm chỉ mục


7

Tôi có một [UserActivity]bảng cơ bản để ghi lại ActivityTypeIdmỗi UserIdActivityDatetại đó Hoạt động xảy ra.

Tôi viết một truy vấn / thủ tục lưu trữ cho phép đầu vào của @UserId, @ForTypeIdcũng như các @DurationInterval@DurationIncrementkết quả trả lại tự động dựa trên N số giây / phút / giờ / ngày / tháng / năm. Cho rằng datepartđối số bên trong DATEADD/DATEDIFFkhông cho phép tham số, tôi đã phải hoàn nguyên một chút mánh khóe để có được kết quả mong muốn trong WHEREmệnh đề.

Ban đầu tôi đã viết truy vấn bằng cách sử dụng DATEDIFF, nhưng ngay sau khi viết và xem qua kế hoạch thực hiện, tôi nhớ rằng đó không phải là chức năng SARGable (cùng với thực tế là các mức chính xác có thể cung cấp cho một số ngày rơi vào Năm nhuận). Vì vậy, tôi đã viết lại truy vấn để sử dụng DATEPARTsuy nghĩ rằng tôi sẽ đạt được một tìm kiếm chỉ mục thay vì quét chỉ mục và thường hoạt động tốt hơn.

Thật không may, tôi đã phát hiện ra rằng việc viết truy vấn sẽ DATEADDmang lại kết quả tương tự: quá trình quét chỉ mục đang diễn ra và trình tối ưu hóa truy vấn không tận dụng chỉ mục không được nhóm [ActivityDate].

Tôi đọc bài viết trên blog Aaron Bertrand, "Hiệu suất Surprises và Giả định: DATEADD" , và thực hiện những thay đổi ông mô tả để CONVERTcác DATEADDphần vào tương đương với datetime2định nghĩa cột do lừa đảo kỳ lạ liên quan đến datetime2. Tuy nhiên, vấn đề vẫn còn hiện diện ngay cả sau khi làm như vậy.

Để minh họa rõ hơn cho kịch bản, đây là một định nghĩa bảng so sánh.

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

Nhập bảng với dữ liệu giả theo cách đệ quy cho 5 người dùng khác nhau với ngẫu nhiên ActivityTypeIdtừ 1 đến 10 với mới ActivityDate4 phút một lần.

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;

Dưới đây là truy vấn đầu tiên tôi viết với DATEDIFF. Lưu ý Tôi đang loại trừ các biến vị ngữ @UserIdvà các biến @ForTypeIdvị ngữ một cách có chủ ý để tránh các tra cứu chính đó và giảm nhiễu trong các kế hoạch được đính kèm.

Như bạn sẽ tìm thấy trên PasteThePlan cho truy vấn này , nó đang thực hiện quét chỉ mục như mong đợi mà DATEDIFFkhông phải là 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

Dưới đây là DATEADDtruy vấn. DánThePlan ở đây. Thật không may, một tìm kiếm chỉ mục không xảy ra. Đây có thể là một giả định không chính xác về phía tôi, nhưng tôi bối rối về lý do tại sao nó không xảy ra.

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())))
        )

Nguyên nhân của điều này là gì? Có phải hành vi tôi đang thấy là kết quả của việc tôi sử dụng để ORphủ nhận bất kỳ tiềm năng nào để nó thậm chí có thể sử dụng chỉ mục không? Tôi đang nhìn một cái gì đó rõ ràng ở đây?

CẬP NHẬT: Câu hỏi thứ hai của tôi ở trên dẫn tôi thực hiện một truy vấn nêu trên các ORhoạt động. Truy vấn đã thực hiện tìm kiếm chỉ mục, do đó, một cái gì đó đang xảy ra trong những so sánh này mà SQL Server không thích. DánThePlan ở đây.

DECLARE @DurationIncrement int = 1

SELECT
    COUNT(UA.[UserActivityId]) AS 'ActivityTypeCount'
FROM
    [dbo].[UserActivity] UA
WHERE
    UA.[ActivityDate] > CONVERT(datetime2(0), DATEADD(HOUR, -@DurationIncrement, GETDATE()))

CẬP NHẬT: Giải pháp được chia sẻ ở đây.

Câu trả lời:


9

Điều ORkiện đánh giá tại thời gian biên dịch, thay vì tại thời gian chạy, điều đó có nghĩa là WHEREđiều kiện của bạn không tạo ra một tìm kiếm.

Và chỉ để dọn sạch mã, tôi đã cấu trúc lại mã của bạn CONVERTđể làm cho mã dễ đọc hơn một chút.

Tôi sẽ thử thay đổi WHEREmệnh đề thành:

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))

Tôi không có quyền truy cập vào một môi trường nơi tôi có thể xác minh điều này, nhưng vui lòng cho tôi biết nếu nó hoạt động.


Đó là giải pháp! Bằng cách hoán đổi WHEREmệnh đề xung quanh như vậy, nó đánh vào chỉ mục không được phân cụm một cách thích hợp. Tôi đã cập nhật OP của mình với truy vấn chính xác. Cảm ơn ngài.
PicoDeGallo

7

Khi biên dịch, SQL Server không biết giá trị của @DurationIntervalvà do đó biên dịch gói phù hợp nhất để truy xuất dữ liệu cho bất kỳ kịch bản có thể nào.

Bạn có thể chứng minh điều đó bằng cách thêm một WITH (FORCESEEK)tùy chọn vào truy vấn, trong đó cho thấy rằng, để thực hiện Tìm kiếm chỉ mục cho truy vấn đã cho, sẽ có một tìm kiếm riêng cho từng ORđiều kiện.

https://www.brentozar.com/pastetheplan/?id=HkE3lkuqf

nhập mô tả hình ảnh ở đây

Quá trình quét được xác định là cách lấy dữ liệu tối ưu hơn 6 lần tìm kiếm.

@Daniel Hutmacher cung cấp một giải pháp tối ưu để thực hiện một Chỉ số Tìm kiếm trên IX_UserActivity_ActivityDate. Ngoài ra, bạn có thể thêm một OPTION(RECOMPILE), mặc dù điều này sẽ buộc biên dịch lại mỗi khi truy vấn được chạy, có khả năng gây hại nhiều hơn là tốt.


2
Lưu ý rằng các Bộ lọc có Dự đoán biểu thức khởi động, vì vậy chỉ một tìm kiếm sẽ thực thi khi chạy.
Paul White 9

6

Một truy vấn "bồn rửa nhà bếp" như thế (nhiều mệnh đề lọc riêng biệt một hoặc nhiều trong số đó được sử dụng tùy thuộc vào giá trị của đầu vào) sẽ không bao giờ có thể thực hiện được ngay cả khi tất cả các mệnh đề riêng lẻ của nó.

Hai tùy chọn nhanh là chia chúng thành các thủ tục riêng lẻ và gọi từng thủ tục khi cần bằng thủ tục chính hoặc sử dụng SQL ad-hoc.

Để biết một bài viết chi tiết mô tả một số tùy chọn cho loại truy vấn / thủ tục này, hãy xem http://www.sommarskog.se/dyn-search.html


1
Tôi nói về mô hình bồn rửa nhà bếp ở đây: blog.sentryone.com/aaronbertrand/ Kẻ
Aaron Bertrand

Một bài viết của bạn để đánh dấu. Cảm ơn, @AaronBertrand
PicoDeGallo

3

Để tham khảo trong tương lai, đây là giải pháp tôi đã đưa ra dựa trên câu trả lời được đề xuất của 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))
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.