Mệnh đề SARGable WHERE cho hai cột ngày


24

Tôi có một câu hỏi thú vị về SARGability. Trong trường hợp này, đó là về việc sử dụng một vị ngữ về sự khác biệt giữa hai cột ngày. Đây là thiết lập:

USE [tempdb]
SET NOCOUNT ON  

IF OBJECT_ID('tempdb..#sargme') IS NOT NULL
BEGIN
DROP TABLE #sargme
END

SELECT TOP 1000
IDENTITY (BIGINT, 1,1) AS ID,
CAST(DATEADD(DAY, [m].[severity] * -1, GETDATE()) AS DATE) AS [DateCol1],
CAST(DATEADD(DAY, [m].[severity], GETDATE()) AS DATE) AS [DateCol2]
INTO #sargme
FROM sys.[messages] AS [m]

ALTER TABLE [#sargme] ADD CONSTRAINT [pk_whatever] PRIMARY KEY CLUSTERED ([ID])
CREATE NONCLUSTERED INDEX [ix_dates] ON [#sargme] ([DateCol1], [DateCol2])

Những gì tôi sẽ thấy khá thường xuyên, là một cái gì đó như thế này:

/*definitely not sargable*/
SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [#sargme] AS [s]
WHERE
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) >= 48;

... mà chắc chắn không phải là SARGable. Nó dẫn đến việc quét chỉ mục, đọc tất cả 1000 hàng, không tốt. Hàng ước tính bốc mùi. Bạn sẽ không bao giờ đưa điều này vào sản xuất.

Không thưa ngài, tôi không thích nó.

Sẽ thật tuyệt nếu chúng ta có thể thực hiện các CTE, bởi vì điều đó sẽ giúp chúng ta thực hiện điều này, tốt hơn, SARGable-er, nói về mặt kỹ thuật. Nhưng không, chúng tôi có kế hoạch thực hiện tương tự như lên trên.

/*would be nice if it were sargable*/
WITH    [x] AS ( SELECT
                * ,
                DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) AS [ddif]
               FROM
                [#sargme] AS [s])
     SELECT
        *
     FROM
        [x]
     WHERE
        [x].[ddif] >= 48;

Và tất nhiên, vì chúng tôi không sử dụng hằng số, mã này không thay đổi gì và thậm chí không bằng một nửa SARGable. Không vui. Kế hoạch thực hiện tương tự.

/*not even half sargable*/
SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [#sargme] AS [s]
WHERE
    [s].[DateCol2] >= DATEADD(DAY, 48, [s].[DateCol1])

Nếu bạn cảm thấy may mắn và bạn tuân theo tất cả các tùy chọn ANSI SET trong chuỗi kết nối của mình, bạn có thể thêm một cột được tính toán và tìm kiếm trên đó ...

ALTER TABLE [#sargme] ADD [ddiff] AS 
DATEDIFF(DAY, DateCol1, DateCol2) PERSISTED

CREATE NONCLUSTERED INDEX [ix_dates2] ON [#sargme] ([ddiff], [DateCol1], [DateCol2])

SELECT [s].[ID] ,
       [s].[DateCol1] ,
       [s].[DateCol2]
FROM [#sargme] AS [s]
WHERE [ddiff] >= 48

Điều này sẽ giúp bạn tìm kiếm chỉ mục với ba truy vấn. Người đàn ông kỳ lạ là nơi chúng tôi thêm 48 ngày vào DateCol1. Truy vấn với DATEDIFFtrong WHEREđiều khoản, các CTE, và truy vấn cuối cùng với một vị trên cột tính toán tất cả các cung cấp cho bạn một kế hoạch rất đẹp với ước tính đẹp hơn nhiều, và tất cả những gì.

Tôi có thể sống với điều này.

Điều này đưa tôi đến câu hỏi: trong một truy vấn duy nhất, có cách nào SARGable để thực hiện tìm kiếm này không?

Không có bảng tạm thời, không có biến bảng, không thay đổi cấu trúc bảng và không có khung nhìn.

Tôi ổn với việc tự tham gia, CTE, truy vấn con hoặc nhiều lần truyền dữ liệu. Có thể hoạt động với mọi phiên bản SQL Server.

Tránh cột được tính toán là một giới hạn nhân tạo vì tôi quan tâm đến giải pháp truy vấn hơn bất kỳ điều gì khác.

Câu trả lời:


16

Chỉ cần thêm nó nhanh chóng để nó tồn tại như một câu trả lời (mặc dù tôi biết đó không phải là câu trả lời bạn muốn).

Một cột được tính toán được lập chỉ mục thường là giải pháp phù hợp cho loại vấn đề này.

Nó:

  • làm cho vị ngữ trở thành một biểu thức có thể lập chỉ mục
  • cho phép tạo số liệu thống kê tự động để ước tính số lượng thẻ tốt hơn
  • không cần lấy bất kỳ khoảng trống nào trong bảng cơ sở

Để rõ ràng về điểm cuối cùng đó, cột được tính không bắt buộc phải tồn tại trong trường hợp này:

-- Note: not PERSISTED, metadata change only
ALTER TABLE #sargme
ADD DayDiff AS DATEDIFF(DAY, DateCol1, DateCol2);

-- Index the expression
CREATE NONCLUSTERED INDEX index_name
ON #sargme (DayDiff)
INCLUDE (DateCol1, DateCol2);

Bây giờ truy vấn:

SELECT
    S.ID,
    S.DateCol1,
    S.DateCol2,
    DATEDIFF(DAY, S.DateCol1, S.DateCol2)
FROM
    #sargme AS S
WHERE
    DATEDIFF(DAY, S.DateCol1, S.DateCol2) >= 48;

... đưa ra kế hoạch tầm thường sau :

Kế hoạch thực hiện

Như Martin Smith đã nói, nếu bạn có kết nối sử dụng các tùy chọn cài đặt sai, bạn có thể tạo một cột thông thường và duy trì giá trị được tính bằng các kích hoạt.

Tất cả điều này chỉ thực sự quan trọng (thách thức mã sang một bên) nếu có một vấn đề thực sự cần giải quyết, tất nhiên, như Aaron nói trong câu trả lời của mình .

Thật thú vị khi nghĩ về điều này, nhưng tôi không biết cách nào để đạt được những gì bạn muốn một cách hợp lý với những hạn chế trong câu hỏi. Có vẻ như bất kỳ giải pháp tối ưu nào cũng cần một cấu trúc dữ liệu mới; gần nhất chúng ta có xấp xỉ 'chỉ số hàm' được cung cấp bởi một chỉ mục trên cột được tính không tồn tại như trên.


12

Rủi ro chế giễu từ một số tên tuổi lớn nhất trong cộng đồng SQL Server, tôi sẽ ló đầu ra và nói, không.

Để truy vấn của bạn có thể là SARGable, về cơ bản, bạn phải xây dựng một truy vấn có thể xác định chính xác một hàng bắt đầu trong một phạm vi các hàng liên tiếp trong một chỉ mục. Với chỉ mục ix_dates, các hàng không được sắp xếp theo chênh lệch ngày giữa DateCol1DateCol2, vì vậy các hàng mục tiêu của bạn có thể được trải ra ở bất kỳ đâu trong chỉ mục.

Tự tham gia, nhiều lượt đi, v.v ... đều có điểm chung là chúng bao gồm ít nhất một Quét chỉ mục, mặc dù phép nối (vòng lặp lồng nhau) có thể sử dụng Chỉ số Tìm kiếm. Nhưng tôi không thể thấy làm thế nào có thể loại bỏ Quét.

Đối với việc ước tính hàng chính xác hơn, không có số liệu thống kê về chênh lệch ngày.

Cấu trúc CTE đệ quy khá xấu xí sau đây về mặt kỹ thuật sẽ loại bỏ việc quét toàn bộ bảng, mặc dù nó giới thiệu một Nested Loop Join và số lượng Tìm kiếm Index rất lớn (có khả năng rất lớn).

DECLARE @from date, @count int;
SELECT TOP 1 @from=DateCol1 FROM #sargme ORDER BY DateCol1;
SELECT TOP 1 @count=DATEDIFF(day, @from, DateCol1) FROM #sargme WHERE DateCol1<=DATEADD(day, -48, {d '9999-12-31'}) ORDER BY DateCol1 DESC;

WITH cte AS (
    SELECT 0 AS i UNION ALL
    SELECT i+1 FROM cte WHERE i<@count)

SELECT b.*
FROM cte AS a
INNER JOIN #sargme AS b ON
    b.DateCol1=DATEADD(day, a.i, @from) AND
    b.DateCol2>=DATEADD(day, 48+a.i, @from)
OPTION (MAXRECURSION 0);

Nó tạo ra một Spool Index chứa mọi thứ DateCol1trong bảng, sau đó thực hiện Index Seek (quét phạm vi) cho từng cái DateCol1DateCol2đó là ít nhất 48 ngày về phía trước.

Nhiều IO hơn, thời gian thực hiện lâu hơn một chút, ước tính hàng vẫn chưa hết và không có cơ hội song song vì đệ quy: Tôi đoán truy vấn này có thể hữu ích nếu bạn có số lượng giá trị rất lớn trong một vài tương đối khác biệt, liên tiếp DateCol1(giữ số lượng Tìm kiếm xuống).

Kế hoạch truy vấn CTE đệ quy điên


9

Tôi đã thử một loạt các biến thể lập dị, nhưng không tìm thấy phiên bản nào tốt hơn phiên bản của bạn. Vấn đề chính là chỉ mục của bạn trông như thế này về cách sắp xếp date1 và date2. Cột đầu tiên sẽ nằm trong một hàng có giá đỡ đẹp trong khi khoảng cách giữa chúng sẽ rất lởm chởm. Bạn muốn nó trông giống một cái phễu hơn là cách nó thực sự sẽ:

Date1    Date2
-----    -------
*             *
*             *
*              *
 *       * 
 *        *
 *         *
  *      *
  *           *

Thực sự không có cách nào tôi có thể nghĩ ra để làm cho điều đó có thể tìm kiếm cho một vùng đồng bằng (hoặc phạm vi đồng bằng) nhất định giữa hai điểm. Và tôi có nghĩa là một tìm kiếm duy nhất được thực hiện một lần + quét phạm vi, không phải là tìm kiếm được thực hiện cho mỗi hàng. Điều đó sẽ liên quan đến việc quét và / hoặc sắp xếp tại một số điểm, và đây là những điều bạn muốn tránh rõ ràng. Thật tệ khi bạn không thể sử dụng các biểu thức như DATEADD/ DATEDIFFtrong các chỉ mục được lọc hoặc thực hiện bất kỳ sửa đổi lược đồ nào có thể cho phép sắp xếp trên sản phẩm của ngày khác (như tính toán delta tại thời điểm chèn / cập nhật). Như vậy, đây dường như là một trong những trường hợp quét thực sự là phương pháp truy xuất tối ưu.

Bạn nói rằng truy vấn này không có gì thú vị, nhưng nếu bạn nhìn kỹ hơn, thì đây là truy vấn tốt nhất (và thậm chí sẽ tốt hơn nếu bạn bỏ qua đầu ra vô hướng tính toán):

SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [#sargme] AS [s]
WHERE
    [s].[DateCol2] >= DATEADD(DAY, 48, [s].[DateCol1])

Lý do là để tránh DATEDIFFkhả năng làm mất một số CPU so với phép tính chỉ dựa vào cột khóa không dẫn đầu trong chỉ mục và cũng tránh một số chuyển đổi ngầm ẩn khó chịu datetimeoffset(7)(đừng hỏi tôi tại sao chúng lại ở đó, nhưng chúng là vậy). Đây là DATEDIFFphiên bản:

<Vị ngữ>
<ScalarOperator ScalarString = "dateiff (day, CONVERT_IMPLICIT (datetimeoffset (7), [splunge]. [Dbo]. [Sargme]. [DateCol1] là [s]. [DateCol1], [DateCol1] 7), [splunge]. [Dbo]. [Sargme]. [DateCol2] là [s]. [DateCol2], 0))> = (48) ">

Và đây là cái không có DATEDIFF:

<Vị ngữ>
<ScalarOperator ScalarString = "[splunge]. [Dbo]. [Sargme]. [DateCol2] là [s]. [DateCol2]> = dateadd (day, (48), [splunge]. [Dbo]. sargme]. [DateCol1] là [s]. [DateCol1]) ">

Ngoài ra, tôi thấy kết quả tốt hơn một chút về thời lượng khi tôi thay đổi chỉ mục thành chỉ bao gồm DateCol2 (và khi có cả hai chỉ mục, SQL Server luôn chọn một có một khóa và một bao gồm cột so với đa khóa). Đối với truy vấn này, vì chúng tôi phải quét tất cả các hàng để tìm phạm vi dù sao, không có lợi ích gì khi có cột ngày thứ hai là một phần của khóa và được sắp xếp theo bất kỳ cách nào. Và trong khi tôi biết rằng chúng ta không thể tìm kiếm ở đây, có một điều gì đó vốn dĩ có cảm giác tốt về việc không cản trở khả năng có được một bằng cách buộc các phép tính đối với cột khóa dẫn đầu và chỉ thực hiện chúng đối với các cột thứ cấp hoặc bao gồm.

Nếu là tôi và tôi đã từ bỏ việc tìm giải pháp khả thi, tôi biết mình sẽ chọn giải pháp nào - giải pháp nào khiến SQL Server thực hiện ít công việc nhất (ngay cả khi delta gần như không có). Hoặc tốt hơn nữa tôi sẽ nới lỏng các hạn chế của tôi về thay đổi lược đồ và tương tự.

Và bao nhiêu tất cả những vấn đề đó? Tôi không biết. Tôi đã tạo bảng 10 triệu hàng và tất cả các biến thể truy vấn ở trên vẫn hoàn thành trong một giây. Và đây là trên máy ảo trên máy tính xách tay (được cấp, có SSD).


3

Tất cả các cách tôi đã nghĩ để làm cho mệnh đề WHERE có thể rất phức tạp và cảm thấy như làm việc đối với chỉ mục tìm kiếm như một mục tiêu cuối cùng chứ không phải là một phương tiện. Vì vậy, không, tôi không nghĩ rằng nó (thực tế) có thể.

Tôi không chắc chắn nếu "không thay đổi cấu trúc bảng" có nghĩa là không có chỉ mục bổ sung. Đây là một giải pháp tránh hoàn toàn việc quét chỉ mục, nhưng dẫn đến rất nhiều tìm kiếm chỉ mục riêng biệt, tức là một giải pháp cho mỗi ngày DateCol1 có thể có trong phạm vi giá trị tối thiểu / tối đa trong bảng. (Không giống như Daniel dẫn đến một tìm kiếm cho mỗi ngày riêng biệt thực sự xuất hiện trong bảng). Về mặt lý thuyết nó là một ứng cử viên cho sự song song b / c nó tránh được đệ quy. Nhưng thành thật mà nói, thật khó để thấy một phân phối dữ liệu trong đó điều này nhanh hơn là chỉ quét và thực hiện DATEDIFF. (Có lẽ aa thực sự cao DOP?) Và ... mã là xấu. Tôi đoán nỗ lực này được tính là một "bài tập tinh thần".

--Add this index to avoid the scan when determining the @MaxDate value
--CREATE NONCLUSTERED INDEX [ix_dates2] ON [#sargme] ([DateCol2]);
DECLARE @MinDate DATE, @MaxDate DATE;
SELECT @MinDate=DateCol1 FROM (SELECT TOP 1 DateCol1 FROM #sargme ORDER BY DateCol1 ASC) ss;
SELECT @MaxDate=DateCol2 FROM (SELECT TOP 1 DateCol2 FROM #sargme ORDER BY DateCol2 DESC) ss;

--Used 44 just to get a few more rows to test my logic
DECLARE @DateDiffSearchValue INT = 44, 
    @MinMaxDifference INT = DATEDIFF(DAY, @MinDate, @MaxDate);

--basic data profile in the table
SELECT [MinDate] = @MinDate, 
        [MaxDate] = @MaxDate, 
        [MinMaxDifference] = @MinMaxDifference, 
        [LastDate1SearchValue] = DATEADD(DAY, 0-@DateDiffSearchValue, @MaxDate);

;WITH rn_base AS (
SELECT [col1] = 0
        UNION ALL SELECT 0
        UNION ALL SELECT 0
        UNION ALL SELECT 0
),
rn_1 AS (
    SELECT t0.col1 FROM rn_base t0
        CROSS JOIN rn_base t1
        CROSS JOIN rn_base t2
        CROSS JOIN rn_base t3
),
rn_2 AS (
    SELECT rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    FROM rn_1 t0
        CROSS JOIN rn_1 t1
),
candidate_searches AS (
    SELECT 
        [Date1_EqualitySearch] = DATEADD(DAY, t.rn-1, @MinDate),
        [Date2_RangeSearch] = DATEADD(DAY, t.rn-1+@DateDiffSearchValue, @MinDate)
    FROM rn_2 t
    WHERE DATEADD(DAY, t.rn-1, @MinDate) <= DATEADD(DAY, 0-@DateDiffSearchValue, @MaxDate)
    /* Of course, ignore row-number values that would result in a
       Date1_EqualitySearch value that is < @DateDiffSearchValue days before @MaxDate */
)
--select * from candidate_searches

SELECT c.*, xapp.*, dd_rows = DATEDIFF(DAY, xapp.DateCol1, xapp.DateCol2)
FROM candidate_searches c
    cross apply (
        SELECT t.*
        FROM #sargme t
        WHERE t.DateCol1 = c.date1_equalitysearch
        AND t.DateCol2 >= c.date2_rangesearch
    ) xapp
ORDER BY xapp.ID asc --xapp.DateCol1, xapp.DateCol2 

3

Câu trả lời Wiki cộng đồng ban đầu được thêm bởi tác giả câu hỏi dưới dạng chỉnh sửa cho câu hỏi

Sau khi để điều này diễn ra một chút và một số người thực sự thông minh, tôi nghĩ ban đầu về điều này có vẻ đúng: không có cách nào lành mạnh và SARGable để viết truy vấn này mà không cần thêm một cột, được tính toán hoặc duy trì thông qua một số cơ chế khác, cụ thể là gây nên.

Tôi đã thử một vài thứ khác, và tôi có một số quan sát khác có thể hoặc không thú vị với bất cứ ai đọc.

Đầu tiên, chạy lại thiết lập bằng bảng thông thường thay vì bảng tạm thời

  • Mặc dù tôi biết danh tiếng của họ, tôi muốn thử thống kê nhiều cột. Họ đã vô dụng.
  • Tôi muốn xem thống kê nào đã được sử dụng

Đây là thiết lập mới:

USE [tempdb]
SET NOCOUNT ON  

DBCC FREEPROCCACHE

IF OBJECT_ID('tempdb..sargme') IS NOT NULL
BEGIN
DROP TABLE sargme
END

SELECT TOP 1000
IDENTITY (BIGINT, 1,1) AS ID,
CAST(DATEADD(DAY, [m].[severity] * -1, GETDATE()) AS DATE) AS [DateCol1],
CAST(DATEADD(DAY, [m].[severity], GETDATE()) AS DATE) AS [DateCol2]
INTO sargme
FROM sys.[messages] AS [m]

ALTER TABLE [sargme] ADD CONSTRAINT [pk_whatever] PRIMARY KEY CLUSTERED ([ID])
CREATE NONCLUSTERED INDEX [ix_dates] ON [sargme] ([DateCol1], [DateCol2])

CREATE STATISTICS [s_sargme] ON [sargme] ([DateCol1], [DateCol2])

Sau đó, chạy truy vấn đầu tiên, nó sử dụng chỉ mục ix_dates và quét, giống như trước đây. Không có thay đổi ở đây. Điều này có vẻ dư thừa, nhưng gắn bó với tôi.

SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [sargme] AS [s]
WHERE
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) >= 48

Chạy lại truy vấn CTE, vẫn như cũ ...

WITH    [x] AS ( SELECT
                * ,
                DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) AS [ddif]
               FROM
                [sargme] AS [s])
     SELECT
        *
     FROM
        [x]
     WHERE
        [x].[ddif] >= 48;

Ổn thỏa! Chạy lại truy vấn không đồng đều một nửa:

SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [sargme] AS [s]
WHERE
    [s].[DateCol2] >= DATEADD(DAY, 48, [s].[DateCol1])

Bây giờ thêm cột được tính toán và chạy lại cả ba, cùng với truy vấn truy cập cột được tính:

ALTER TABLE [sargme] ADD [ddiff] AS 
DATEDIFF(DAY, DateCol1, DateCol2) PERSISTED

CREATE NONCLUSTERED INDEX [ix_dates2] ON [sargme] ([ddiff], [DateCol1], [DateCol2])

SELECT [s].[ID] ,
       [s].[DateCol1] ,
       [s].[DateCol2]
FROM [sargme] AS [s]
WHERE [ddiff] >= 48

Nếu bạn mắc kẹt với tôi đến đây, cảm ơn. Đây là phần quan sát thú vị của bài.

Chạy một truy vấn với cờ theo dõi không có giấy tờ của Fabiano Amorim để xem thống kê nào mỗi truy vấn được sử dụng là khá tuyệt. Thấy rằng không có kế hoạch nào chạm vào một đối tượng thống kê cho đến khi cột được tính toán được tạo và lập chỉ mục có vẻ kỳ quặc.

Cái gì

Heck, ngay cả truy vấn nhấn cột CHỈ đã không chạm vào một đối tượng thống kê cho đến khi tôi chạy nó một vài lần và nó có tham số hóa đơn giản. Vì vậy, mặc dù ban đầu tất cả họ đều quét chỉ số ix_dates, họ đã sử dụng ước tính số lượng thẻ được mã hóa cứng (30% của bảng) thay vì bất kỳ đối tượng thống kê nào có sẵn cho họ.

Một điểm khác khiến người ta chú ý ở đây là khi tôi chỉ thêm chỉ mục không bao gồm, truy vấn sẽ lập kế hoạch quét toàn bộ HEAP, thay vì sử dụng chỉ mục không bao gồm trên cả hai cột ngày.

Cảm ơn mọi người đã trả lời. Bạn thật tuyệt vời.

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.