Резюме
Основными проблемами являются:
- Выбор плана оптимизатора предполагает равномерное распределение значений.
- Отсутствие подходящих индексов означает:
- Сканирование таблицы - единственный вариант.
- Объединение является наивными вложенными циклы, а не индекс вложенных циклов. В наивном соединении предикаты объединения оцениваются в соединении, а не выталкиваются вниз по внутренней стороне объединения.
подробности
Эти два плана в основном очень похожи, хотя производительность может сильно отличаться:
План с дополнительными столбцами
Сначала возьмем лишние столбцы, которые не завершаются в разумные сроки:
Интересные особенности:
- Вершина в узле 0 ограничивает число возвращаемых строк до 100. Он также устанавливает цель для оптимизатора, поэтому все, что находится под ним в плане, выбрано для быстрого возврата первых 100 строк.
- Сканирование на узле 4 находит строки из таблицы, где значение
Start_Time
не равно нулю, State
равно 3 или 4 и Operation_Type
является одним из перечисленных значений. Таблица полностью сканируется один раз, причем каждая строка проверяется на соответствие предикатам. Только строки, которые проходят все тесты, передаются в сортировку. По оценкам оптимизатора, 38 283 строки будут соответствовать требованиям.
- Сортировка на узле 3 использует все строки из сканирования на узле 4 и сортирует их в порядке
Start_Time DESC
. Это последний порядок представления, запрошенный запросом.
- Оптимизатор оценивает, что 93 строки (фактически 93.2791) должны быть прочитаны из сортировки, чтобы весь план возвратил 100 строк (учитывая ожидаемый эффект объединения).
- Ожидается, что объединение вложенных циклов на узле 2 выполнит свой внутренний ввод (нижняя ветвь) 94 раза (на самом деле 94,2791). Дополнительная строка требуется для обмена параллельным остановом в узле 1 по техническим причинам.
- Сканирование на узле 5 полностью сканирует таблицу на каждой итерации. Он находит строки, которые
Start_Time
не State
равны NULL и равны 3 или 4. Предполагается, что на каждой итерации будет получено 400 875 строк. За 94,2791 итераций общее количество строк составляет почти 38 миллионов.
- Соединение с вложенными циклами на узле 2 также применяет предикаты объединения. Он проверяет, что
Operation_Type
совпадает, что Start_Time
из узла 4 меньше, чем Start_Time
из узла 5, что Start_Time
из узла 5 меньше, чем Finish_Time
из узла 4, и что эти два Id
значения не совпадают.
- Сборные потоки (остановка параллельного обмена) в узле 1 объединяют упорядоченные потоки из каждого потока, пока не будет создано 100 строк. Сохраняющий порядок характер слияния между несколькими потоками - это то, что требует дополнительной строки, упомянутой на шаге 5.
Большая неэффективность очевидно на шагах 6 и 7 выше. Полное сканирование таблицы в узле 5 для каждой итерации может быть даже незначительным, если это происходит только 94 раза, как предсказывает оптимизатор. Набор сравнений ~ 38 миллионов на строку в узле 2 также является большой стоимостью.
Важно отметить, что оценка цели в строке 93/94 также, скорее всего, будет неправильной, поскольку она зависит от распределения значений. Оптимизатор предполагает равномерное распределение при отсутствии более подробной информации. Говоря простым языком, это означает, что если ожидается, что 1% строк в таблице будет соответствовать требованиям, оптимизатор полагает, что для поиска 1 подходящей строки необходимо прочитать 100 строк.
Если вы выполнили этот запрос до завершения (что может занять очень много времени), вы, скорее всего, обнаружите, что из сортировки нужно было прочитать более 93/94 строк, чтобы в итоге получить 100 строк. В худшем случае 100-й ряд будет найден с использованием последнего ряда из сортировки. Если предположить, что оценка оптимизатора на узле 4 верна, это означает, что сканирование выполняется на узле 5 38 284 раза, что в общей сложности составляет около 15 миллиардов строк. Это может быть больше, если оценки сканирования также отключены.
Этот план выполнения также включает предупреждение об отсутствующем индексе:
/*
The Query Processor estimates that implementing the following index
could improve the query cost by 72.7096%.
WARNING: This is only an estimate, and the Query Processor is making
this recommendation based solely upon analysis of this specific query.
It has not considered the resulting index size, or its workload-wide
impact, including its impact on INSERT, UPDATE, DELETE performance.
These factors should be taken into account before creating this index.
*/
CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[Batch_Tasks_Queue] ([Operation_Type],[State],[Start_Time])
INCLUDE ([Id],[Parameters])
Оптимизатор предупреждает вас о том, что добавление индекса в таблицу повысит производительность.
План без лишних столбцов
По сути, это тот же план, что и предыдущий, с добавлением катушки индекса на узле 6 и фильтра на узле 5. Важными отличиями являются:
- Буфер индекса на узле 6 является готовой буферизацией. Он охотно использует результат сканирования под ним и создает временный индекс с ключом
Operation_Type
и Start_Time
, Id
в качестве неключевого столбца.
- Объединение вложенных циклов на узле 2 теперь является индексным соединением. Нет предикаты оцениваются здесь, вместо этого за итерацию текущих значений
Operation_Type
, Start_Time
, Finish_Time
и Id
из проверки на узле 4 передаются на внутренней стороне ветвь , как внешние ссылки.
- Сканирование на узле 7 выполняется только один раз.
- Буфер индекса на узле 6 ищет строки из временного индекса, где
Operation_Type
совпадает с текущим внешним значением ссылки, и Start_Time
находится в диапазоне, определенном внешними Start_Time
и Finish_Time
внешними ссылками.
- Фильтр на узле 5 проверяет
Id
значения в пуле индексов на предмет неравенства с текущим внешним эталонным значением Id
.
Ключевые улучшения:
- Сканирование внутренней стороны выполняется только один раз
- Временный индекс на (
Operation_Type
, Start_Time
) с Id
включенным столбцом позволяет объединять вложенные циклы индекса. Индекс используется для поиска совпадающих строк на каждой итерации, а не для сканирования всей таблицы каждый раз.
Как и прежде, оптимизатор включает предупреждение об отсутствующем индексе:
/*
The Query Processor estimates that implementing the following index
could improve the query cost by 24.1475%.
WARNING: This is only an estimate, and the Query Processor is making
this recommendation based solely upon analysis of this specific query.
It has not considered the resulting index size, or its workload-wide
impact, including its impact on INSERT, UPDATE, DELETE performance.
These factors should be taken into account before creating this index.
*/
CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[Batch_Tasks_Queue] ([State],[Start_Time])
INCLUDE ([Id],[Operation_Type])
GO
Вывод
План без дополнительных столбцов быстрее, потому что оптимизатор решил создать временный индекс для вас.
План с дополнительными столбцами сделает создание временного индекса более дорогим. [Parameters
Колонка] есть nvarchar(2000)
, который хотел бы добавить до 4000 байт для каждой строки индекса. Дополнительные затраты достаточны, чтобы убедить оптимизатора в том, что построение временного индекса при каждом выполнении не окупится.
В обоих случаях оптимизатор предупреждает, что постоянный индекс будет лучшим решением. Идеальная структура индекса зависит от вашей более широкой рабочей нагрузки. Для этого конкретного запроса предложенные индексы являются разумной отправной точкой, но вы должны понимать преимущества и затраты.
Рекомендация
Широкий диапазон возможных индексов будет полезным для этого запроса. Важным выводом является то, что нужен какой-то некластеризованный индекс. Исходя из представленной информации, разумным показателем на мой взгляд будет:
CREATE NONCLUSTERED INDEX i1
ON dbo.Batch_Tasks_Queue (Start_Time DESC)
INCLUDE (Operation_Type, [State], Finish_Time);
Я также хотел бы немного лучше организовать запрос и отложить поиск широких [Parameters]
столбцов в кластеризованном индексе до тех пор, пока не будут найдены первые 100 строк (используя Id
в качестве ключа):
SELECT TOP (100)
BTQ1.id,
BTQ2.id,
BTQ3.[Parameters],
BTQ4.[Parameters]
FROM dbo.Batch_Tasks_Queue AS BTQ1
JOIN dbo.Batch_Tasks_Queue AS BTQ2 WITH (FORCESEEK)
ON BTQ2.Operation_Type = BTQ1.Operation_Type
AND BTQ2.Start_Time > BTQ1.Start_Time
AND BTQ2.Start_Time < BTQ1.Finish_Time
AND BTQ2.id != BTQ1.id
-- Look up the [Parameters] values
JOIN dbo.Batch_Tasks_Queue AS BTQ3
ON BTQ3.Id = BTQ1.Id
JOIN dbo.Batch_Tasks_Queue AS BTQ4
ON BTQ4.Id = BTQ2.Id
WHERE
BTQ1.[State] IN (3, 4)
AND BTQ2.[State] IN (3, 4)
AND BTQ1.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
AND BTQ2.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
-- These predicates are not strictly needed
AND BTQ1.Start_Time IS NOT NULL
AND BTQ2.Start_Time IS NOT NULL
ORDER BY
BTQ1.Start_Time DESC;
Если [Parameters]
столбцы не нужны, запрос можно упростить до:
SELECT TOP (100)
BTQ1.id,
BTQ2.id
FROM dbo.Batch_Tasks_Queue AS BTQ1
JOIN dbo.Batch_Tasks_Queue AS BTQ2 WITH (FORCESEEK)
ON BTQ2.Operation_Type = BTQ1.Operation_Type
AND BTQ2.Start_Time > BTQ1.Start_Time
AND BTQ2.Start_Time < BTQ1.Finish_Time
AND BTQ2.id != BTQ1.id
WHERE
BTQ1.[State] IN (3, 4)
AND BTQ2.[State] IN (3, 4)
AND BTQ1.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
AND BTQ2.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
AND BTQ1.Start_Time IS NOT NULL
AND BTQ2.Start_Time IS NOT NULL
ORDER BY
BTQ1.Start_Time DESC;
FORCESEEK
Подсказка , чтобы помочь гарантировать , что оптимизатор выбирает индексированные вложенные циклы планирование (есть стоимостный соблазн для оптимизатора , чтобы выбрать хеш или (много-много) слиянием в противном случае, который , как правило , не хорошо работать с этим типом запрос на практике. Оба результата приводят к большим остаткам: много элементов в каждом сегменте в случае хэша и много перемоток для объединения).
альтернатива
Если бы запрос (включая его конкретные значения) был особенно важен для производительности чтения, я бы рассмотрел два отфильтрованных индекса:
CREATE NONCLUSTERED INDEX i1
ON dbo.Batch_Tasks_Queue (Start_Time DESC)
INCLUDE (Operation_Type, [State], Finish_Time)
WHERE
Start_Time IS NOT NULL
AND [State] IN (3, 4)
AND Operation_Type <> 23
AND Operation_Type <> 24
AND Operation_Type <> 25
AND Operation_Type <> 26
AND Operation_Type <> 27
AND Operation_Type <> 28
AND Operation_Type <> 30;
CREATE NONCLUSTERED INDEX i2
ON dbo.Batch_Tasks_Queue (Operation_Type, [State], Start_Time)
WHERE
Start_Time IS NOT NULL
AND [State] IN (3, 4)
AND Operation_Type <> 23
AND Operation_Type <> 24
AND Operation_Type <> 25
AND Operation_Type <> 26
AND Operation_Type <> 27
AND Operation_Type <> 28
AND Operation_Type <> 30;
Для запроса, который не нуждается в [Parameters]
столбце, предполагаемый план с использованием отфильтрованных индексов:
Сканирование индекса автоматически возвращает все подходящие строки без оценки каких-либо дополнительных предикатов. Для каждой итерации соединения с вложенными циклами индекса поиск индекса выполняет две операции поиска:
- Префикс поиска совпадает с
Operation_Type
и State
= 3, а затем ищет диапазон Start_Time
значений, остаточный предикат Id
неравенства.
- Префикс поиска соответствует
Operation_Type
и State
= 4, затем ищет диапазон Start_Time
значений, остаточный предикат Id
неравенства.
Там, где необходим [Parameters]
столбец, план запроса просто добавляет не более 100 одиночных поисков для каждой таблицы:
В заключение, вам следует рассмотреть возможность использования встроенных стандартных целочисленных типов вместо того, numeric
где это применимо.