У меня очень важное, очень медленное представление, которое включает в себя некоторые действительно уродливые условия, подобные этому, в предложении where. Мне также известно, что объединения - это грубые и медленные объединения varchar(13)
вместо целочисленных идентификаторов, но я хотел бы улучшить простой запрос ниже, который использует это представление:
CREATE VIEW [dbo].[vwReallySlowView] AS
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.tblItemtran AS I
INNER JOIN -- booking_no = varchar(13)
dbo.tblbookings AS B ON B.booking_no = I.booking_no_v32 -- string inner-join
INNER JOIN -- product_code = varchar(13)
dbo.tblInvmas AS M ON I.product_code_v42 = M.product_code -- string inner-join
WHERE (I.trans_type_v41 NOT IN (2, 3, 7, 18, 19, 20, 21, 12, 13, 22)) AND (I.trans_type_v41 NOT IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) OR
(I.trans_type_v41 NOT IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) AND (B.BookingProgressStatus = 1) OR
(I.trans_type_v41 IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) AND (I.QtyCheckedOut = 0) OR
(I.trans_type_v41 IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) AND (I.QtyCheckedOut > 0) AND (I.trans_qty - (I.QtyCheckedOut - I.QtyReturned) > 0)
Это представление обычно используется так:
select * from vwReallySlowView
where product_code_v42 = 'LIGHTBULB100W' -- find "100 watt lightbulb" rows
Когда я запускаю его, я получаю этот элемент плана выполнения стоимостью от 20 до 80% от общей стоимости пакета с предикатом, CONVERT_IMPLICIT( .... &(4))
показывающим, что он, похоже, очень медленно выполняет bitwise boolean tests
подобные действия (I.ibitfield & 4 = 0)
.
Я не являюсь экспертом по MS SQL или по типу работы администратора баз данных, так как большую часть времени я не являюсь разработчиком программного обеспечения, отличного от SQL. Но я подозреваю, что такие побитовые комбинации - плохая идея, и что было бы лучше иметь дискретные логические поля.
Могу ли я каким-то образом улучшить этот индекс, чтобы лучше обрабатывать это представление, не меняя схему (которая уже используется в тысячах местоположений), или я должен изменить базовую таблицу, в которой несколько логических значений упакованы в целое число bit_field_v41
, чтобы решить эту проблему ?
Вот мой кластерный индекс, tblItemtran
который сканируется в этом плане выполнения:
-- goal: speed up select * from vwReallySlowView where productcode = 'X'
CREATE CLUSTERED INDEX [idxtblItemTranProductCodeAndTransType] ON [dbo].[tblItemtran]
(
[product_code_v42] ASC, -- varchar(13)
[trans_type_v41] ASC -- int
)WITH ( PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF,
IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON)
ON [PRIMARY]
Вот план выполнения для одного из других продуктов, который приводит к 27% -ной стоимости для этого CONVERT_IMPLICIT
предиката. Обновление Обратите внимание, что в этом случае мой худший узел теперь "соответствует хешу" для an inner join
, что стоит 34%. Я считаю, что это цена, которую я не могу избежать, если не смогу избежать объединения строк, которые в настоящее время я не могу избавиться от. Обе INNER JOIN
операции в представлении выше относятся к varchar(13)
полям.
Увеличено в правом нижнем углу:
Весь план выполнения в виде .sqlplan доступен на skydrive. Это изображение просто визуальный обзор. Нажмите здесь, чтобы увидеть изображение само по себе.
Обновление опубликовано весь план выполнения. Кажется, я не могу понять, какая product_code
ценность была патологически плохой, но один из способов сделать это - select count(*) from view
вместо одного продукта. Но продукты, которые используются только в 5% записей в базовой таблице или менее, по-видимому, показывают гораздо более низкие затраты на CONVERT_IMPLICIT
операцию. Если бы я собирался исправить SQL здесь, я думаю, что я бы взял грубое WHERE
предложение в представлении, вычислил и сохранил результат этого гигантского условия where-условия в виде битового поля «IncludeMeInTheView» в базовой таблице. , Presto, проблема решена, верно?
источник
product_code
значение, которое я использовал для генерации этого патологического87%
случая с моими данными. Изображения теперь показывают27%
. Опять же, извинения за путаницу из-за моих правок.Ответы:
Вы не должны слишком полагаться на процентные доли затрат в планах выполнения. Это всегда предполагаемые затраты , даже в планах после выполнения с «фактическими» числами для таких вещей, как количество строк. Сметные расходы основаны на модели, которая работает достаточно хорошо для той цели, для которой она предназначена: дать оптимизатору возможность выбирать между различными подходящими планами выполнения для одного и того же запроса. Информация о затратах интересна и является важным фактором, но она редко должна быть основным показателем для настройки запросов. Интерпретация информации плана выполнения требует более широкого представления представленных данных.
Оператор поиска кластерного индекса 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
это хитрость, предотвращающая автопараметризацию, противоречие устраняется оптимизатором и не отображается в план запроса):Представляет интерес производительность этого запроса с холодным кешем данных, поскольку упреждающее чтение будет зависеть от фрагментации таблицы (кластеризованного индекса). Ключ кластеризации для этой таблицы допускает фрагментацию, поэтому может быть важно регулярно поддерживать (реорганизовывать или перестраивать) этот индекс и использовать соответствующий
FILLFACTOR
для предоставления места для новых строк между окнами обслуживания индекса.Я выполнил тест влияния фрагментации на упреждающее чтение, используя образцы данных, сгенерированные с помощью генератора данных SQL . При использовании того же количества строк в таблице, как показано в плане запроса вопроса, кластеризованный индекс с высокой степенью фрагментации привел к тому,
SELECT * FROM view
что после этого потребовалось 15 секундDBCC DROPCLEANBUFFERS
. Тот же тест в тех же условиях со вновь перестроенным кластерным индексом в таблице ItemTrans завершился за 3 секунды.Если данные таблицы, как правило, полностью находятся в кеше, проблема фрагментации будет гораздо менее важной. Но даже при низкой фрагментации широкие строки таблицы могут означать, что число логических и физических чтений намного выше, чем можно было ожидать. Вы также можете поэкспериментировать с добавлением и удалением явного,
CONVERT
чтобы подтвердить мое ожидание, что проблема неявного преобразования здесь не важна, за исключением случаев нарушения правил.Более конкретно, это приблизительное количество строк, оставленных оператором поиска. Оценка времени оптимизации составляет 165 строк, но 4226 были получены во время выполнения. Я вернусь к этому вопросу позже, но главная причина расхождений заключается в том, что оптимизатору остаточный предикат (включающий побитовое И) очень сложно предсказать - фактически он прибегает к угадыванию.
Оператор фильтра
Здесь я показываю предикат фильтра в основном, чтобы проиллюстрировать, как эти два
NOT IN
списка объединяются, упрощаются, а затем расширяются, а также чтобы дать ссылку для последующего обсуждения совпадения хеша. Тестовый запрос от поиска может быть расширен, чтобы включить его эффекты и определить влияние оператора Filter на производительность:Оператор Compute Scalar в плане определяет следующее выражение (само вычисление откладывается до тех пор, пока результат не потребуется последующему оператору):
Оператор хэш-матча
Выполнение объединения для символьных типов данных не является причиной высокой оценочной стоимости этого оператора. Во всплывающей подсказке 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
). Мы не можем создавать статистику по выражению напрямую, поэтому мы должны сначала создать вычисляемый столбец для выражения битового поля, а затем создать статистику по нескольким столбцам вручную:Текстовое определение вычисляемого столбца должно в точности соответствовать тексту в определении представления для используемой статистики, поэтому исправление представления для устранения неявного преобразования должно быть выполнено одновременно, и необходимо обеспечить соответствие текста.
Многостолбцовая статистика должна приводить к гораздо лучшим оценкам, значительно снижая вероятность того, что оператор совпадения хэшей будет использовать рекурсивный сброс или алгоритм спасения. Добавление вычисляемого столбца (который является операцией только для метаданных и не занимает места в таблице, поскольку она не помечена
PERSISTED
) и статистика по нескольким столбцам - мое лучшее предположение при первом решении.При решении проблем с производительностью запросов важно измерять такие вещи, как истекшее время, загрузка ЦП, логическое чтение, физическое чтение, типы ожидания и продолжительность ... и так далее. Также может быть полезно запустить части запроса отдельно для проверки предполагаемых причин, как показано выше.
В некоторых средах, где представление данных с точностью до секунды не важно, может быть полезно запускать фоновый процесс, который материализует все представление в таблицу снимков время от времени. Эта таблица является обычной базовой таблицей и может быть проиндексирована для запросов на чтение, не беспокоясь о влиянии на производительность обновления.
Просмотр индексации
Не поддавайтесь искушению индексировать исходный вид напрямую. Производительность чтения будет удивительно высокой (один поиск по индексу представления), но (в этом случае) все проблемы с производительностью в существующих планах запросов будут перенесены в запросы, которые изменяют любой из столбцов таблицы, на которые есть ссылки в представлении. На запросы, которые изменяют строки базовой таблицы, это действительно очень сильно повлияет.
Расширенное решение с частичным индексированным представлением
Для этого конкретного запроса существует частичное решение с индексированным представлением, которое корректирует оценки количества элементов и удаляет остатки фильтра и проб, но оно основано на некоторых предположениях о данных (в основном это мое предположение относительно схемы) и требует экспертной реализации, особенно в отношении подходящих индексы для поддержки планов обслуживания индексированного представления. Я делюсь приведенным ниже кодом для интереса, я не предлагаю вам реализовать его без очень тщательного анализа и тестирования.
Существующее представление настроено для использования индексированного представления выше:
Пример запроса и план выполнения:
В новом плане у совпадения хеш-функции нет остаточного предиката , нет сложного фильтра , нет остаточного предиката при поиске в индексированном представлении, и оценки количества элементов являются точными.
В качестве примера того, как будут затронуты планы вставки / обновления / удаления, это план для вставки в таблицу ItemTrans:
Выделенный раздел является новым и необходим для ведения индексированного представления. Буферная таблица воспроизводит вставленные строки базовой таблицы для обслуживания индексированного представления. Каждая строка присоединяется к таблице заказов с использованием поиска по кластерному индексу, затем фильтр применяет
WHERE
предикаты сложного предложения, чтобы увидеть, нужно ли добавить строку в представление. Если это так, вставка выполняется в кластеризованный индекс представления.Тот же
SELECT * FROM view
тест, выполненный ранее, завершился через 150 мс с индексированным представлением на месте.И последнее: я заметил, что ваш сервер 2008 R2 все еще работает на RTM. Это не исправит ваши проблемы с производительностью, но Service Pack 2 для 2008 R2 был доступен с июля 2012 года, и есть много веских причин, чтобы как можно более актуально использовать пакеты обновления.
источник