Плохо выполняющий подзапрос с датами сравнения

15

При использовании подзапроса для поиска общего количества всех предыдущих записей с совпадающим полем, производительность ужасна для таблицы с всего лишь 50 тыс. Записей. Без подзапроса запрос выполняется за несколько миллисекунд. С подзапросом время выполнения превышает одну минуту.

Для этого запроса результат должен:

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

Схема базовой таблицы

Activity
======================
Id int Identifier
Address varchar(25)
ActionDate datetime2
Process varchar(50)
-- 7 other columns

Пример данных

Id  Address     ActionDate (Time part excluded for simplicity)
===========================
99  000         2017-05-30
98  111         2017-05-30
97  000         2017-05-29
96  000         2017-05-28
95  111         2017-05-19
94  222         2017-05-30

Ожидаемые результаты

Для диапазона дат 2017-05-29до2017-05-30

Id  Address     ActionDate    PriorCount
=========================================
99  000         2017-05-30    2  (3 total, 2 prior to ActionDate)
98  111         2017-05-30    1  (2 total, 1 prior to ActionDate)
94  222         2017-05-30    0  (1 total, 0 prior to ActionDate)
97  000         2017-05-29    1  (3 total, 1 prior to ActionDate)

Записи 96 и 95 исключены из результата, но включены в PriorCount подзапрос

Текущий запрос

select 
    *.a
    , ( select count(*) 
        from Activity
        where 
            Activity.Address = a.Address
            and Activity.ActionDate < a.ActionDate
    ) as PriorCount
from Activity a
where a.ActionDate between '2017-05-29' and '2017-05-30'
order by a.ActionDate desc

Текущий индекс

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON [dbo].[Activity]
(
    [ActionDate] ASC
)
INCLUDE ([Address]) WITH (
    PAD_INDEX = OFF, 
    STATISTICS_NORECOMPUTE = OFF, 
    SORT_IN_TEMPDB = OFF, 
    DROP_EXISTING = OFF, 
    ONLINE = OFF, 
    ALLOW_ROW_LOCKS = ON, 
    ALLOW_PAGE_LOCKS = ON
)

Вопрос

  • Какие стратегии можно использовать для повышения производительности этого запроса?

Редактировать 1
В ответ на вопрос, что я могу изменить в БД: я могу изменять индексы, но не структуру таблицы.

Правка 2
Теперь я добавил базовый индекс в Addressстолбец, но, похоже, он не сильно улучшился. В настоящее время я нахожу гораздо лучшую производительность с созданием временной таблицы и вставкой значений без, PriorCountа затем обновляя каждую строку с их конкретными счетчиками.

Редактировать 3
Индекс Spool Джо Оббиш (принятый ответ) была найдена проблема. Как только я добавил новое nonclustered index [xyz] on [Activity] (Address) include (ActionDate), время запроса сократилось с одной минуты до менее секунды без использования временной таблицы (см. Редактирование 2).

Метро Смурф
источник

Ответы:

17

С помощью определения индекса, которое у вас есть IDX_my_nme, SQL Server сможет выполнять поиск по ActionDateстолбцу, но не по Addressстолбцу. Индекс содержит все столбцы, необходимые для покрытия подзапроса, но он, вероятно, не очень избирателен для этого подзапроса. Предположим, что почти все данные в таблице имеют ActionDateзначение раньше, чем '2017-05-30'. Поиск ActionDate < '2017-05-30'вернет почти все строки из индекса, которые далее фильтруются после выборки строки из индекса. Если ваш запрос возвращает 200 строк, вы, вероятно, выполните почти 200 полных сканирований индексаIDX_my_nme , что означает, что вы прочитаете около 50000 * 200 = 10 миллионов строк из индекса.

Вероятно, поиск Addressбудет гораздо более избирательным для вашего подзапроса, хотя вы не предоставили нам полную статистическую информацию о запросе, так что это предположение с моей стороны. Однако предположим, что вы создаете индекс для just, Addressи ваша таблица имеет 10 000 уникальных значений для Address. С новым индексом SQL Server нужно будет только искать 5 строк из индекса для каждого выполнения подзапроса, поэтому вы будете читать около 200 * 5 = 1000 строк из индекса.

Я тестирую против SQL Server 2016, поэтому могут быть небольшие различия в синтаксисе. Ниже приведены некоторые примеры данных, в которых я сделал предположения, аналогичные приведенным выше для распределения данных:

CREATE TABLE #Activity (
    Id int NOT NULL,
    [Address] varchar(25) NULL,
    ActionDate datetime2 NULL,
    FILLER varchar(100),
    PRIMARY KEY (Id)
);

INSERT INTO #Activity WITH (TABLOCK)
SELECT TOP (50000) -- 50k total rows
x.RN
, x.RN % 10000 -- 10k unique addresses
, DATEADD(DAY, x.RN / 100, '20160201') -- 100 rows per day
, REPLICATE('Z', 100)
FROM
(
    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) x;

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON #Activity
([ActionDate] ASC) INCLUDE ([Address]);

Я создал ваш индекс, как описано в вопросе. Я проверяю этот запрос, который возвращает те же данные, что и в вопросе:

select 
    a.*
    , ( select count(*) 
        from #Activity Activity
        where 
            Activity.[Address] = a.[Address]
            and Activity.ActionDate < a.ActionDate
    ) as PriorCount
from #Activity a
where a.ActionDate between '2017-05-29' and '2017-05-30'
order by a.ActionDate desc;

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

золотник

Запрос все еще быстро заканчивается для меня. Возможно, в вашей системе не выполняется оптимизация катушки индекса, или в определении таблицы или запросе есть что-то другое. В образовательных целях я могу использовать недокументированную функцию OPTION (QUERYRULEOFF BuildSpool)для отключения катушки индекса. Вот как выглядит план:

плохой поиск индекса

Не обманывайтесь появлением простого поиска индекса. SQL Server читает почти 10 миллионов строк из индекса:

10 миллионов строк из индекса

Если я собираюсь выполнить запрос более одного раза, то оптимизатору запросов, вероятно, не имеет смысла создавать индекс при каждом запуске. Я мог бы создать индекс заранее, который будет более избирательным для этого запроса:

CREATE NONCLUSTERED INDEX [IDX_my_nme_2] ON #Activity
([Address] ASC) INCLUDE (ActionDate);

План похож на ранее:

индекс поиска

Однако с новым индексом SQL Server считывает только 1000 строк из индекса. 800 строк возвращаются для подсчета. Индекс может быть определен как более избирательный, но этого может быть достаточно, в зависимости от вашего распределения данных.

хороший поиск

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

SELECT t.*
FROM
(
    select 
        a.*
        , -1 + ROW_NUMBER() OVER (PARTITION BY [Address] ORDER BY ActionDate) PriorCount
    from #Activity a
) t
where t.ActionDate between '2017-05-29' and '2017-05-30'
order by t.ActionDate desc;

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

плохая сортировка

Однако, если вам действительно нравится этот шаблон кода, вы можете определить индекс, чтобы сделать его более эффективным:

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON #Activity
([Address], [ActionDate]) INCLUDE (FILLER);

Это перемещает сортировку к концу, что будет намного дешевле:

хороший вид

Если ничего из этого не помогло, вам нужно будет добавить дополнительную информацию к вопросу, предпочтительно с фактическими планами выполнения.

Джо Оббиш
источник
1
Проблема была в катушке индекса. Как только я добавил новое nonclustered index [xyz] on [Activity] (Address) include (ActionDate), время запроса сократилось с минуты на минуту до менее чем секунды. +10 если бы мог. Благодарность!
Метро Смурф