Запрос не отвечает при добавлении двух столбцов

9

Когда я добавляю два столбца в мой выбор, запрос не отвечает. Тип столбца nvarchar(2000). Это немного необычно.

  • Версия SQL Server - 2014.
  • Существует только один первичный индекс.
  • Всего записей всего 1000 строк.

Вот план выполнения до ( XML showplan ):

введите описание изображения здесь

План выполнения после ( XML showplan ):

введите описание изображения здесь

Вот запрос:

select top(100)
  Batch_Tasks_Queue.id,
  btq.id,
  Batch_Tasks_Queue.[Parameters], -- this field
  btq.[Parameters]  -- and this field
from
        Batch_Tasks_Queue with(nolock)
    inner join  Batch_Tasks_Queue btq with(nolock)  on  Batch_Tasks_Queue.Start_Time < btq.Start_Time
                            and btq.Start_Time < Batch_Tasks_Queue.Finish_Time
                            and Batch_Tasks_Queue.id <> btq.id                            
                            and btq.Start_Time is not null
                            and btq.State in (3, 4)                          
where
    Batch_Tasks_Queue.Start_Time is not null      
    and Batch_Tasks_Queue.State in (3, 4)
    and Batch_Tasks_Queue.Operation_Type = btq.Operation_Type
    and Batch_Tasks_Queue.Operation_Type not in (23, 24, 25, 26, 27, 28, 30)

order by
    Batch_Tasks_Queue.Start_Time desc

Общее количество результатов составляет 17 строк. Грязные данные (подсказка nolock) не важны.

Вот структура таблицы:

CREATE TABLE [dbo].[Batch_Tasks_Queue](
    [Id] [int] NOT NULL,
    [OBJ_VERSION] [numeric](8, 0) NOT NULL,
    [Operation_Type] [numeric](2, 0) NULL,
    [Request_Time] [datetime] NOT NULL,
    [Description] [varchar](1000) NULL,
    [State] [numeric](1, 0) NOT NULL,
    [Start_Time] [datetime] NULL,
    [Finish_Time] [datetime] NULL,
    [Parameters] [nvarchar](2000) NULL,
    [Response] [nvarchar](max) NULL,
    [Billing_UserId] [int] NOT NULL,
    [Planned_Start_Time] [datetime] NULL,
    [Input_FileId] [uniqueidentifier] NULL,
    [Output_FileId] [uniqueidentifier] NULL,
    [PRIORITY] [numeric](2, 0) NULL,
    [EXECUTE_SEQ] [numeric](2, 0) NULL,
    [View_Access] [numeric](1, 0) NULL,
    [Seeing] [numeric](1, 0) NULL,
 CONSTRAINT [PKBachTskQ] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [Batch_Tasks_QueueData]
) ON [Batch_Tasks_QueueData] TEXTIMAGE_ON [Batch_Tasks_QueueData]
GO    
SET ANSI_PADDING OFF
GO
ALTER TABLE [dbo].[Batch_Tasks_Queue]  WITH NOCHECK ADD  CONSTRAINT [FK0_BtchTskQ_BlngUsr] FOREIGN KEY([Billing_UserId])
REFERENCES [dbo].[BILLING_USER] ([ID])
GO
ALTER TABLE [dbo].[Batch_Tasks_Queue] CHECK CONSTRAINT [FK0_BtchTskQ_BlngUsr]
GO
Хамид Фатхи
источник
Обсуждение этого вопроса было перенесено в этот чат .
Пол Уайт 9

Ответы:

15

Резюме

Основными проблемами являются:

  • Выбор плана оптимизатора предполагает равномерное распределение значений.
  • Отсутствие подходящих индексов означает:
    • Сканирование таблицы - единственный вариант.
    • Объединение является наивными вложенными циклы, а не индекс вложенных циклов. В наивном соединении предикаты объединения оцениваются в соединении, а не выталкиваются вниз по внутренней стороне объединения.

подробности

Эти два плана в основном очень похожи, хотя производительность может сильно отличаться:

План с дополнительными столбцами

Сначала возьмем лишние столбцы, которые не завершаются в разумные сроки:

Медленный план

Интересные особенности:

  1. Вершина в узле 0 ограничивает число возвращаемых строк до 100. Он также устанавливает цель для оптимизатора, поэтому все, что находится под ним в плане, выбрано для быстрого возврата первых 100 строк.
  2. Сканирование на узле 4 находит строки из таблицы, где значение Start_Timeне равно нулю, Stateравно 3 или 4 и Operation_Typeявляется одним из перечисленных значений. Таблица полностью сканируется один раз, причем каждая строка проверяется на соответствие предикатам. Только строки, которые проходят все тесты, передаются в сортировку. По оценкам оптимизатора, 38 283 строки будут соответствовать требованиям.
  3. Сортировка на узле 3 использует все строки из сканирования на узле 4 и сортирует их в порядке Start_Time DESC. Это последний порядок представления, запрошенный запросом.
  4. Оптимизатор оценивает, что 93 строки (фактически 93.2791) должны быть прочитаны из сортировки, чтобы весь план возвратил 100 строк (учитывая ожидаемый эффект объединения).
  5. Ожидается, что объединение вложенных циклов на узле 2 выполнит свой внутренний ввод (нижняя ветвь) 94 раза (на самом деле 94,2791). Дополнительная строка требуется для обмена параллельным остановом в узле 1 по техническим причинам.
  6. Сканирование на узле 5 полностью сканирует таблицу на каждой итерации. Он находит строки, которые Start_Timeне Stateравны NULL и равны 3 или 4. Предполагается, что на каждой итерации будет получено 400 875 строк. За 94,2791 итераций общее количество строк составляет почти 38 миллионов.
  7. Соединение с вложенными циклами на узле 2 также применяет предикаты объединения. Он проверяет, что Operation_Typeсовпадает, что Start_Timeиз узла 4 меньше, чем Start_Timeиз узла 5, что Start_Timeиз узла 5 меньше, чем Finish_Timeиз узла 4, и что эти два Idзначения не совпадают.
  8. Сборные потоки (остановка параллельного обмена) в узле 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. Важными отличиями являются:

  1. Буфер индекса на узле 6 является готовой буферизацией. Он охотно использует результат сканирования под ним и создает временный индекс с ключом Operation_Typeи Start_Time, Idв качестве неключевого столбца.
  2. Объединение вложенных циклов на узле 2 теперь является индексным соединением. Нет предикаты оцениваются здесь, вместо этого за итерацию текущих значений Operation_Type, Start_Time, Finish_Timeи Idиз проверки на узле 4 передаются на внутренней стороне ветвь , как внешние ссылки.
  3. Сканирование на узле 7 выполняется только один раз.
  4. Буфер индекса на узле 6 ищет строки из временного индекса, где Operation_Typeсовпадает с текущим внешним значением ссылки, и Start_Timeнаходится в диапазоне, определенном внешними Start_Timeи Finish_Timeвнешними ссылками.
  5. Фильтр на узле 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]столбце, предполагаемый план с использованием отфильтрованных индексов:

Простой отфильтрованный индексный план

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

  1. Префикс поиска совпадает с Operation_Typeи State= 3, а затем ищет диапазон Start_Timeзначений, остаточный предикат Idнеравенства.
  2. Префикс поиска соответствует Operation_Typeи State= 4, затем ищет диапазон Start_Timeзначений, остаточный предикат Idнеравенства.

Там, где необходим [Parameters]столбец, план запроса просто добавляет не более 100 одиночных поисков для каждой таблицы:

Отфильтрованный план индекса с дополнительными столбцами

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

Пол Уайт 9
источник
-2

Пожалуйста, создайте следующий индекс:

create index Batch_Tasks_Queue_ix_Start_Time on Batch_Tasks_Queue(Start_Time);
Дэвид Markודו Марковиц
источник
Ты пробовал это? Я провел несколько тестов, и это имело огромное значение.
Дэвид דודו Марковиц