Запит SARGable WHERE для двох стовпців дати


24

У мене є цікаве питання щодо SARGability. У цьому випадку мова йде про використання присудка на різницю між двома стовпцями дати. Ось налаштування:

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

Що я бачу досить часто, - це щось подібне:

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

... що, безумовно, НЕ МОЖЛИВО. Це призводить до сканування індексу, зчитує всі 1000 рядків, нічого хорошого. Орієнтовні ряди смердять. Ви б ніколи цього не ставили у виробництво.

Ні, пане, мені це не сподобалось.

Було б добре, якби ми змогли реалізувати CTE, тому що це допоможе нам зробити це, ну, більш SARGable-er, технічно кажучи. Але ні, ми отримуємо той же план виконання, що і вгорі.

/*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;

І звичайно, оскільки ми не використовуємо константи, цей код нічого не змінює і навіть не є наполовину SARGable. Не смішно. План виконання.

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

Якщо вам пощастило, і ви підкоряєтесь усім параметрам ANSI SET у рядках підключення, ви можете додати обчислений стовпець і здійснити пошук у ньому ...

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

Це допоможе вам отримати індекс з трьома запитами. Незвичайна людина - це додавання до DateCol1 48 днів. Запит з DATEDIFFв WHEREстатті, то CTEі остаточний запит з предикатом на обчислюваному стовпці все дає вам набагато приємніше план з набагато більш гарними оцінками, і все це.

Я міг би з цим жити.

Що приводить мене до питання: чи в одному запиті існує SARGable спосіб здійснення цього пошуку?

Немає тимчасових таблиць, немає змінних таблиць, не змінюється структура таблиці та немає переглядів.

Мені добре з самостійними приєднаннями, CTE, підзапитами або декількома передачами даних. Може працювати з будь-якою версією SQL Server.

Уникнення обчислених стовпців є штучним обмеженням, оскільки мене більше цікавить рішення запиту, ніж будь-що інше.

Відповіді:


16

Просто додайте це швидко, щоб воно існувало як відповідь (хоча я знаю, що це не потрібна відповідь).

Індексуються обчислюваний стовпець , як правило , правильне рішення для такого роду проблеми.

Це:

  • робить предикат виразним виразом
  • дозволяє створювати автоматичну статистику для кращої оцінки кардинальності
  • не потрібно займати місця в базовій таблиці

Щоб було зрозуміло в останньому пункті, обчислюваний стовпчик у цьому випадку не потрібно зберігати :

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

Тепер запит:

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;

... дає такий тривіальний план:

План виконання

Як сказав Мартін Сміт, якщо у вас є з'єднання, використовуючи неправильно встановлені параметри, ви можете створити звичайний стовпець і підтримувати обчислене значення, використовуючи тригери.

Все це дійсно має значення (виклик коду вбік), якщо існує реальна проблема, яку, звичайно, потрібно вирішити, як каже Аарон у своїй відповіді .

Це цікаво подумати, але я не знаю жодного способу досягти того, що ви хочете розумно, враховуючи обмеження у питанні. Схоже, що будь-яке оптимальне рішення потребує нової структури даних певного типу; найближчим до нас є наближення 'індексу функції', що надається індексом на непостійному обчисленому стовпчику, як зазначено вище.


12

Ризикуючи насмішки з одних з найбільших імен у спільноті SQL Server, я збираюся висунути шию і сказати: ні.

Для того, щоб ваш запит був SARGable, вам доведеться в основному побудувати запит, який може точно вказати початковий рядок у діапазоні послідовних рядків в індексі. Що стосується індексу ix_dates, рядки не впорядковуються за різницею між датами DateCol1та DateCol2, тому ваші цільові рядки можуть бути розкинуті в будь-якому місці індексу.

Самопоєднання, декілька пропусків і т. Д. Мають усе спільне, що вони включають щонайменше одне сканування індексів, хоча при вкладенні циклу (вкладеного циклу) цілком можна використовувати індекс пошуку. Але я не бачу, як можна було б усунути сканування.

Щодо отримання більш точних оцінок рядків, статистики щодо різниці дат немає.

Наведена нижче досить потворна рекурсивна конструкція CTE технічно усуває сканування всієї таблиці, хоча вона вводить вкладене вкладене цикл та (можливо, дуже велике) число індексів шукає.

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

Він створює котушку індексу, що містить кожну DateCol1таблицю, а потім виконує пошук індексу (сканування діапазону) для кожного з них, DateCol1і DateCol2це не менше 48 днів вперед.

Більше IO, трохи більший час виконання, оцінка рядків все ще вимикається, і нульовий шанс паралелізації через рекурсії: я здогадуюсь, цей запит може бути корисним, якщо у вас дуже велика кількість значень в порівняно мало чітких, послідовних DateCol1(зменшення кількості шукань).

Божевільний рекурсивний план запитів CTE


9

Я спробував купу нерозумних варіацій, але не знайшов жодної версії кращої за вашу. Основна проблема полягає в тому, що ваш індекс виглядає таким чином з точки зору того, як сортуються дата1 та дата2. Перший стовпчик буде знаходитись у приємній відкладеній лінії, тоді як зазор між ними буде дуже нерівним. Ви хочете, щоб це більше нагадувало воронку, ніж так, як це буде насправді:

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

Насправді я не можу придумати, щоб це було шукати для певної дельти (або діапазону дельт) між двома точками. І я маю на увазі один пошук, який виконується один раз + сканування діапазону, а не пошук, який виконується для кожного ряду. Це передбачає сканування та / або сортування в якийсь момент, і це очевидно, чого ви хочете уникати. Це дуже погано, що ви не можете використовувати вирази, як DATEADD/ DATEDIFFу відфільтрованих індексах, або виконувати будь-які можливі модифікації схеми, які дозволяли б сортувати на творі різницю дати (наприклад, обчислення дельти під час вставки / оновлення). Так, здається, це один із тих випадків, коли сканування насправді є оптимальним методом пошуку.

Ви сказали, що цей запит не цікавий, але якщо ви придивитесь ближче, це, безумовно, найкращий (і було б навіть краще, якщо ви вийшли з обчислювального скалярного результату):

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

Причина полягає в тому, що уникнення DATEDIFFпотенційно гоління деякого процесора порівняно з обчисленням лише для непровідних стовпців ключових даних в індексі, а також дозволяє уникнути неприємних неявних перетворень datetimeoffset(7)(не запитуйте мене, чому вони є, але вони є). Ось DATEDIFFверсія:

<Predicate>
<ScalarOperator ScalarString = "dateiff (день, CONVERT_IMPLICIT (datetimeoffset (7), [splunge]. [Dbo]. [Sargme]. [DateCol1]] [[]]. 7), [splunge]. [Dbo]. [Sargme]. [DateCol2] як [s]. [DateCol2], 0))> = (48) ">

І ось такий без DATEDIFF:

<Predicate>
<ScalarOperator ScalarString = "[splunge]. [Dbo]. [Sargme]. [DateCol2] as [s]. [DateCol2]> = dateadd (день, (48), [splunge]. [Dbo]. [Dbo]. [ sargme]. [DateCol1] як [s]. [DateCol1]) ">

Крім того, я виявив трохи кращі результати за тривалістю, коли я змінив індекс на лише включення DateCol2 (і коли обидва індексу були присутніми, SQL Server завжди вибирав один з одним ключем, а один включає стовпець проти багатоклавішний). Для цього запиту, оскільки нам потрібно сканувати всі рядки, щоб знайти діапазон у будь-якому випадку, немає користі, щоб другий стовпець дати був частиною ключа та відсортований будь-яким чином. І хоча я знаю, що ми не можемо шукати тут, є щось, що є по суті приємним почуттям у тому, щоб не перешкоджати можливості отримати його, примушуючи обчислення проти провідного стовпчика ключів, а лише виконуючи їх проти вторинних або включених стовпців.

Якби це я, і я відмовився від пошуку вирішального рішення, я знаю, яке з них я б обрав - той, який змушує SQL Server робити найменшу кількість роботи (навіть якщо дельта майже відсутня). Або ще краще, я б зняв свої обмеження щодо зміни схеми тощо.

І наскільки все це має значення? Не знаю. Я зробив у таблиці 10 мільйонів рядків, і всі вищезазначені варіанти запитів все ще виконані за секунду. І це на VM на ноутбуці (надано, із SSD).


3

Усі способи, про які я думав зробити так, щоб слово «WHERE», яке є спроможним для sarg, є складним і відчуваю, що робота над індексом шукає як кінцеву мету, а не як засіб. Отже, ні, я не думаю, що це (прагматично) можливо.

Я не був впевнений, що "жодна зміна структури таблиці" не означає додаткових індексів. Ось рішення, яке повністю дозволяє уникнути сканування індексів, але приводить до отримання багато окремих пошукових записів індексу, тобто для кожної можливої дати DateCol1 у діапазоні значень дати мінімум / макс у таблиці. (На відміну від Даніеля, який призводить до пошуку кожної окремої дати, яка фактично відображається в таблиці). Це теоретично є кандидатом на паралелізм, не допускаючи рекурсії. Але, чесно кажучи, важко побачити розподіл даних, де ця річ швидша, ніж просто сканування та виконання DATEDIFF. (Можливо, справді високий DOP?) І ... код некрасивий. Я думаю, що це зусилля розцінюється як "розумова вправа".

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

Відповідь Вікі спільноти спочатку додав автор запитання як редагування питання

Після того, як я трохи посидів, і деякі по-справжньому розумні люди задзвонили, моя початкова думка про це видається правильною: немає жодного розумного і SARGable способу написання цього запиту без додавання стовпчика, ні обчисленого, ні підтримуваного за допомогою якогось іншого механізму, а саме тригери.

Я спробував ще декілька речей, і у мене є деякі інші спостереження, які можуть чи не можуть зацікавити когось, хто читає.

По-перше, повторно запустіть налаштування, використовуючи звичайну таблицю, а не тимчасову таблицю

  • Незважаючи на те, що я знаю їхню репутацію, я хотів випробувати статистику з кількома колонками. Вони були марними.
  • Я хотів подивитися, яка статистика використовується

Ось нова настройка:

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

Потім, виконуючи перший запит, він використовує індекс ix_dates та сканує, як і раніше. Тут немає змін. Це здається зайвим, але дотримуйтесь мене.

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

Запустіть запит CTE ще раз, все одно ...

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

Добре! Запустіть ще раз не парний запит:

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

Тепер додайте обчислений стовпець та повторно запустіть усі три разом із запитом, який потрапляє у обчислений стовпець:

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

Якщо ви затрималися зі мною тут, дякую. Це цікава частина спостереження за постом.

Запуск запиту з недокументованим прапором сліду від Fabiano Amorim, щоб побачити, яка статистика кожного запиту використовується досить класно. Бачачи, що жоден план не торкався об’єкта статистики, поки обчислений стовпець не був створений та індексований, здавалося дивним.

Який кров’яний згусток

Чорт, навіть запит, що потрапив у обчислений стовпець ТОЛЬКО, не торкнувся об’єкта статистики, поки я не запустив його кілька разів, і він отримав просту параметризацію. Отже, хоча вони спочатку сканували індекс ix_dates, вони використовували жорстко оцінені показники кардинальності (30% таблиці), а не будь-який доступний їм об'єкт статистики.

Ще один момент, який підняв брову тут, - це те, що коли я додав лише некластеризований індекс, плани запитів усі сканували HEAP, а не використовувати некластеризований індекс в обох стовпцях дати.

Дякую всім, хто відгукнувся. Ви всі прекрасні.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.