Змініть запит, щоб покращити оцінки операторів


14

У мене є запит, який працює протягом прийнятного часу, але я хочу витіснити з нього максимальну ефективність.

Операція, яку я намагаюся вдосконалити, - це "Пошук індексу" праворуч від плану, від Вузла 17.

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

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

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

Хтось має якісь пропозиції щодо того, що ще я можу спробувати?

З повним планом та його деталями можна ознайомитись тут .

Неанонімізований план можна знайти тут.

Оновлення:

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

create procedure [dbo].[someProcedure] @asType int, @customAttrValIds idlist readonly
as
begin
    set nocount on;

    declare @dist_ca_id int;

    select *
    into #temp
    from @customAttrValIds
        where id is not null;

    select @dist_ca_id = count(distinct CustomAttrID) 
    from CustomAttributeValues c
        inner join #temp a on c.Id = a.id;

    select a.Id
        , a.AssortmentId 
    from Assortments a
        inner join AssortmentCustomAttributeValues acav
            on a.Id = acav.Assortment_Id
        inner join CustomAttributeValues cav 
            on cav.Id = acav.CustomAttributeValue_Id
    where a.AssortmentType = @asType
        and acav.CustomAttributeValue_Id in (select id from #temp)
    group by a.AssortmentId
        , a.Id
    having count(distinct cav.CustomAttrID) = @dist_ca_id
    option(recompile);

end

Відповіді:

  1. Чому непарні початкові імена у посиланні pasteThePlan?

    Відповідь : Тому що я використовував план анонімізації з Провідника плану Sentry Plan.

  2. Чому OPTION RECOMPILE?

    Відповідь : Тому що я можу дозволити собі перекомпіляції, щоб уникнути нюху параметрів (дані є / могла б перекоситися). Я протестував, і я задоволений планом, який оптимізатор створює під час використання OPTION RECOMPILE.

  3. WITH SCHEMABINDING?

    Відповідь : Я б дуже хотів цього уникнути, і використовував би його лише тоді, коли я маю індексований вигляд. У всякому разі, це системна функція ( COUNT()), тому тут немає ніякої користі SCHEMABINDING.

Відповіді на більш можливі запитання:

  1. Для чого я використовую INSERT INTO #temp FROM @customAttrributeValues?

    Відповідь : Оскільки я помітив і тепер знаю, що при використанні змінних, підключених до запиту, будь-які оцінки, які виходять з роботи зі змінною, завжди є 1. І я перевірив, вводячи дані в таблицю тимчасових тем, і Оцінене тоді дорівнює фактичним рядкам .

  2. Чому я користувався and acav.CustomAttributeValue_Id in (select id from #temp)?

    Відповідь : Я міг би замінити його приєднанням до #temp, але розробники дуже розгубилися і запропонували INваріант. Я не думаю, що це буде різницею навіть заміною і в будь-якому випадку, з цим немає жодних проблем.


Я б здогадався, що #tempстворення та використання буде проблемою для продуктивності, а не виграшу. Ви зберігаєте в неіндексованій таблиці лише один раз. Спробуйте видалити його повністю (і, можливо, змінити це in (select id from #temp)на existsпідзапит.
ypercubeᵀᴹ

@ ypercubeᵀᴹ Щоправда, приблизно на кілька менших сторінок, прочитаних за допомогою змінної замість таблиці темп.
Раду Георгіу

До речі, таблична змінна забезпечить правильну оцінку кількості рядків при використанні з Option (перекомпіляція) - але все ще не має детальної статистики, кардинальності тощо
TH

@TH Добре, я дивився у фактичному плані виконання оцінок, коли використовувався select id from @customAttrValIdsзамість, select id from #tempа передбачувана кількість рядків була 1для змінної та 3для #temp (яка відповідала фактичному # рядків). Ось чому я замінив @на #. І я РОБИТИ пам'ятаю розмову (від Brent O або Aaron Bertrand) , де вони сказали , що при використанні змінної TBL оцінка для цього завжди буде 1. А як поліпшення , щоб отримати кращі оцінки вони будуть використовувати тимчасову таблицю.
Radu Gheorghiu

@RaduGheorghiu Так, але в світі цих хлопців варіант (перекомпіляція) рідко є варіантом, і вони також віддають перевагу тимчасовим таблицям з інших поважних причин. Можливо, оцінка просто завжди неправильно відображається як 1, оскільки це змінює план, як видно тут: theboreddba.com/Categories/FunWithFlags/…
TH

Відповіді:


12

План був складений на екземплярі RTM SQL Server 2008 R2 RT (збірка 10.50.1600). Вам слід встановити Service Pack 3 (збірка 10.50.6000) з подальшими останніми виправленнями, щоб довести його до (поточної) останньої збірки 10.50.6542. Це важливо з кількох причин, включаючи безпеку, виправлення помилок та нові функції.

Оптимізація вбудовування параметрів

Відповідно до цього питання, SQL Server 2008 R2 RTM не підтримує оптимізацію вбудовування параметрів (PEO) для OPTION (RECOMPILE). Зараз ви оплачуєте витрати на перекомпіляції, не усвідомлюючи однієї з головних переваг.

Коли PEO доступний, SQL Server може використовувати буквальні значення, що зберігаються в локальних змінних та параметрах безпосередньо в плані запитів. Це може призвести до кардинальних спрощень та підвищення продуктивності. Про це можна отримати більше інформації в моїй статті « Параметри нюху», «Вбудовування» та «РЕКОМПЛІЙ» .

Хеш, сортування та обмін розливів

Вони відображаються в планах виконання лише тоді, коли запит був складений на SQL Server 2012 або новіших версіях. У попередніх версіях нам довелося стежити за розлиттями, поки запит виконувався за допомогою Profiler або Extended Events. Проливання завжди призводить до фізичного вводу / виводу до (і від) стійкого tempdb резервного зберігання , що може мати важливі наслідки для продуктивності, особливо якщо кількість розливу є великим або шлях вводу / виводу знаходиться під тиском.

У вашому плані виконання є два оператори Hash Match (Aggregate). Пам'ять, зарезервована для хеш-таблиці, заснована на оцінці вихідних рядків (іншими словами, вона пропорційна кількості груп, знайдених під час виконання). Надана пам'ять фіксується безпосередньо перед початком виконання і не може зростати під час виконання, незалежно від того, скільки вільної пам'яті має примірник. У наданому плані обидва оператори Hash Match (Aggregate) видають більше рядків, ніж очікував оптимізатор, і тому може виникнути розлив tempdb під час виконання.

У плані також є оператор Hash Match (Inner Join). Пам'ять, зарезервована для хеш-таблиці, заснована на оцінці для вхідних рядків на боці . Вхід зонда оцінює 847 399 рядків, але під час виконання зустрічаються 1223 636. Цей надлишок також може стати причиною розливу хешу.

Надлишковий агрегат

Hash Match (агрегат) у вузлі 8 виконує операцію групування (Assortment_Id, CustomAttrID), але вхідні рядки рівні вихідним рядкам:

Вузол 8 хеш-матчу (сукупний)

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

З огляду на те, що задіяні стовпці надходять з різних таблиць, важче, ніж зазвичай, передавати оптимізатору цю унікальну інформацію, тому можна уникнути зайвої операції групування та непотрібних обмінів.

Неефективний розподіл ниток

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

Ви можете вирішити цю проблему (для отримання розділення навколо кругової або широкомовної передачі), усунувши Розрізнення сортування у вузлі 13. Найпростіший спосіб зробити це створити кластеризований первинний ключ на #tempстолі та виконати окрему операцію під час завантаження таблиці:

CREATE TABLE #Temp
(
    id integer NOT NULL PRIMARY KEY CLUSTERED
);

INSERT #Temp
(
    id
)
SELECT DISTINCT
    CAV.id
FROM @customAttrValIds AS CAV
WHERE
    CAV.id IS NOT NULL;

Кешування тимчасової таблиці

Незважаючи на використання OPTION (RECOMPILE), SQL Server все ще може кешувати тимчасовий об'єкт таблиці та пов’язану з ним статистику між викликами процедури. Це, як правило, оптимізація ефективності роботи, але якщо тимчасова таблиця заповнена аналогічним обсягом даних про суміжні виклики процедури, перекомпільований план може базуватися на неправильній статистиці (кешований з попереднього виконання). Про це детально розповідають у моїх статтях, Тимчасові таблиці в збережених процедурах та пояснення тимчасових таблиць .

Щоб уникнути цього, використовуйте OPTION (RECOMPILE)разом із явним UPDATE STATISTICS #TempTableпісля заповнення тимчасової таблиці та перед цим посилання у запиті.

Перепишіть запит

Ця частина передбачає, що зміни в створенні #Tempтаблиці вже внесені.

Враховуючи витрати на можливі розсипання хешу та надлишковий агрегат (та навколишні біржі), він може заплатити за матеріалізацію набору у вузлі 10:

CREATE TABLE #Temp2
(
    CustomAttrID integer NOT NULL,
    Assortment_Id integer NOT NULL,
);

INSERT #Temp2
(
    Assortment_Id,
    CustomAttrID
)
SELECT
    ACAV.Assortment_Id,
    CAV.CustomAttrID
FROM #temp AS T
JOIN dbo.CustomAttributeValues AS CAV
    ON CAV.Id = T.id
JOIN dbo.AssortmentCustomAttributeValues AS ACAV
    ON T.id = ACAV.CustomAttributeValue_Id;

ALTER TABLE #Temp2
ADD CONSTRAINT PK_#Temp2_Assortment_Id_CustomAttrID
PRIMARY KEY CLUSTERED (Assortment_Id, CustomAttrID);

PRIMARY KEYДодаються в окремому кроці , щоб забезпечити побудову індексу має точну інформацію кардинальної і уникнути тимчасових статистичних таблиць кешування питання.

Ця матеріалізація, швидше за все, відбудеться в пам'яті (уникаючи tempdb I / O), якщо екземпляр має достатньо пам'яті. Це ще ймовірніше, коли ви оновите до SQL Server 2012 (SP1 CU10 / SP2 CU1 або новішої версії), що покращило поведінку Eager Write .

Ця дія дає оптимізатору точну інформацію про кардинальність проміжного набору, дозволяє йому створювати статистику та дозволяє заявляти (Assortment_Id, CustomAttrID)як ключову.

План для населення населення #Temp2повинен виглядати приблизно так (зверніть увагу на кластерне сканування індексів #Temp, без чіткого сортування, і обмін тепер використовує розділовий рядок з круглим числом):

# Населення Temp2

Якщо цей набір доступний, остаточний запит стає:

SELECT
    A.Id,
    A.AssortmentId
FROM
(
    SELECT
        T.Assortment_Id
    FROM #Temp2 AS T
    GROUP BY
        T.Assortment_Id
    HAVING
        COUNT_BIG(DISTINCT T.CustomAttrID) = @dist_ca_id
) AS DT
JOIN dbo.Assortments AS A
    ON A.Id = DT.Assortment_Id
WHERE
    A.AssortmentType = @asType
OPTION (RECOMPILE);

Ми могли б вручну переписати COUNT_BIG(DISTINCT...як простий COUNT_BIG(*), але з новою ключовою інформацією оптимізатор робить це для нас:

Остаточний план

Остаточний план може використовувати з'єднання циклу / хеш / злиття залежно від статистичної інформації про дані, до яких я не маю доступу. Ще одна невелика примітка: я припустив, що такий індекс CREATE [UNIQUE?] NONCLUSTERED INDEX IX_ ON dbo.Assortments (AssortmentType, Id, AssortmentId);існує.

У будь-якому випадку, важливим у фінальних планах є те, що оцінки повинні бути набагато кращими, а складна послідовність операцій групування зводиться до єдиного агрегату потоку (який не потребує пам’яті і тому не може розпливатися на диск).

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


9

Оцінки кардинальності вашого запиту насправді дуже хороші. Рідко можна отримати кількість оцінених рядків, щоб точно відповідати кількості фактичних рядків, особливо коли у вас є така кількість приєднань. Приєднатись до оцінок кардинальності - це складно для оптимізатора. Важливо відзначити, що кількість оцінених рядків для внутрішньої частини вкладеної петлі дорівнює виконанню цієї петлі. Отже, коли SQL Server каже, що 463869 рядків буде отримано індексом, шукайте реальну оцінку, в цьому випадку - кількість виконань (2) * 463869 = 927738, що не так далеко від фактичної кількості рядків, 1391608. Дивно, кількість оцінених рядків близька до ідеальної відразу після приєднання вкладеного циклу в ідентифікаторі вузла 10.

Погані оцінки кардинальності в основному є проблемою, коли оптимізатор запитів вибирає неправильний план або не надає достатньо пам'яті плану. Я не бачу розливів tempdb для цього плану, тому пам'ять виглядає нормально. Для вкладеного циклу приєднання, який ви викликаєте, у вас є невелика зовнішня таблиця та індексована внутрішня таблиця. Що з цим? Якщо бути точним, що ви очікуєте, що оптимізатор запитів тут може по-іншому?

Що стосується підвищення продуктивності, мені особливо виділяється те, що SQL Server використовує алгоритм хешування для розподілу паралельних рядків, в результаті чого всі вони знаходяться в одній нитці:

дисбаланс нитки

Як результат, один потік шукає всю роботу з індексом:

шукати дисбаланс нитки

Це означає, що ваш запит фактично не працює паралельно, поки оператор потоку перерозподілу не відповідає номеру вузла 9. Що ви, мабуть, хочете, - це круглий розділ роботів, так що кожен рядок закінчується в його власному потоці. Це дозволить двом потокам зробити індекс шукати ідентифікатор вузла 17. Якщо додати зайвий TOPоператор, ви зможете отримати круглий розділ роботів. Я можу додати тут деталі, якщо вам подобається.

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

Якщо ви не використовуєте прапорці слідів 4199 або 2301, ви можете їх розглянути. Trace flag 4199 пропонує широкий спектр виправлень оптимізатора, але вони можуть погіршити деякі навантаження. Прапор трасування 2301 змінює деякі припущення про кардинальність приєднання оптимізатора запитів і змушує його працювати. В обох випадках тестуйте ретельно, перш ніж їх вмикати.


-2

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

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


3
Існує дуже великий простір планів для цього запиту, з багатьма варіантами порядку приєднання та гніздування, паралелізму, локальної / глобальної агрегації тощо тощо. Більшість з яких впливатимуть на зміни отриманих статистичних даних (розподіл, а також необроблена кардинальність) у плані 10. Зауважте також, що підказки приєднання, як правило, слід уникати, оскільки вони надходять безшумно OPTION(FORCE ORDER), що не дозволяє оптимізатору впорядкувати з'єднання з текстової послідовності та багато інших оптимізацій.
Пол Білий 9

-12

Ви не збираєтеся вдосконалюватись від [некластеризованого] пошуку шукати індекс. Єдине, що краще, ніж шукати некластеризований індекс, - це пошук кластерних індексів.

Крім того, я був SQL DBA протягом останніх десяти років, і SQL розробник протягом п'яти років до цього, і на мій досвід надзвичайно рідко можна знайти вдосконалення запиту SQL, вивчаючи план виконання, який ви не могли ' не знайти інших способів. Основна причина для створення плану виконання полягає в тому, що він часто пропонує вам відсутні індекси, які ви можете додати для підвищення продуктивності.

Основні переваги в продуктивності полягатимуть у коригуванні самого SQL-запиту, якщо там є неефективність. Наприклад, пару місяців тому я отримав функцію SQL для запуску в 160 разів швидше, переписавши SELECT UNION SELECTстильову таблицю стилів для використання стандартного PIVOTоператора SQL .

insert into Variable1 values (?), (?), (?)


select *
    into Object1
    from Variable2
        where Column1 is not null;



select Variable3 = Function1(distinct Column2) 
    from Object2 Object3
        inner join Object1 Object4 on Object3.Column1 = Object4.Column1;



select Object4.Column1
        , Object4.Column3 
    from Object5 Object4
        inner join Object6 Object7
            on Object4.Column1 = Object7.Column4
        inner join Object2 Object8 
            on Object8.Column1 = Object7.Column5
    where Object4.Column6 = Variable4
        and Object7.Column5 in (select Column1 from Object1)
    group by Object4.Column3
        , Object4.Column1
    having Function1(distinct Object8.Column2) = Variable3
    option(recompile);

Отже, давайте подивимось, SELECT * INTOяк правило, менш ефективний, ніж стандарт INSERT Object1 (column list) SELECT column list. Тому я б це переписав. Далі, якщо Function1 була визначена без WITH SCHEMABINDING, додавання WITH SCHEMABINDINGпункту повинно дозволяти їй працювати швидше.

Ви вибрали безліч псевдонімів, які не мають сенсу, як, наприклад, псевдонім Object2 як Object3. Вам слід вибрати кращі псевдоніми, які не затьмарять код. У вас "Object7.Column5 in (виберіть стовпчик1 з Object1)".

INпункти такого характеру завжди ефективніше пишуться як EXISTS (SELECT 1 FROM Object1 o1 WHERE o1.Column1 = Object7.Column5). Можливо, я мав би написати це інакше. EXISTSзавжди буде принаймні так добре, як IN. Не завжди краще, але зазвичай є.

Також я сумніваюся, що option(recompile)тут покращується продуктивність запитів. Я б спробував її видалити.


6
Якщо некластеризований пошук індексу охоплює запит, це майже завжди буде краще, ніж кластерний пошук індексу, оскільки, за визначенням, кластерний індекс містить усі стовпці в ньому, а некластеризований індекс має менше стовпців, тому знадобиться менше шукає сторінки (і менший рівень кроків у b-дерево) для отримання даних. Тому не точно сказати, що пошук кластерних індексів завжди буде кращим.
ЕрікЕ
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.