Чому додавання TOP 1 значно погіршує продуктивність?


39

У мене досить простий запит

SELECT TOP 1 dc.DOCUMENT_ID,
        dc.COPIES,
        dc.REQUESTOR,
        dc.D_ID,
        cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
JOIN CORRESPONDENCE_JOURNAL cj
    ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
WHERE dc.QUEUE_DATE <= GETDATE()
  AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER

Це дає мені жахливу виставу (як ніколи не заважав чекати, коли вона закінчиться). План запитів виглядає приблизно так:

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

Однак якщо я видаляю, TOP 1я отримую такий вигляд плану, і він працює за 1-2 секунди:

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

Виправте ПК та індексацію нижче.

Те, що TOP 1змінений план запитів мене не дивує, я просто здивований, що це робить його набагато гірше.

Примітка. Я прочитав результати цього допису і зрозумів поняття Row Goalтощо. Мені цікаво, як я можу йти про зміну запиту, щоб він використовував кращий план. Наразі я скидаю дані в темп-таблицю, а потім витягую з неї перший рядок. Мені цікаво, чи є кращий метод.

Змінити Для людей, які читають це після факту, ось кілька додаткових відомостей.

  • Черга документа - PK / CI - це D_ID і має ~ 5k рядків.
  • Кореспонденція_Журнал - PK / CI - FILE_NUMBER, CORRESPONDENCE_ID і має ~ 1,4 млн рядків.

Коли я починав, інших показників не було. Я опинився в одному з журналу Cor лістаence_Journal (Document_Id, File_Number)


1
Чи є у вас обмеження для зовнішніх ключів, яке примушує DOCUMENT_IDвідносини між двома таблицями (або чи має кожен запис у CORRESPONDENCE_JOURNALвідповідний запис DOCUMENT_QUEUE)?
Даніель Хатмахер

Відповіді:


28

Спробуйте змусити приєднати хеш *

SELECT TOP 1 
       dc.DOCUMENT_ID,
       dc.COPIES,
       dc.REQUESTOR,
       dc.D_ID,
       cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
INNER HASH JOIN CORRESPONDENCE_JOURNAL cj
        ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
       AND dc.QUEUE_DATE <= GETDATE()
       AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER

Оптимізатор, напевно, думав, що цикл буде кращим з топ-1, і це має сенс, але насправді це не працює. Тут лише здогадка, але, можливо, передбачувана вартість цієї котушки була вимкнена - вона використовує TEMPDB - у вас може бути низькопродуктивний TEMPDB.


* Будьте обережні з підказками про приєднання , тому що вони змушують порядок доступу до таблиці таблиць відповідати письмовому порядку таблиць у запиті (так само, як якщо OPTION (FORCE ORDER)б було вказано). З посилання на документацію:

Екстракт BOL

Це може не спричинити небажаних ефектів у прикладі, але в цілому це може бути дуже добре. FORCE ORDER(мається на увазі або явно) - дуже потужний натяк, що виходить за рамки виконання наказу; це запобігає застосуванню широкого спектру методів оптимізатора, включаючи часткове агрегування та переупорядкування.

OPTION (HASH JOIN) Запиту натяк може бути менш нав'язливою в відповідних випадках, так як це не має на увазі FORCE ORDER. Однак це стосується всіх об'єднань у запиті. Інші рішення доступні.


1
Схоже, правильна відповідь і єдиною різницею між нею та більш простим планом був додатковий Сортування на фронті.
Кеннет Фішер

3
Не впевнений, що мені подобається ця відповідь. Підказки про приєднання дуже інвазивні. Спершу слід спробувати деякі прості зміни індексації, наприклад, індекс у стовпці дати.
usr

@usr Це просте приєднання ПК, яке працює менше однієї секунди. Тут досить безпечна ставка.
папараццо

4
Примушуючи приєднання хешу, ви змушуєте сканувати велику таблицю. Є кращі варіанти.
Роб Фарлі

30

Оскільки ви маєте правильний план з ORDER BY, можливо, ви могли б просто скотити власного TOPоператора?

SELECT DOCUMENT_ID, COPIES, REQUESTOR, D_ID, FILE_NUMBER
FROM (
    SELECT dc.DOCUMENT_ID,
           dc.COPIES,
           dc.REQUESTOR,
           dc.D_ID,
           cj.FILE_NUMBER,
           ROW_NUMBER() OVER (ORDER BY cj.FILE_NUMBER) AS _rownum
    FROM DOCUMENT_QUEUE dc
    INNER JOIN CORRESPONDENCE_JOURNAL cj
        ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
    WHERE dc.QUEUE_DATE <= GETDATE()
      AND dc.PRINT_LOCATION = 2
) AS sub
WHERE _rownum=1;

На мою думку, план запитів для ROW_NUMBER()вищезазначеного повинен бути таким же, як якщо б у вас був ORDER BY. Тепер план запитів повинен мати сегмент, проект послідовності і, нарешті, оператор фільтра, решта має виглядати так само, як і ваш хороший план.


3
Насправді, хоча він дав топ-оператору (і купі інших матеріалів (проект послідовності, сегмент та сортування)), він все ще пройшов підсекунд. Я збираюся дати правильну відповідь на @frisbee, хоча, оскільки його було першим, і це простіше. Хоча чудова відповідь.
Кеннет Фішер

10
@KennethFisher, відповідь фрісбі простіша, але в способі кувалда забиває фінішний цвях просто, ніж стандартний каркасний молоточок. Це також піддається великому ризику, особливо якщо його залишити на місці для тривалого перевезення. Я б не використовував такі підказки, за винятком тестування чи, можливо, МОЖЕТЕ виключити бахрому.
Стів Манґямелі

@SteveMangiameli У цьому конкретному випадку є лише один приєднатись, тому ряд проблем піде. Мені відомо про ризики використання підказки (або підказки щодо запиту). Я просто думаю, що це виправдано в цьому випадку.
Кеннет Фішер

5
@KennethFisher Імо, головний ризик натяків на запити полягає в тому, що у міру зростання чи зміни ваших даних план запитів, який ви застосовуєте, може стати гіршим, ніж те, що система знайшла б самостійно. Ви вже бачили, як невелика помилка в плані може серйозно вплинути на результативність. Використання підказки у виробництві декларує: "Я знаю, що цей план завжди, завжди буде найкращим, тому що я так повністю розумію планувальник і як мої дані будуть вести себе протягом життя цього запиту у виробництві". Я ніколи не був такою впевненою у запиті.
jpmc26

29

Редагувати: +1 працює в цій ситуації, оскільки виявляється, що FILE_NUMBERце нульова версія рядка цілого числа. Кращим рішенням для рядків є додавання ''(порожній рядок), оскільки додавання значення може впливати на порядок або для чисел додавати щось, що є константою, але містить недетерміновану функцію, наприклад sign(rand()+1). Ідея "зламати сорт" все ще справедлива тут, якраз в тому, що мій метод не був ідеальним.

+1

Ні, я не маю на увазі, що я згоден ні з чим, я маю на увазі це як рішення. Якщо ви зміните свій запит, ORDER BY cj.FILE_NUMBER + 1тоді TOP 1засіб буде поводитися інакше.

Розумієте, якщо впорядкований запит встановлений для малого рядка, система намагатиметься споживати дані для того, щоб уникнути наявності оператора Сортування. Це також дозволить уникнути побудови хеш-таблиці, вважаючи, що, ймовірно, не потрібно робити занадто багато роботи, щоб знайти цей перший рядок. У вашому випадку це неправильно - з товщини цих стрілок, схоже, що для знаходження однієї відповідності потрібно споживати багато даних.

Товщина цих стрілок говорить про те, що ваша DOCUMENT_QUEUEтаблиця (DQ) набагато менша, ніж ваша CORRESPONDENCE_JOURNAL(CJ). І що найкращим планом насправді було б перевірити рядки DQ, поки не буде знайдено рядок CJ. Дійсно, саме це зробив Оптимізатор запитів (QO), якби у нього не було цього прискіпливого ORDER BY, це добре підтримується індексом покриття на CJ.

Отже, якщо ви ORDER BYповністю відмовилися , я очікую, що ви отримаєте план, який передбачає вкладений цикл, перебираючи рядки в DQ, шукаючи CJ, щоб переконатися, що рядок існує. І з TOP 1цим це припиниться після того, як буде виведено один ряд.

Але якщо вам дійсно потрібен перший рядок для FILE_NUMBERтого, то ви можете підманути систему, щоб ігнорувати той індекс, який здається (неправильно) таким корисним, виконуючи ORDER BY CJ.FILE_NUMBER+1- який, як ми знаємо, буде зберігати той самий порядок, що і раніше, але важливо QO не робить. QO зосередить увагу на тому, щоб викласти весь набір, щоб оператор Top N Sort був задоволений. Цей метод повинен створити план, який містить оператора «Обчислити скаляр» для визначення значення для замовлення та оператора «Сортування вгорі N» для отримання першого рядка. Але праворуч від них ви повинні побачити приємну вкладену петлю, що робить багато шукань на CJ. І краща продуктивність, ніж біг через велику таблицю рядків, яка не відповідає нічого в DQ.

Hash Match не обов'язково жахливий, але якщо набір рядків, які ви повертаєтеся з DQ, набагато менший, ніж CJ (як я б очікував, що це буде), то Hash Match буде сканувати набагато більше CJ ніж це потрібно.

Примітка. Я використовував +1 замість +0, оскільки оптимізатор запитів, ймовірно, визнає, що +0 нічого не змінює. Звичайно, те саме може застосуватись і до +1, якщо не зараз, то в якийсь момент майбутнього.


7

Я прочитав результати цієї публікації та зрозумів поняття цілі рядків тощо. Мені цікаво, як я можу йти про зміну запиту, щоб він використовував кращий план

Додавання OPTION (QUERYTRACEON 4138)вимикає ефект цілей рядків лише для цього запиту, не надто нав'язуючи остаточний план, і, ймовірно, буде найпростішим / найпрямим способом.

Якщо додавання цієї підказки надає помилку дозволу (потрібно для DBCC TRACEON), ви можете застосувати її за допомогою посібника щодо плану:

Використання QUERYTRACEONв плані посібників від спагеттідби

... або просто використовувати збережену процедуру:

Які дозволи QUERYTRACEONпотрібні? по Кендра Літтл


3

Новіші версії SQL Server пропонують різні (і, можливо, кращі) варіанти розгляду запитів, які отримують неоптимальну ефективність, коли оптимізатор може застосувати оптимізацію цілей рядків. SQL Server 2016 SP1 представив той DISABLE_OPTIMIZER_ROWGOAL USE HINTсамий ефект, що і прапор сліду 4138. Якщо ви не в цій версії, ви також можете скористатися OPTIMIZE FORпідказкою запиту, щоб отримати план запитів, призначений для повернення всіх рядків, а не лише 1. Запит нижче поверне ті самі результати, що і у запитанні, але це не буде створено з метою отримання лише 1 рядка.

DECLARE @top INT = 1;

SELECT TOP (@top) dc.DOCUMENT_ID,
        dc.COPIES,
        dc.REQUESTOR,
        dc.D_ID,
        cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
JOIN CORRESPONDENCE_JOURNAL cj
    ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
WHERE dc.QUEUE_DATE <= GETDATE()
  AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER
OPTION (OPTIMIZE FOR (@top = 987654321));

2

Оскільки ви займаєтесь TOP(1), я рекомендую зробити ORDER BYдетермінований для початку. Принаймні, це забезпечить функціонально передбачувані результати (завжди корисні для тесту регресії). Схоже, вам потрібно додати DC.D_IDі CJ.CORRESPONDENCE_IDдля цього.

Переглядаючи плани запитів, мені іноді здається, що спростити запит: можливо, заздалегідь виберіть усі відповідні рядки постійного струму в таблицю темпів, щоб усунути проблеми з оцінкою кардинальності на QUEUE_DATEі PRINT_LOCATION. Це має бути швидко, враховуючи низьку кількість рядків. Потім ви можете додати індекси до цієї тимчасової таблиці, не змінюючи постійну таблицю.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.