Чому вибір усіх отриманих стовпців цього запиту швидший, ніж вибір одного стовпця, який мене цікавить?


13

У мене є запит, коли використання select *не тільки набагато менше читає, але й використовує значно менший час процесора, ніж використання select c.Foo.

Це запит:

select top 1000 c.ID
from ATable a
    join BTable b on b.OrderKey = a.OrderKey and b.ClientId = a.ClientId
    join CTable c on c.OrderId = b.OrderId and c.ShipKey = a.ShipKey
where (a.NextAnalysisDate is null or a.NextAnalysisDate < @dateCutOff)
    and b.IsVoided = 0
    and c.ComplianceStatus in (3, 5)
    and c.ShipmentStatus in (1, 5, 6)
order by a.LastAnalyzedDate

У цьому закінчено 2473658 логічних показань, в основному в таблиці B. У ньому було використано 26 562 ЦП і тривало 7 965.

Це генерований план запитів:

План із вибору значення одного стовпця На PasteThePlan: https://www.brentozar.com/pastetheplan/?id=BJAp2mQIQ

Коли я переходжу c.IDна *, запит закінчується 107,049 логічними читаннями, досить рівномірно розподіленими між усіма трьома таблицями. Він використовував 4266 ЦП і тривав 1147.

Це генерований план запитів:

План з вибору всіх значень На PasteThePlan: https://www.brentozar.com/pastetheplan/?id=SyZYn7QUQ

Я намагався використовувати підказки, запропоновані Джо Оббіше, з такими результатами:
select c.IDбез підказки: https://www.brentozar.com/pastetheplan/?id=SJfBdOELm
select c.ID з підказкою: https://www.brentozar.com/pastetheplan/ ? id = B1W ___ N87
select * без підказки: https://www.brentozar.com/pastetheplan/?id=HJ6qddEIm
select * із підказкою: https://www.brentozar.com/pastetheplan/?id=rJhhudNIQ

Використання OPTION(LOOP JOIN)підказки з " select c.IDдрайвом" різко зменшило кількість читань порівняно з версією без натяку, але це все ще робить приблизно 4 рази кількість прочитаних select *запитів без будь-яких підказок. Додавання OPTION(RECOMPILE, HASH JOIN)в select *запит зробив це виконати набагато гірше , ніж все інше я намагався.

Після оновлення статистики на таблицях та їх індексах за WITH FULLSCANдопомогою select c.IDзапит працює набагато швидше:
select c.IDперед оновленням: https://www.brentozar.com/pastetheplan/?id=SkiYoOEUm
select * перед оновленням: https://www.brentozar.com/ pastetheplan /? id = ryrvodEUX
select c.ID після оновлення: https://www.brentozar.com/pastetheplan/?id=B1MRoO487
select * після оновлення: https://www.brentozar.com/pastetheplan/?id=Hk7si_V8m

select *як і раніше перевищує select c.IDпоказник загальної тривалості та загального читання ( select *має приблизно половину прочитаних), але він використовує більше процесора. Загалом вони набагато ближче, ніж до оновлення, проте плани все ж відрізняються.

Така ж поведінка спостерігається і в 2016 році, що працює в режимі сумісності 2014 року, і в 2014 році. Що може пояснити розбіжність між двома планами? Чи може бути, що "правильні" індекси не створені? Чи може статистика трохи застаріла?

Я намагався переміщувати предикати до ONчастини з'єднання різними способами, але план запитів кожен раз однаковий.

Після відновлення індексу

Я відновив усі індекси на трьох таблицях, задіяних у запиті. c.IDяк і раніше робить найбільше читань (понад удвічі більше *), але використання процесора становить приблизно половину *версії. c.IDВерсію також потрапило на даний TempDb сортування ATable:
c.ID: https://www.brentozar.com/pastetheplan/?id=HyHIeDO87
* : https://www.brentozar.com/pastetheplan/?id=rJ4deDOIQ

Я також спробував змусити його працювати без паралелізму, і це дало мені найкращий запит: https://www.brentozar.com/pastetheplan/?id=SJn9-vuLX

Я помічаю кількість виконання операторів ПІСЛЯ великого пошуку індексу, який виконує замовлення, виконаного лише 1000 разів у однопотоковій версії, але значно більше в паралелізованій версії, між 2622 та 4315 виконанням різних операторів.

Відповіді:


4

Це правда, що вибір більше стовпців означає, що SQL серверу, можливо, доведеться працювати більше, щоб отримати запитувані результати запиту. Якщо оптимізатору запитів вдалося скласти ідеальний план запитів для обох запитів, тоді було б розумно розраховувати наSELECT *запит запускається довше, ніж запит, який вибирає всі стовпці з усіх таблиць. Ви спостерігали навпаки для вашої пари запитів. Потрібно бути обережним при порівнянні витрат, але повільний запит має загальну оціночну вартість 1090,08 оптимізаторських одиниць, а швидкий запит має загальну оціночну вартість 6823,11 оптимізаторських одиниць. У цьому випадку можна сказати, що оптимізатор робить погану роботу з оцінкою загальних витрат на запити. Він обрав інший план для вашого запиту SELECT *, і очікував, що план буде дорожчим, але це було не так. Цей тип невідповідності може статися з багатьох причин, і однією з найпоширеніших причин є проблеми з оцінкою кардинальності. Операційні витрати значною мірою визначаються оцінками кардинальності. Якщо оцінка кардинальності у ключовій точці плану є неточною, то загальна вартість плану може не відображати реальність. Це важка спрощення, але я сподіваюся, що це буде корисно для розуміння того, що тут відбувається.

Почнемо з обговорення, чому SELECT *запит може бути дорожчим, ніж вибір одного стовпця. SELECT *Запит може перетворити деякі покривають індекси в noncovering індексів, що може означати , що оптимізатор повинен зробити аддитивную роботу , щоб отримати всі стовпці, необхідні або , можливо , доведеться читати з великим індексом.SELECT *може також призвести до збільшення проміжних наборів результатів, які потрібно обробити під час виконання запиту. Ви можете бачити це в дії, переглядаючи орієнтовні розміри рядків в обох запитах. У швидкому запиті розміри рядків варіюються від 664 байт до 3019 байт. У повільному запиті розміри рядків варіюються від 19 до 36 байт. Блокування операторів, таких як сортування або складання хешів, матиме більш високі витрати на дані з більшим розміром рядків, оскільки SQL Server знає, що дорожче сортувати більший обсяг даних або перетворити їх у хеш-таблицю.

Оглядаючи швидкий запит, оптимізатор підраховує, що йому потрібно виконати 2,4 мільйона індексу Database1.Schema1.Object5.Index3. Саме звідси походить більша частина плану. І все-таки фактичний план показує, що лише 1332 пошукових покажчиків індексу було здійснено на цьому операторі. Якщо порівнювати фактичні з оцінними рядками для зовнішніх частин цих циклів приєднується, ви побачите великі відмінності. Оптимізатор вважає, що для пошуку перших 1000 рядків, необхідних для результатів запиту, знадобиться ще багато пошукових покажчиків. Ось чому запит має порівняно високий план витрат, але закінчується так швидко: оператор, який, за прогнозами, був найдорожчим, зробив менше 0,1% очікуваної роботи.

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

Вам може бути корисно додавати підказки до запитів до обох версій запиту, щоб змусити план запитів, пов’язаний з іншою версією. Підказки на запити можуть бути хорошим інструментом, щоб з’ясувати, чому оптимізатор зробив деякі з своїх виборів. Якщо ви додасте OPTION (RECOMPILE, HASH JOIN)до SELECT *запиту, я думаю, ви побачите подібний план запитів до запиту приєднання хеш. Я також очікую, що витрати на запит будуть значно вищими для плану приєднання хешу, оскільки розміри ваших рядків набагато більше. Отже, тому запит не було обрано для SELECT *запиту хеш-об'єднання . Якщо ви додасте OPTION (LOOP JOIN)до запиту, який вибирає лише один стовпець, я думаю, ви побачите план запитів, подібний до запиту дляSELECT *запит. У цьому випадку зменшення розміру рядка не повинно мати значного впливу на загальну вартість запиту. Ви можете пропустити ключові пошуку, але це невеликий відсоток від орієнтовної вартості.

Підводячи підсумок, я очікую, що більші розміри рядків, необхідні для задоволення SELECT *запиту, підштовхнуть оптимізатор до плану з'єднання циклу замість плану хеш-об’єднання. План з'єднання циклу коштує вище, ніж повинен бути пов'язаний із проблемами оцінки кардинальності. Зменшення розмірів рядків шляхом вибору лише одного стовпця значно знижує вартість плану хеш-об’єднання, але, ймовірно, не матиме великого ефекту на вартість плану з’єднання циклу, тож у результаті ви отримаєте менш ефективний план приєднання хешу. Важко сказати більше, ніж це для анонімізованого плану.


Дякую вам за вашу розгорнуту та інформативну відповідь. Я спробував додати запропоновані вами підказки. Це зробило select c.IDзапит набагато швидше, але він все ще виконує додаткову роботу, яку робить select *запит, без підказки.
Л. Міллер

2

Затхла статистика, безумовно, може змусити оптимізатора вибрати поганий метод пошуку даних. Ви пробували робити UPDATE STATISTICS ... WITH FULLSCANчи робити повноцінний REBUILDпоказник? Спробуйте це і подивіться, чи допоможе це.

ОНОВЛЕННЯ

Відповідно до оновлення з ОП:

Після оновлення статистики на таблицях та їх індексах за WITH FULLSCANдопомогою select c.IDзапит працює набагато швидше

Отже, якщо єдиною дією було вжито UPDATE STATISTICS, то спробуйте зробити індекс REBUILD(не REORGANIZE), як я бачив, що допомога з розрахунковими підрахунками рядків там, де UPDATE STATISTICSі індекс REORGANIZEне робив.


Мені вдалося отримати всі індекси на трьох таблицях, які були відновлені протягом вихідних, і оновив свою публікацію, щоб відобразити ці результати.
Л. Міллер

-1
  1. Чи можете ви включити індексні скрипти?
  2. Ви усунули можливі проблеми із "нюханням параметрів"? https://www.mssqltips.com/sqlservertip/3257/different-approaches-to-correct-sql-server-parameter-sniffing/
  3. Я вважаю, що ця методика є корисною в деяких випадках:
    а) перепишіть кожну таблицю як підзапит, дотримуючись цих правил:
    b) ВИБІРИ - покладіть спочатку стовпці об'єднання
    c) ПРЕДИКАТИ - перейдіть у відповідні підзапити
    d) ORDER BY - перейдіть у свої відповідні підзапити, відсортуйте за ПЕРШИМИ СПОСОБАМИ
    e) Додайте запит на обгортку для остаточного сортування та виберіть.

Ідея полягає у попередньому сортуванні стовпців приєднання всередині кожного підселектора, розміщуючи стовпці об’єднання спочатку у кожному списку вибору.

Ось що я маю на увазі….

SELECT ... wrapper query
FROM
(
    SELECT ...
    FROM
        (SELECT ClientID, ShipKey, NextAnalysisDate
         FROM ATABLE
         WHERE (a.NextAnalysisDate is null or a.NextAnalysisDate < @dateCutOff) -- Predicates
         ORDER BY OrderKey, ClientID, LastAnalyzedDate  ---- Pre-sort the join columns
        ) as a
        JOIN 
        (SELECT OrderKey, ClientID, OrderID, IsVoided
         FROM BTABLE
         WHERE IsVoided = 0             ---- Include all predicates
         ORDER BY OrderKey, OrderID, IsVoided       ---- Pre-sort the join columns
        ) as b ON b.OrderKey = a.OrderKey and b.ClientId = a.ClientId
        JOIN
        (SELECT OrderID, ShipKey, ComplianceStatus, ShipmentStatus, ID
         FROM CTABLE
         WHERE ComplianceStatus in (3, 5)       ---- Include all predicates
             AND ShipmentStatus in (1, 5, 6)        ---- Include all predicates
         ORDER BY OrderID, ShipKey          ---- Pre-sort the join columns
        ) as c ON c.OrderId = b.OrderId and c.ShipKey = a.ShipKey
) as d
ORDER BY d.LastAnalyzedDate

1
1. Я спробую додати сценарії DDL-індексу до оригінальної публікації, що може зайняти деякий час, щоб "очистити" їх. 2. Я перевірив цю можливість, як очистивши кеш плану перед запуском, так і замінивши параметр прив'язки фактичним значенням. 3. Я спробував це, але ORDER BYнедійсний у підзапиті без TOP, FORXML тощо. Я спробував це без ORDER BYпунктів, але це був той самий план.
Л. Міллер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.