Вы не должны слишком полагаться на процентные доли затрат в планах выполнения. Это всегда предполагаемые затраты , даже в планах после выполнения с «фактическими» числами для таких вещей, как количество строк. Сметные расходы основаны на модели, которая работает достаточно хорошо для той цели, для которой она предназначена: дать оптимизатору возможность выбирать между различными подходящими планами выполнения для одного и того же запроса. Информация о затратах интересна и является важным фактором, но она редко должна быть основным показателем для настройки запросов. Интерпретация информации плана выполнения требует более широкого представления представленных данных.
Оператор поиска кластерного индекса ItemTran
Этот оператор действительно две операции в одной. Сначала операция поиска по индексу находит все строки, соответствующие предикату product_code_v42 = 'M10BOLT'
, затем к каждой строке применяется остаточный предикат bit_field_v41 & 4 = 0
. Существует неявное преобразование bit_field_v41
из его базового типа ( tinyint
или smallint
) в integer
.
Преобразование происходит потому, что оператор побитового И (&) требует, чтобы оба операнда были одного типа. Неявный тип константного значения '4' является целым числом, а правила приоритета типа данных означают, что значение bit_field_v41
поля с более низким приоритетом преобразуется.
Проблема (такая, как она есть) легко может быть исправлена записью предиката как, то bit_field_v41 & CONVERT(tinyint, 4) = 0
есть постоянное значение имеет более низкий приоритет и преобразуется (во время свертывания константы), а не в значение столбца. Если bit_field_v41
это tinyint
преобразование не происходит вообще. Аналогично, CONVERT(smallint, 4)
может быть использовано, если bit_field_v41
есть smallint
. Тем не менее, преобразование не является проблемой производительности в этом случае, но все же рекомендуется использовать сопоставление типов и избегать неявных преобразований, где это возможно.
Основная часть сметных затрат на поиск сводится к размеру базовой таблицы. Хотя ключ кластеризованного индекса сам по себе достаточно узок, размер каждой строки велик. Определение для таблицы не дано, но только столбцы, используемые в представлении, добавляют значительную ширину строки. Поскольку кластерный индекс включает в себя все столбцы, расстояние между ключами кластерного индекса является шириной строки , а не шириной ключей индекса . Использование суффиксов версий в некоторых столбцах предполагает, что в реальной таблице еще больше столбцов для предыдущих версий.
Если посмотреть на столбцы поиска, остаточного предиката и вывода, то производительность этого оператора можно проверить изолированно, создав эквивалентный запрос ( 1 <> 2
это хитрость, предотвращающая автопараметризацию, противоречие устраняется оптимизатором и не отображается в план запроса):
SELECT
it.booking_no_v32,
it.QtyCheckedOut,
it.QtyReturned,
it.Trans_qty,
it.trans_type_v41
FROM dbo.tblItemTran AS it
WHERE
1 <> 2
AND it.product_code_v42 = 'M10BOLT'
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0;
Представляет интерес производительность этого запроса с холодным кешем данных, поскольку упреждающее чтение будет зависеть от фрагментации таблицы (кластеризованного индекса). Ключ кластеризации для этой таблицы допускает фрагментацию, поэтому может быть важно регулярно поддерживать (реорганизовывать или перестраивать) этот индекс и использовать соответствующий FILLFACTOR
для предоставления места для новых строк между окнами обслуживания индекса.
Я выполнил тест влияния фрагментации на упреждающее чтение, используя образцы данных, сгенерированные с помощью генератора данных SQL . При использовании того же количества строк в таблице, как показано в плане запроса вопроса, кластеризованный индекс с высокой степенью фрагментации привел к тому, SELECT * FROM view
что после этого потребовалось 15 секунд DBCC DROPCLEANBUFFERS
. Тот же тест в тех же условиях со вновь перестроенным кластерным индексом в таблице ItemTrans завершился за 3 секунды.
Если данные таблицы, как правило, полностью находятся в кеше, проблема фрагментации будет гораздо менее важной. Но даже при низкой фрагментации широкие строки таблицы могут означать, что число логических и физических чтений намного выше, чем можно было ожидать. Вы также можете поэкспериментировать с добавлением и удалением явного, CONVERT
чтобы подтвердить мое ожидание, что проблема неявного преобразования здесь не важна, за исключением случаев нарушения правил.
Более конкретно, это приблизительное количество строк, оставленных оператором поиска. Оценка времени оптимизации составляет 165 строк, но 4226 были получены во время выполнения. Я вернусь к этому вопросу позже, но главная причина расхождений заключается в том, что оптимизатору остаточный предикат (включающий побитовое И) очень сложно предсказать - фактически он прибегает к угадыванию.
Оператор фильтра
Здесь я показываю предикат фильтра в основном, чтобы проиллюстрировать, как эти два NOT IN
списка объединяются, упрощаются, а затем расширяются, а также чтобы дать ссылку для последующего обсуждения совпадения хеша. Тестовый запрос от поиска может быть расширен, чтобы включить его эффекты и определить влияние оператора Filter на производительность:
SELECT
it.booking_no_v32,
it.trans_type_v41,
it.Trans_qty,
it.QtyReturned,
it.QtyCheckedOut
FROM dbo.tblItemTran AS it
WHERE
it.product_code_v42 = 'M10BOLT'
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND
(
(
it.trans_type_v41 NOT IN (2, 3, 6, 7, 18, 19, 20, 21, 12, 13, 22)
AND it.trans_type_v41 NOT IN (6, 7)
)
OR
(
it.trans_type_v41 NOT IN (6, 7)
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.QtyCheckedOut = 0
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.QtyCheckedOut > 0
AND it.trans_qty - (it.QtyCheckedOut - it.QtyReturned) > 0
)
);
Оператор Compute Scalar в плане определяет следующее выражение (само вычисление откладывается до тех пор, пока результат не потребуется последующему оператору):
[Expr1016] = (trans_qty - (QtyCheckedOut - QtyReturned))
Оператор хэш-матча
Выполнение объединения для символьных типов данных не является причиной высокой оценочной стоимости этого оператора. Во всплывающей подсказке SSMS отображается только запись «Зонд хэш-ключей», но важные детали находятся в окне свойств SSMS.
Оператор Hash Match создает хеш-таблицу, используя значения booking_no_v32
столбца (Hash Keys Build) из таблицы ItemTran, а затем проверяет совпадения, используя booking_no
столбец (Hash Keys Probe) из таблицы Bookings. Во всплывающей подсказке SSMS также обычно отображается «Остаток зонда», но текст слишком длинный для всплывающей подсказки и просто пропускается.
Остаток зондирования аналогичен остатку, замеченному после поиска индекса ранее; остаточный предикат оценивается во всех строках, которые соответствуют хешу, чтобы определить, должна ли строка быть передана родительскому оператору. Поиск совпадений хеша в хорошо сбалансированной хеш-таблице чрезвычайно быстр, но применение сложного остаточного предиката к каждой строке, которая соответствует, сравнительно медленное по сравнению. Во всплывающей подсказке «Hash Match» в Plan Explorer отображаются подробные сведения, включая выражение остаточного зонда:
Остаточный предикат является сложным и включает проверку статуса выполнения бронирования, теперь этот столбец доступен из таблицы заказов. Всплывающая подсказка также показывает такое же несоответствие между оценочным и фактическим количеством строк, которые мы видели ранее при поиске по индексу. Может показаться странным, что большая часть фильтрации выполняется дважды, но оптимизатор настроен оптимистично. Он не ожидает, что части фильтра, которые могут быть сдвинуты вниз по плану от остатка зонда, устранят какие-либо строки (оценки количества строк одинаковы до и после фильтра), но оптимизатор знает, что в этом может быть ошибка. Возможность ранней фильтрации строк (снижение стоимости хеш-соединения) стоит небольших затрат на дополнительный фильтр. Весь фильтр нельзя сдвинуть вниз, потому что он включает в себя тест по столбцу из таблицы заказов, но большинство из них может быть.
Недооценка количества строк является проблемой для оператора Hash Match, поскольку объем памяти, зарезервированный для хеш-таблицы, основан на приблизительном количестве строк. Если объем памяти слишком мал для размера хеш-таблицы, требуемой во время выполнения (из-за большего числа строк), хеш-таблица рекурсивно перетекает в физическое хранилище tempdb , что часто приводит к очень низкой производительности. В худшем случае механизм выполнения останавливает рекурсивное разливание хэш-блоков и использует очень медленныйалгоритм спасения. Распределение хэша (рекурсивное или спасение) является наиболее вероятной причиной проблем с производительностью, описанных в вопросе (не в столбцах соединения символьного типа или неявных преобразованиях). Основной причиной может быть то, что сервер зарезервировал слишком мало памяти для запроса на основании неверного подсчета количества строк (количества элементов).
К сожалению, до SQL Server 2012 в плане выполнения не было указаний на то, что операция хеширования превысила свое выделение памяти (которая не может динамически увеличиваться после резервирования до начала выполнения, даже если на сервере есть много свободной памяти) и должна была перетекать в Tempdb. Можно отслеживать класс событий хэш-предупреждений с помощью Profiler, но может быть сложно соотнести предупреждения с конкретным запросом.
Исправлять проблемы
Три проблемы - это фрагментация, сложный остаток зонда в операторе совпадения хеша и неправильная оценка количества элементов, полученная в результате угадывания при поиске по индексу.
Рекомендуемое решение
Проверьте фрагментацию и исправьте ее при необходимости, составив график обслуживания, чтобы убедиться, что индекс остается приемлемо организованным. Обычный способ исправить оценку количества элементов - предоставить статистику. В этом случае оптимизатору нужна статистика для комбинации ( product_code_v42
, bitfield_v41 & 4 = 0
). Мы не можем создавать статистику по выражению напрямую, поэтому мы должны сначала создать вычисляемый столбец для выражения битового поля, а затем создать статистику по нескольким столбцам вручную:
ALTER TABLE dbo.tblItemTran
ADD Bit3 AS bit_field_v41 & CONVERT(tinyint, 4);
CREATE STATISTICS [stats dbo.ItemTran (product_code_v42, Bit3)]
ON dbo.tblItemTran (product_code_v42, Bit3);
Текстовое определение вычисляемого столбца должно в точности соответствовать тексту в определении представления для используемой статистики, поэтому исправление представления для устранения неявного преобразования должно быть выполнено одновременно, и необходимо обеспечить соответствие текста.
Многостолбцовая статистика должна приводить к гораздо лучшим оценкам, значительно снижая вероятность того, что оператор совпадения хэшей будет использовать рекурсивный сброс или алгоритм спасения. Добавление вычисляемого столбца (который является операцией только для метаданных и не занимает места в таблице, поскольку она не помечена PERSISTED
) и статистика по нескольким столбцам - мое лучшее предположение при первом решении.
При решении проблем с производительностью запросов важно измерять такие вещи, как истекшее время, загрузка ЦП, логическое чтение, физическое чтение, типы ожидания и продолжительность ... и так далее. Также может быть полезно запустить части запроса отдельно для проверки предполагаемых причин, как показано выше.
В некоторых средах, где представление данных с точностью до секунды не важно, может быть полезно запускать фоновый процесс, который материализует все представление в таблицу снимков время от времени. Эта таблица является обычной базовой таблицей и может быть проиндексирована для запросов на чтение, не беспокоясь о влиянии на производительность обновления.
Просмотр индексации
Не поддавайтесь искушению индексировать исходный вид напрямую. Производительность чтения будет удивительно высокой (один поиск по индексу представления), но (в этом случае) все проблемы с производительностью в существующих планах запросов будут перенесены в запросы, которые изменяют любой из столбцов таблицы, на которые есть ссылки в представлении. На запросы, которые изменяют строки базовой таблицы, это действительно очень сильно повлияет.
Расширенное решение с частичным индексированным представлением
Для этого конкретного запроса существует частичное решение с индексированным представлением, которое корректирует оценки количества элементов и удаляет остатки фильтра и проб, но оно основано на некоторых предположениях о данных (в основном это мое предположение относительно схемы) и требует экспертной реализации, особенно в отношении подходящих индексы для поддержки планов обслуживания индексированного представления. Я делюсь приведенным ниже кодом для интереса, я не предлагаю вам реализовать его без очень тщательного анализа и тестирования.
-- Indexed view to optimize the main view
CREATE VIEW dbo.V1
WITH SCHEMABINDING
AS
SELECT
it.ID,
it.product_code_v42,
it.trans_type_v41,
it.booking_no_v32,
it.Trans_qty,
it.QtyReturned,
it.QtyCheckedOut,
it.QtyReserved,
it.bit_field_v41,
it.prep_on,
it.From_locn,
it.Trans_to_locn,
it.PDate,
it.FirstDate,
it.PTimeH,
it.PTimeM,
it.RetnDate,
it.BookDate,
it.TimeBookedH,
it.TimeBookedM,
it.TimeBookedS,
it.del_time_hour,
it.del_time_min,
it.return_to_locn,
it.return_time_hour,
it.return_time_min,
it.AssignTo,
it.AssignType,
it.InRack
FROM dbo.tblItemTran AS it
JOIN dbo.tblBookings AS tb ON
tb.booking_no = it.booking_no_v32
WHERE
(
it.trans_type_v41 NOT IN (2, 3, 7, 18, 19, 20, 21, 12, 13, 22)
AND it.trans_type_v41 NOT IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
)
OR
(
it.trans_type_v41 NOT IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND tb.BookingProgressStatus = 1
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND it.QtyCheckedOut = 0
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND it.QtyCheckedOut > 0
AND it.trans_qty - (it.QtyCheckedOut - it.QtyReturned) > 0
);
GO
CREATE UNIQUE CLUSTERED INDEX cuq ON dbo.V1 (product_code_v42, ID);
GO
Существующее представление настроено для использования индексированного представления выше:
CREATE VIEW [dbo].[vwReallySlowView2]
AS
SELECT
I.booking_no_v32 AS bkno,
I.trans_type_v41 AS trantype,
B.Assigned_to_v61 AS Assignbk,
B.order_date AS dateo,
B.HourBooked AS HBooked,
B.MinBooked AS MBooked,
B.SecBooked AS SBooked,
I.prep_on AS Pon,
I.From_locn AS Flocn,
I.Trans_to_locn AS TTlocn,
CASE I.prep_on
WHEN 'Y' THEN I.PDate
ELSE I.FirstDate
END AS PrDate,
I.PTimeH AS PrTimeH,
I.PTimeM AS PrTimeM,
CASE
WHEN I.RetnDate < I.FirstDate
THEN I.FirstDate
ELSE I.RetnDate
END AS RDatev,
I.bit_field_v41 AS bitField,
I.FirstDate AS FDatev,
I.BookDate AS DBooked,
I.TimeBookedH AS TBookH,
I.TimeBookedM AS TBookM,
I.TimeBookedS AS TBookS,
I.del_time_hour AS dth,
I.del_time_min AS dtm,
I.return_to_locn AS rtlocn,
I.return_time_hour AS rth,
I.return_time_min AS rtm,
CASE
WHEN
I.Trans_type_v41 IN (6, 7)
AND I.Trans_qty < I.QtyCheckedOut
THEN 0
WHEN
I.Trans_type_v41 IN (6, 7)
AND I.Trans_qty >= I.QtyCheckedOut
THEN I.Trans_Qty - I.QtyCheckedOut
ELSE
I.trans_qty
END AS trqty,
CASE
WHEN I.Trans_type_v41 IN (6, 7)
THEN 0
ELSE I.QtyCheckedOut
END AS MyQtycheckedout,
CASE
WHEN I.Trans_type_v41 IN (6, 7)
THEN 0
ELSE I.QtyReturned
END AS retqty,
I.ID,
B.BookingProgressStatus AS bkProg,
I.product_code_v42,
I.return_to_locn,
I.AssignTo,
I.AssignType,
I.QtyReserved,
B.DeprepOn,
CASE B.DeprepOn
WHEN 1 THEN B.DeprepDateTime
ELSE I.RetnDate
END AS DeprepDateTime,
I.InRack
FROM dbo.V1 AS I WITH (NOEXPAND)
JOIN dbo.tblbookings AS B ON
B.booking_no = I.booking_no_v32
JOIN dbo.tblInvmas AS M ON
I.product_code_v42 = M.product_code;
Пример запроса и план выполнения:
SELECT
vrsv.*
FROM dbo.vwReallySlowView2 AS vrsv
WHERE vrsv.product_code_v42 = 'M10BOLT';
В новом плане у совпадения хеш-функции нет остаточного предиката , нет сложного фильтра , нет остаточного предиката при поиске в индексированном представлении, и оценки количества элементов являются точными.
В качестве примера того, как будут затронуты планы вставки / обновления / удаления, это план для вставки в таблицу ItemTrans:
Выделенный раздел является новым и необходим для ведения индексированного представления. Буферная таблица воспроизводит вставленные строки базовой таблицы для обслуживания индексированного представления. Каждая строка присоединяется к таблице заказов с использованием поиска по кластерному индексу, затем фильтр применяет WHERE
предикаты сложного предложения, чтобы увидеть, нужно ли добавить строку в представление. Если это так, вставка выполняется в кластеризованный индекс представления.
Тот же SELECT * FROM view
тест, выполненный ранее, завершился через 150 мс с индексированным представлением на месте.
И последнее: я заметил, что ваш сервер 2008 R2 все еще работает на RTM. Это не исправит ваши проблемы с производительностью, но Service Pack 2 для 2008 R2 был доступен с июля 2012 года, и есть много веских причин, чтобы как можно более актуально использовать пакеты обновления.