Як оптимізувати запит, який працює повільно у вкладених петлях (Внутрішнє приєднання)


39

TL; DR

Оскільки це питання продовжує отримувати перегляд, я підсумую його тут, щоб новачки не мучились страждати історією:

JOIN table t ON t.member = @value1 OR t.member = @value2 -- this is slow as hell
JOIN table t ON t.member = COALESCE(@value1, @value2)    -- this is blazing fast
-- Note that here if @value1 has a value, @value2 is NULL, and vice versa

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

Оригінальний текст

Розглянемо наступний простий запит (задіяно лише 3 таблиці)

    SELECT

        l.sku_id AS ProductId,
        l.is_primary AS IsPrimary,
        v1.category_name AS Category1,
        v2.category_name AS Category2,
        v3.category_name AS Category3,
        v4.category_name AS Category4,
        v5.category_name AS Category5

    FROM category c4
    JOIN category_voc v4 ON v4.category_id = c4.category_id and v4.language_code = 'en'

    JOIN category c3 ON c3.category_id = c4.parent_category_id
    JOIN category_voc v3 ON v3.category_id = c3.category_id and v3.language_code = 'en'

    JOIN category c2 ON c2.category_id = c3.category_id
    JOIN category_voc v2 ON v2.category_id = c2.category_id and v2.language_code = 'en'

    JOIN category c1 ON c1.category_id = c2.parent_category_id
    JOIN category_voc v1 ON v1.category_id = c1.category_id and v1.language_code = 'en'

    LEFT OUTER JOIN category c5 ON c5.parent_category_id = c4.category_id
    LEFT OUTER JOIN category_voc v5 ON v5.category_id = c5.category_id and v5.language_code = @lang

    JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
    (
        l.category_id = c4.category_id OR
        l.category_id = c5.category_id
    )

    WHERE c4.[level] = 4 AND c4.version_id = 5

Це досить простий запит, єдиною заплутаною частиною є приєднання останньої категорії. Це так, тому що 5-й рівень категорії може бути або не існувати. Наприкінці запиту я шукаю інформацію про категорії за ідентифікатором продукту (SKU ID), і саме там надходить дуже велика таблиця category_link. Нарешті, таблиця #Ids - це лише тимчасова таблиця, що містить 10 000 ідентифікаторів.

Після виконання я отримую такий фактичний план виконання:

Фактичний план виконання

Як бачите, майже 90% часу проводиться у вкладених петлях (Inner Join). Ось додаткова інформація про ці вкладені петлі:

Вкладені петлі (внутрішнє з'єднання)

Зауважте, що назви таблиць не відповідають точно тому, що я редагував назви таблиці запитів щодо читабельності, але зіставити їх досить легко (ads_alt_category = категорія). Чи є спосіб оптимізувати цей запит? Також зауважте, що у виробництві тимчасова таблиця #Ids не існує, це параметр, що оцінюється у таблиці, з тих же 10000 ідентифікаторів, переданих в процедуру збереження.

Додаткова інформація:

  • індекси категорії на категорію_id та батьківська_категорія_id
  • index_voc індекс категорії_id, код_код мови
  • індекс категорії_посилання на sku_id, категорія_id

Редагувати (вирішено)

Як було зазначено у прийнятій відповіді, проблемою було положення АБО у категорії_посилання ПРИЄДНАЙТЕСЬ. Однак код, запропонований у прийнятій відповіді, дуже повільний, повільніший, ніж оригінальний код. Набагато швидше, а також чистіше рішення - просто замінити поточний стан JOIN на наступне:

JOIN category_link l on l.sku_id IN (SELECT value FROM @p1) AND l.category_id = COALESCE(c5.category_id, c4.category_id)

Ця хвилинна настройка - це найшвидше рішення, випробуване на подвійне з'єднання з прийнятої відповіді, а також перевірене на CROSS APPLY, як це запропонувало valverij.


Нам потрібно переглянути решту запиту.
RBarryYoung

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

Чи передбачає план виконання пропозиції щодо індексів? Також не забувайте, що ви можете встановити первинні ключі та індекси на своїх тимчасових таблицях (більше інформації тут )

@rbarry Якщо після спробу поточних рішень я нічого не отримаю, я вдосконалюю питання

1
Як щодо дублювання запиту в UNION та позбавлення АБО

Відповіді:


17

Здається, проблема в цій частині коду:

JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
(
    l.category_id = c4.category_id OR
    l.category_id = c5.category_id
)

orв умовах приєднання завжди підозріло. Одна з пропозицій - розділити це на два об'єднання:

JOIN category_link l1 on l1.sku_id in (SELECT value FROM #Ids) and l1.category_id = cr.category_id
left outer join
category_link l1 on l2.sku_id in (SELECT value FROM #Ids) and l2.category_id = cr.category_id

Тоді вам доведеться змінити решту запитів, щоб впоратися з цим. . . coalesce(l1.sku_id, l2.sku_id)наприклад, у selectпункті.


Оскільки кількість фільтрування робиться на цьому конкретному з'єднанні, я б також перевірив зміну JOINна «a» CROSS APPLYз INпереключенням на « EXISTSв APPLY» WHEREпункт.

Дякую Гордону, я спершу випробую це вранці. @Valverij, я не знайомий із застосуванням перехресних застосувань, чи могли б ви описати своє рішення більше, можливо, у правильному відповіді, тож я можу проголосувати, якщо це виявиться найшвидшим сценарієм?

3
Я приймаю цю відповідь, тому що саме перша відповідь вказала мені на проблему. Пропоноване рішення, однак, надзвичайно повільне, повільніше, ніж оригінальний код. Однак, знаючи, що проблема АБО - це проблема, просто замінивши її, ON l.category_id = ISNULL(c5.category_id, c4.category_idзробив трюк.
Луїс Феррао

1
@LuisFerrao. . . Дякую за додаткову інформацію. Корисно знати, що coalesce()штовхає оптимізатор в потрібному напрямку.
Гордон Лінофф

9

Як зазначив інший користувач, це приєднання, ймовірно, є причиною:

JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
(
    l.category_id = c4.category_id OR
    l.category_id = c5.category_id
)

Крім розділення їх на кілька приєднань, ви також можете спробувати CROSS APPLY

CROSS APPLY (
    SELECT [some column(s)]
    FROM category_link x
    WHERE EXISTS(SELECT value FROM #Ids WHERE value = x.sku_id)
    AND (x.category_id = c4.category_id OR x.category_id = c5.category_id)        
) l

З посилання MSDN вище:

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

По суті, APPLYце як підзапит, який спочатку фільтрує записи праворуч, а потім застосовує їх до решти запиту.

Ця стаття дуже добре пояснює, що це таке, і коли їх використовувати: http://explainextended.com/2009/07/16/inner-join-vs-cross-apply/

Важливо зауважити, однак, що CROSS APPLYце не завжди працює швидше ніж INNER JOIN. У багатьох ситуаціях це, мабуть, буде приблизно однаково. Однак у рідкісних випадках я насправді бачу це повільніше (знову ж, все це залежить від вашої структури таблиці та самого запиту).

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

Також весела примітка: OUTER APPLYбуде діяти якLEFT JOIN

Також, будь ласка, врахуйте мій вибір, EXISTSа не використовувати IN. Виконуючи INпідзапит, пам’ятайте, що він поверне весь набір результатів, навіть після того, як знайде ваше значення. З EXISTS, хоча він зупиниться підзапитів в той момент , вона знаходить матч.


Я ретельно випробував це рішення. Як ви це писали, це досить повільно, але ви забули застосувати пораду, з якої розпочали своє повідомлення. Заміна AND x.cat = c4.cat OR x.cat = c5.catпо x.cat = ISNULL(c5.cat, c4.cat)і позбавленні від пункту IN зробив це друге саме швидке рішення, і гідним upvote, тому що це досить інформативні.
Луїс Феррао

Спасибі. Рядок IN насправді не мав бути там (не міг визначитися з використанням IN або дотримуватися АБО), я його видалю.
valverij
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.