Проблема оптимізації за допомогою функції, визначеної користувачем


26

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

select  
    S.GROUPCODE,
    H.ORDERCATEGORY
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT    
    cross apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

Для цього запиту SQL Server вирішує викликати функцію GetGroupCode для кожного окремого значення, яке існує в таблиці PRODUCT, навіть якщо оцінка та фактична кількість рядків, повернених з ORDERLINE, дорівнює 1 (це первинний ключ):

План запитів

Цей самий план у програмі провідника плану, що показує кількість рядків:

План дослідника Таблиці:

ORDERLINE: 1.5M rows, primary key: ORDERNUMBER + ORDERLINE + RMPHASE (clustered)
ORDERHDR:  900k rows, primary key: ORDERID (clustered)
PRODUCT:   6655 rows, primary key: PRODUCT (clustered)

Індекс, що використовується для сканування, є:

create unique nonclustered index PRODUCT_FACTORY on PRODUCT (PRODUCT, FACTORY)

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

create function GetGroupCode (@FACTORY varchar(4))
returns @t table(
    TYPE        varchar(8),
    GROUPCODE   varchar(30)
)
as begin
    insert into @t (TYPE, GROUPCODE) values ('XX', 'YY')
    return
end

Я зміг "виправити" продуктивність, змусивши SQL-сервер взяти перший продукт 1, хоча 1 макс, який коли-небудь можна знайти:

select  
    S.GROUPCODE,
    H.ORDERCAT
from    
    ORDERLINE L
    join ORDERHDR H
        on H.ORDERID = M.ORDERID
    cross apply (select top 1 P.FACTORY from PRODUCT P where P.PRODUCT = L.PRODUCT) P
    cross apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

Тоді форма плану також змінюється, щоб я щось сподівався:

План запиту зверху

Я також вважаю, що індекс PRODUCT_FACTORY менший, ніж кластерний індекс PRODUCT_PK, впливатиме, але навіть при змушуванні запиту використовувати PRODUCT_PK, план все одно такий же, як і оригінальний, з 6655 викликами до функції.

Якщо я залиште ORDERHDR повністю, тоді план починається з вкладеного циклу між ORDERLINE та PRODUCT спочатку, а функція викликається лише один раз.

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

Редагувати: Створюйте таблиці операторів:

CREATE TABLE dbo.ORDERHDR(
    ORDERID varchar(8) NOT NULL,
    ORDERCATEGORY varchar(2) NULL,
    CONSTRAINT ORDERHDR_PK PRIMARY KEY CLUSTERED (ORDERID)
)

CREATE TABLE dbo.ORDERLINE(
    ORDERNUMBER varchar(16) NOT NULL,
    RMPHASE char(1) NOT NULL,
    ORDERLINE char(2) NOT NULL,
    ORDERID varchar(8) NOT NULL,
    PRODUCT varchar(8) NOT NULL,
    CONSTRAINT ORDERLINE_PK PRIMARY KEY CLUSTERED (ORDERNUMBER,ORDERLINE,RMPHASE)
)

CREATE TABLE dbo.PRODUCT(
    PRODUCT varchar(8) NOT NULL,
    FACTORY varchar(4) NULL,
    CONSTRAINT PRODUCT_PK PRIMARY KEY CLUSTERED (PRODUCT)
)

Відповіді:


30

Існує три основні технічні причини, з яких ви отримуєте план, який ви робите:

  1. Рамка витрат оптимізатора не має реальної підтримки функцій, що не вбудовуються. Він не робить жодної спроби заглянути все визначення функції, щоб побачити, наскільки це може бути дорогим, він просто призначає дуже невелику фіксовану вартість, і підраховує, що функція буде створювати 1 рядок виходу кожного разу, коли вона викликається. Обидва ці припущення щодо моделювання дуже часто є абсолютно небезпечними. Ситуація дуже дещо покращилася в 2014 році, коли ввімкнено новий оцінювальний коефіцієнт, оскільки фіксований 1-рядовий здогад замінено на фіксовану 100-рядкову здогадку. Однак досі немає підтримки для витрат на вміст не вбудованих функцій.
  2. SQL Server спочатку згортає приєднання і застосовується до єдиного внутрішнього n-arry-логічного з'єднання. Це допомагає оптимізаторові пізніше приєднатися до замовлень. Розширення єдиного n-arry приєднання до замовлень щодо приєднання кандидатів відбувається пізніше, і багато в чому базується на евристиці. Наприклад, внутрішні приєднання відбуваються перед зовнішніми приєднаннями, маленькими таблицями та вибірковими об'єднаннями перед великими таблицями та менш вибірковими приєднаннями тощо.
  3. Коли SQL Server здійснює оптимізацію на основі витрат, він розбиває зусилля на необов'язкові фази, щоб мінімізувати шанси витратити занадто довго оптимізацію недорогих запитів. Існують три основні фази: пошук 0, пошук 1 та пошук 2. Кожна фаза має умови входу, а пізніші фази дозволяють проводити більше досліджень оптимізатора, ніж попередні. Ваш запит відповідає вимогам до найменш працездатної фази пошуку, фаза 0. Там знайдено досить низький план витрат, щоб пізніші етапи не були введені.

З огляду на невелику оцінку кардинальності, призначену для UDF, застосовується евристика розширення n-arry, на жаль, переставляє її раніше на дереві, ніж ви хотіли б.

Запит також підходить для оптимізації пошуку 0 завдяки наявності принаймні трьох приєднань (у тому числі застосовується). Кінцевий фізичний план, який ви отримаєте, при незвичайному скануванні, заснований на евристично виведеному порядку приєднання. Він коштує досить низько, що оптимізатор вважає план "досить хорошим". Низька оцінка вартості та кардинальність для АДС сприяє цьому ранньому завершенню.

Пошук 0 (також відомий як фаза обробки транзакцій) орієнтується на запити типу OLTP з низькою кардинальністю, з кінцевими планами, які зазвичай містять вкладені петлі. Що ще важливіше, пошук 0 запускає лише відносно невелику підмножину можливостей пошукового оптимізатора. Цей підмножина не включає підтягування аплікації на дерево запитів над об'єднанням (правилом PullApplyOverJoin). Це саме те, що потрібно в тестовому випадку, щоб перестановка UDF застосовувалася над з'єднаннями, щоб вона була останньою в послідовності операцій (як би).

Існує також проблема, коли оптимізатор може вирішувати між наївним вкладеним циклом приєднання (предикат приєднання на самому об'єднанні) та корельованим індексованим приєднанням (застосувати), коли корельований предикат застосовується на внутрішній стороні об'єднання за допомогою індексу пошуку. Останній, як правило, бажаної форми плану, але оптимізатор здатний дослідити обидва. При неправильних оцінках витрат та кардинальності він може вибрати неприєднувальне приєднання NL, як у поданих планах (пояснення сканування).

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

Плануйте порожні таблиці з вимкненим пошуком 0

Не існує жодного підтримуваного способу уникнути вибору плану пошуку 0, дострокового завершення роботи оптимізатора або поліпшити витрати на UDF (окрім обмежених удосконалень у моделі SQL Server 2014 CE для цього). Це залишає такі речі, як керівництво по плану, переписування запитів вручну (включаючи TOP (1)ідею або використання проміжних тимчасових таблиць) та уникнення погано витрачених «чорних коробок» (з точки зору QO), таких як не вбудовані функції.

Переписування CROSS APPLYяк OUTER APPLYможе також спрацює, оскільки в даний час запобігає деяким раннім об'єднанням роботи, але ви повинні бути обережними, щоб зберегти оригінальну семантику запитів (наприклад, відхилення будь-яких NULLрозширених рядків, які можуть бути введені, без оптимізатора згортання на хрест застосовувати). Вам потрібно знати, що ця поведінка не гарантується залишатись стабільною, тому вам потрібно пам'ятати, щоб повторно перевірити будь-яке подібне спостережуване поведінку під час кожного виправлення або оновлення SQL Server.

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


24

Схоже, це оптимізатор на основі витрат, але досить поганий.

Якщо ви додасте 50000 рядків до PRODUCT, оптимізатор вважає, що сканування занадто багато роботи і дає вам план з трьома пошуками та одним викликом до UDF.

План я отримую на 6655 рядків у PRODUCT

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

Маючи 50000 рядків у ТОВАРІ, я отримую цей план замість цього.

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

Я здогадуюсь, що вартість виклику АДС є заниженою.

Одне вирішення, яке добре працює в цьому випадку, - це зміни запиту на використання зовнішнього застосунку проти UDF. Я отримую хороший план незалежно від того, скільки рядків у таблиці ПРОДУКТ.

select  
    S.GROUPCODE,
    H.ORDERCATEGORY
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT    
    outer apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01' and
    S.GROUPCODE is not null

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

Найкращий спосіб вирішити у вашому випадку - це, мабуть, отримати потрібні вам значення в тимчасовій таблиці, а потім запитати таблицю темп з хрестом, застосувати до UDF. Таким чином ви впевнені, що АДС не буде виконано більше, ніж потрібно.

select  
    P.FACTORY,
    H.ORDERCATEGORY
into #T
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

select  
    S.GROUPCODE,
    T.ORDERCATEGORY
from #T as T
  cross apply dbo.GetGroupCode (T.FACTORY) S

drop table #T

Замість того, щоб зберігати таблицю temp, ви можете використовувати top()у похідній таблиці, щоб змусити SQL Server оцінювати результат від об'єднань до виклику UDF. Просто використовуйте дійсно високе число у верхній частині, що робить SQL Server повинен рахувати ваші рядки для тієї частини запиту, перш ніж він може продовжуватись та використовувати UDF.

select S.GROUPCODE,
       T.ORDERCATEGORY
from (
     select top(2147483647)
         P.FACTORY,
         H.ORDERCATEGORY
     from    
         ORDERLINE L
         join ORDERHDR H on H.ORDERID = L.ORDERID
         join PRODUCT P  on P.PRODUCT = L.PRODUCT    
     where   
         L.ORDERNUMBER = 'XXX/YYY-123456' and
         L.RMPHASE = '0' and
         L.ORDERLINE = '01'
     ) as T
  cross apply dbo.GetGroupCode (T.FACTORY) S

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

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

Я справді не можу відповісти на це, але подумав, що все одно повинен поділитися тим, що знаю. Я не знаю, чому сканування таблиці ПРОДУКТУ взагалі розглядається. Можливо, є випадки, коли це найкраще робити, і є речі, які стосуються того, як оптимізатори поводяться з АДС, про які я не знаю.

Одним із додаткових спостережень було те, що ваш запит отримує хороший план у SQL Server 2014 за допомогою нового оцінювача кардинальності. Це тому, що орієнтовна кількість рядків для кожного виклику до UDF становить 100 замість 1, як це є у SQL Server 2012 та раніше. Але все одно буде прийнято однакове рішення на основі вартості між версією сканування та пошуковою версією плану. Маючи менше ніж 500 (497 у моєму випадку) рядків у PRODUCT, ви отримаєте версію плану сканування навіть у SQL Server 2014.


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