Не слід занадто сильно покладатися на відсоток витрат у планах виконання. Це завжди кошторисні витрати , навіть у планах після виконання з "фактичними" номерами для речей, таких як кількість рядків. Орієнтовні витрати ґрунтуються на моделі, яка працює досить добре з метою, для якої вона призначена: надання можливості оптимізатору вибирати між різними планами виконання кандидатів для одного запиту. Інформація про вартість цікава та важливий фактор, але вона рідко повинна бути основним показником для настройки запитів. Інтерпретація інформації плану виконання вимагає більш широкого перегляду представлених даних.
ItemTran Clustered Index Шукайте оператора
Цей оператор - це дійсно дві операції в одній. Спочатку операція пошуку індексу знаходить усі рядки, що відповідають предикату product_code_v42 = 'M10BOLT'
, потім до кожного рядка застосовано залишковий предикат bit_field_v41 & 4 = 0
. Існує неявна конверсія bit_field_v41
з його базового типу ( tinyint
або smallint
) вinteger
.
Перетворення відбувається тому, що побітовий оператор AND (&) вимагає, щоб обидва операнди були одного типу. Неявний тип постійного значення "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 Data Generator . Використовуючи ті ж підрахунки рядків таблиці, як показано в плані запитів запитання, сильно фрагментований кластерний індекс призвів до SELECT * FROM view
15 секунд післяDBCC DROPCLEANBUFFERS
. Той самий тест у тих самих умовах із щойно відремонтованим кластерним індексом у таблиці ItemTrans завершений за 3 секунди.
Якщо дані таблиці, як правило, повністю знаходяться в кеші, проблема фрагментації дуже менш важлива. Але навіть при низькій фрагментації широкі рядки таблиці можуть означати, що кількість логічних та фізичних зчитувань значно більша, ніж можна було очікувати. Ви також можете експериментувати з додаванням та видаленням явного, CONVERT
щоб підтвердити моє сподівання, що проблема неявного перетворення тут не важлива, за винятком випадків порушення кращої практики.
Більш суттєвим є приблизна кількість рядків, що залишають оператора пошуку. Оцінка часу оптимізації становить 165 рядків, але на час виконання було вироблено 4226. Я повернусь до цього пункту пізніше, але головна причина розбіжності полягає в тому, що вибірковість залишкового предиката (за участю побітових AND) дуже важко передбачити оптимізатору - адже він вдається до здогадок.
Оператор фільтру
Тут я показую предикат фільтрів здебільшого, щоб проілюструвати, як два NOT IN
списки поєднуються, спрощуються та потім розширюються, а також щоб подати посилання на наступне обговорення хеш-відповідності. Тестовий запит із пошуку можна розширити, щоб включити його ефекти та визначити вплив оператора фільтра на продуктивність:
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))
Оператор відповідності Hash
Виконання з'єднання типів символьних даних не є причиною високої оціночної вартості цього оператора. Підказка про SSMS показує лише запис зонду хеш-ключів, але важливі деталі містяться у вікні властивостей SSMS.
Оператор Hash Match будує хеш-таблицю, використовуючи значення booking_no_v32
стовпця (Hash Keys Build) з таблиці ItemTran, а потім зондує на відповідність, використовуючи booking_no
стовпець (Hash Keys Probe) з таблиці Bookings . Підказка SSMS також зазвичай відображатиме залишок зонда, але текст є занадто довгим для підказки і його просто опускають.
Залишковий зонд подібний до Залишкового, який спостерігається після пошуку індексу раніше; залишковий предикат оцінюється у всіх рядках, що відповідають хешу, щоб визначити, чи слід передавати рядок батьківському оператору. Пошук відповідних хеш-таблиць у добре збалансованій хеш-таблиці є надзвичайно швидким, але застосування складного залишкового предиката до кожного ряду, який відповідає, порівняно досить повільний. Підказка Hash Match в Провіднику плану показує деталі, включаючи залишковий вираз зонда:
Залишковий предикат є складним і включає перевірку стану бронювання тепер, коли сторона доступна з таблиці бронювання. Підказка також показує однакову невідповідність між підрахунковими та фактичними підрахунками рядків, поміченими раніше в індексі пошуку. Може здатися дивним, що велика частина фільтрації проводиться двічі, але це лише оптимізатор. Він не очікує, що частини фільтра, які можуть бути відсунуті вниз по плану від залишків зонда для усунення будь-яких рядків (оцінки кількості рядків є однаковими до і після фільтра), але оптимізатор знає, що це може бути не так. Шанс раннього фільтрування рядків (зменшення вартості приєднання хешу) коштує малих витрат на додатковий фільтр. Весь фільтр не можна відсунути вниз, оскільки він включає тест на стовпчик із таблиці бронювання, але більшість може бути.
Недооцінка кількості рядків є проблемою для оператора Hash Match, оскільки обсяг пам’яті, зарезервований для хеш-таблиці, заснований на передбачуваній кількості рядків. Якщо пам'ять занадто мала для розміру хеш-таблиці, необхідної під час виконання (через більшу кількість рядків) хеш-таблиця рекурсивно переливається на фізичне зберігання tempdb , що часто призводить до дуже низької продуктивності. У гіршому випадку двигун виконання припиняє рекурсивно розливати хеш-відра і вдається до a дуже повільноалгоритм порятунку Розлив хешу (рекурсивний або рятувальний) є найбільш ймовірною причиною проблем із ефективністю, викладених у запитанні (не стовпчики приєднання типу символів чи неявні перетворення). Першопричиною буде те, що сервер резервує занадто мало пам’яті для запиту, що базується на оцінці неправильного підрахунку рядків (кардинальності).
На жаль, перед SQL Server 2012 в плані виконання немає вказівки на те, що операція хешування перевищила розподіл пам’яті (яка не може динамічно зростати після резервування перед початком виконання, навіть якщо на сервері є маса вільної пам’яті) і довелося перелитись на tempdb. Можливо відстежувати клас подій Hash Warning за допомогою 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:
Виділений розділ новий та необхідний для обслуговування індексованого перегляду. Котушка таблиці відтворює вставлені рядки базової таблиці для обслуговування індексованого перегляду. Кожен рядок приєднується до таблиці бронювання за допомогою кластеризованого індексу search, тоді фільтр застосовує складні WHERE
предикативні пропозиції, щоб побачити, чи потрібно додавати рядок до перегляду. Якщо так, вкладений вкладок до кластерного індексу подання.
Той самий SELECT * FROM view
тест, виконаний раніше, виконаний за 150 мс, з індексованим видом на місці.
Заключна річ: я помічаю, що ваш сервер R2 2008 року все ще знаходиться на RTM. Це не виправить ваші проблеми з продуктивністю, але пакет оновлень 2 для 2008 року R2 доступний з липня 2012 року, і є безліч вагомих причин залишатися максимально актуальними з пакетами послуг.