Предложение 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;

... что определенно не SARGable. Это приводит к сканированию индекса, читает все 1000 строк, ничего хорошего. Расчетные ряды воняют. Ты бы никогда не запустил это в производство.

Нет, сэр, мне это не понравилось.

Было бы хорошо, если бы мы могли материализовать CTE, потому что это помогло бы нам сделать это, ну, в общем, более САРГЕЙЛЬНО, технически говоря. Но нет, мы получаем тот же план выполнения, что и наверху.

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

Это даст вам поиск по индексу с тремя запросами. Странно, что мы добавляем 48 дней к DateCol1. Запрос 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;

... дает следующий тривиальный план:

План выполнения

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

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

Об этом интересно думать, но я не знаю способа достичь того, чего вы хотите, разумно, учитывая ограничения в вопросе. Кажется, что любое оптимальное решение потребовало бы новой структуры данных некоторого типа; наиболее близким является приближение «индекса функции», обеспечиваемое индексом для непостоянного вычисляемого столбца, как указано выше.

Пол Уайт говорит, что GoFundMonica
источник
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 дней вперед.

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

Сумасшедший рекурсивный план запроса CTE

Даниэль Хутмахер
источник
9

Я перепробовал кучу дурацких вариаций, но не нашел ни одной версии лучше вашей. Основная проблема заключается в том, что ваш индекс выглядит так с точки зрения того, как date1 и date2 отсортированы вместе. Первый столбец будет находиться в хорошей линии, в то время как разрыв между ними будет очень неровным. Вы хотите, чтобы это выглядело скорее как воронка, чем так, как это будет на самом деле:

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 = "datediff (day, CONVERT_IMPLICIT (datetimeoffset (7), [splunge]. [Dbo]. [Sargme]. [DateCol1] как [с]. [DateCol1], 0), CONVERT_IMPLICIT (datetimeoffset ( 7), [splunge]. [Dbo]. [Sargme]. [DateCol2] как [s]. [DateCol2], 0))> = (48) ">

А вот тот без DATEDIFF:

<Predicate>
<ScalarOperator ScalarString = "[splunge]. [Dbo]. [Sargme]. [DateCol2] as [s]. [DateCol2]> = dateadd (день, (48), [разделение]. [Dbo]. [ sargme]. [DateCol1] as [s]. [DateCol1]) ">

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

Если бы это был я, и я отказался от поиска подходящего решения, я знаю, какое из них я выбрал бы - то, которое заставляет SQL Server выполнять наименьшее количество работы (даже если дельта почти не существует). Или, что еще лучше, я бы ослабил свои ограничения по изменению схемы и тому подобное.

И насколько все это имеет значение? Я не знаю. Я сделал таблицу 10 миллионов строк, и все вышеупомянутые варианты запроса все еще завершены менее чем за секунду. И это на ВМ на ноутбуке (предоставляется, с SSD).

Аарон Бертран
источник
3

Все способы, с помощью которых я мог сделать предложение WHERE sargable, сложны и мне кажется, что работа над индексом является конечной целью, а не средством. Так что нет, я не думаю, что это (прагматично) возможно.

Я не был уверен, что «отсутствие изменения структуры таблицы» означает отсутствие дополнительных индексов. Вот решение, которое полностью избегает сканирования индекса, но приводит к МНОГО отдельного поиска индекса, то есть по одному для каждой возможной даты DateCol1 в диапазоне значений даты Min / Max в таблице. (В отличие от Даниэля, который приводит к одному поиску для каждой отдельной даты, которая фактически появляется в таблице). Теоретически является кандидатом на параллелизм, потому что избегает рекурсии. Но, честно говоря, трудно увидеть распределение данных, где это происходит быстрее, чем просто сканирование и выполнение 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

Сообщество Wiki ответ изначально добавлено автором вопроса в качестве правки к вопросу

После того, как я позволил этому немного подождать, и некоторые действительно умные люди заговорили, моя первоначальная мысль об этом кажется верной: нет никакого здравого и приемлемого способа написать этот запрос без добавления столбца, вычисляемого или поддерживаемого с помощью какого-то другого механизма, а именно: триггеры.

Я попробовал несколько других вещей, и у меня есть некоторые другие наблюдения, которые могут или не могут быть интересны любому читающему.

Во-первых, перезапустите установку, используя обычную таблицу, а не временную таблицу

  • Несмотря на то, что я знаю их репутацию, я хотел попробовать статистику из нескольких столбцов. Они были бесполезны.
  • Я хотел посмотреть, какая статистика использовалась

Вот новая настройка:

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;

Хорошо! Еще раз запустите запрос «не даже наполовину sargable»:

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, а не использует некластеризованный индекс в обоих столбцах даты.

Спасибо всем кто ответил. Вы все замечательные.

Пол Уайт говорит, что GoFundMonica
источник