Чи слід в SQL Server примушувати LOOP ПРИЄДНАЙТЕСЬ у наступному випадку?


15

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

--Case 1: NO HINT
SELECT S.*
INTO #Results
FROM #Driver AS D
JOIN SampleTable AS S ON S.ID = D.ID

--Case 2: LOOP JOIN HINT
SELECT S.*
INTO #Results
FROM #Driver AS D
INNER LOOP JOIN SampleTable AS S ON S.ID = D.ID

SampleTable має 1 мільйон рядків, і його ПК - ID.
У таблиці темп #Driver є лише один стовпець, ідентифікатор, відсутні індекси та 50K рядків.

Я послідовно знаходжу таке:

Випадок 1: НЕ
підказки сканування індексу на
Hash SampleTable приєднатися з більшою
тривалістю (середньо 333 мс)
Вища CPU (середня 331 мс)
Нижня логічна читання (4714)

Випадок 2:
Індекс LOOP JOIN HINT ( Шукати підказку LOOP JOIN) Шукайте нижчу тривалість приєднання
циклу SampleTable
(середнє 204 мс, на 39% менше)
Нижчий процесор (середнє 206, 38% менше)
Набагато більше логічних читань (160015, 34X більше)

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

SampleTable міститься на 4714 сторінках, що займає близько 36 Мб. Випадок 1 сканує їх усі, тому ми отримуємо 4714 прочитаних. Крім того, він повинен виконати 1 мільйон хешів, які є інтенсивними процесорами, і в кінцевому підсумку пропорційно збільшує час. Саме це хеширование, здається, призводить до збільшення часу у випадку 1.

Тепер розглянемо випадок 2. Він не робить хешування, але натомість робить 50000 окремих пошуків, і це те, що сприяє читанню. Але наскільки дорого коштують прочитані? Можна сказати, що якщо це фізичні показання, це може бути досить дорогим. Але майте на увазі: 1) лише перше прочитання даної сторінки може бути фізичним, і 2) навіть у тому випадку, якщо випадок 1 матиме ту саму чи гіршу проблему, оскільки гарантовано потрапляти на кожну сторінку.

Отже, враховуючи той факт, що обидва випадки мають хоча б один раз отримати доступ до кожної сторінки, здається, питання про те, що швидше, 1 мільйон хешів або близько 155000 зчитувань проти пам'яті? Мої тести, здається, говорять про останнє, але SQL Server послідовно вибирає перший.

Питання

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

Оновлення 2014-04-28

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

Плани виконання

План виконання Випадок 1 План 1 План виконання Випадок 2 введіть тут опис зображення

Код для створення зразка

------------------------------------------------------------
-- 1. Create SampleTable with 1,000,000 rows
------------------------------------------------------------    

CREATE TABLE SampleTable
    (  
       ID         INT NOT NULL PRIMARY KEY CLUSTERED
     , Number1    INT NOT NULL
     , Number2    INT NOT NULL
     , Number3    INT NOT NULL
     , Number4    INT NOT NULL
     , Number5    INT NOT NULL
    )

--Add 1 million rows
;WITH  
    Cte0 AS (SELECT 1 AS C UNION ALL SELECT 1), --2 rows  
    Cte1 AS (SELECT 1 AS C FROM Cte0 AS A, Cte0 AS B),--4 rows  
    Cte2 AS (SELECT 1 AS C FROM Cte1 AS A ,Cte1 AS B),--16 rows 
    Cte3 AS (SELECT 1 AS C FROM Cte2 AS A ,Cte2 AS B),--256 rows 
    Cte4 AS (SELECT 1 AS C FROM Cte3 AS A ,Cte3 AS B),--65536 rows 
    Cte5 AS (SELECT 1 AS C FROM Cte4 AS A ,Cte2 AS B),--1048576 rows 
    FinalCte AS (SELECT  ROW_NUMBER() OVER (ORDER BY C) AS Number FROM   Cte5)
INSERT INTO SampleTable
SELECT Number, Number, Number, Number, Number, Number
FROM  FinalCte
WHERE Number <= 1000000

------------------------------------------------------------
-- Create 2 SPs that join from #Driver to SampleTable.
------------------------------------------------------------    
GO
IF OBJECT_ID('JoinTest_NoHint') IS NOT NULL DROP PROCEDURE JoinTest_NoHint
GO
CREATE PROC JoinTest_NoHint
AS
    SELECT S.*
    INTO #Results
    FROM #Driver AS D
    JOIN SampleTable AS S ON S.ID = D.ID
GO
IF OBJECT_ID('JoinTest_LoopHint') IS NOT NULL DROP PROCEDURE JoinTest_LoopHint
GO
CREATE PROC JoinTest_LoopHint
AS
    SELECT S.*
    INTO #Results
    FROM #Driver AS D
    INNER LOOP JOIN SampleTable AS S ON S.ID = D.ID
GO

------------------------------------------------------------
-- Create driver table with 50K rows
------------------------------------------------------------    
GO
IF OBJECT_ID('tempdb..#Driver') IS NOT NULL DROP TABLE #Driver
SELECT ID
INTO #Driver
FROM SampleTable
WHERE ID % 20 = 0

------------------------------------------------------------
-- Run each test and run Profiler
------------------------------------------------------------    

GO
/*Reg*/  EXEC JoinTest_NoHint
GO
/*Loop*/ EXEC JoinTest_LoopHint


------------------------------------------------------------
-- Results
------------------------------------------------------------    

/*

Duration CPU   Reads    TextData
315      313   4714     /*Reg*/  EXEC JoinTest_NoHint
309      296   4713     /*Reg*/  EXEC JoinTest_NoHint
327      329   4713     /*Reg*/  EXEC JoinTest_NoHint
398      406   4715     /*Reg*/  EXEC JoinTest_NoHint
316      312   4714     /*Reg*/  EXEC JoinTest_NoHint
217      219   160017   /*Loop*/ EXEC JoinTest_LoopHint
211      219   160014   /*Loop*/ EXEC JoinTest_LoopHint
217      219   160013   /*Loop*/ EXEC JoinTest_LoopHint
190      188   160013   /*Loop*/ EXEC JoinTest_LoopHint
187      187   160015   /*Loop*/ EXEC JoinTest_LoopHint

*/

Відповіді:


13

SampleTable міститься на 4714 сторінках, займаючи близько 36 Мб. Випадок 1 сканує їх усі, тому ми отримуємо 4714 прочитаних. Крім того, він повинен виконати 1 мільйон хешів, які є інтенсивними процесорами, і в кінцевому підсумку пропорційно збільшує час. Саме це хеширование, здається, призводить до збільшення часу у випадку 1.

Для хеш-приєднання є стартова вартість (складання хеш-таблиці, що також є блокуючою операцією), але хеш-з'єднання в кінцевому рахунку має найнижчу теоретичну вартість на ряд з трьох фізичних типів приєднання, підтримуваних SQL Server, обидва в умови IO та CPU. Хеш-з'єднання справді стає власним із порівняно невеликим входом збірки та великим вхідним зондом. Однак, жоден фізичний тип приєднання не є "кращим" у всіх сценаріях.

Тепер розглянемо випадок 2. Він не робить хешування, але замість цього він робить 50000 окремих пошуків, і саме це сприяє читанню. Але наскільки дорого коштують прочитані порівняно? Можна сказати, що якщо це фізичні показання, це може бути досить дорогим. Але майте на увазі: 1) лише перше прочитання даної сторінки може бути фізичним, і 2) навіть у тому випадку, якщо випадок 1 матиме ту саму чи гіршу проблему, оскільки гарантовано потрапляти на кожну сторінку.

Кожна спроба вимагає навігації b-дерева до кореня, що обчислювально дорого порівняно з одним хеш-зондом. Крім того, загальна модель вводу-виводу для внутрішньої сторони вкладеного вкладеного циклу є випадковою, порівняно з послідовною схемою доступу вхідного сканування на стороні зонда до хеш-з'єднання. Залежно від основної фізичної підсистеми вводу-виводу, послідовне зчитування може бути швидшим, ніж випадкове зчитування. Також механізм заздалегідь зчитування SQL Server краще працює з послідовним IO, видаючи більші читання.

Отже, враховуючи той факт, що обидва випадки мають хоча б один раз отримати доступ до кожної сторінки, здається, питання про те, що швидше, 1 мільйон хешів або близько 155000 зчитувань проти пам'яті? Мої тести, здається, говорять про останнє, але SQL Server послідовно вибирає перший.

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

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

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

Чи варто продовжувати змушувати цей підказку LOOP JOIN, коли тестування показує такі результати, чи я щось пропускаю в своєму аналізі? Я вагаюся проти оптимізатора SQL Server, але відчуваю, що він переходить на використання хеш-з'єднання набагато раніше, ніж слід у таких випадках.

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

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

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


Дякую, Павло. Відмінний детальний аналіз. На основі подальших тестувань, які я робив, я думаю, що відбувається, що освічені здогадки оптимізатора послідовно вимикаються для цього конкретного прикладу, коли розмір темп-таблиці становить від 5K до 100K. Зважаючи на той факт, що наші вимоги гарантують, що часова таблиця буде <50K, мені це здається безпечним. Мені цікаво, ти все-таки уникатимеш будь-якого натяку на приєднання, знаючи це?
JohnnyM

1
Підказки @JohnnyM існують не просто так. Добре використовувати їх там, де у вас є обгрунтовані причини. Однак, я дуже рідко використовую підказки про приєднання через те, що мається на увазі FORCE ORDER. У випадковому випадку я використовую підказку про приєднання, я часто додаю OPTION (FORCE ORDER)коментар, щоб пояснити, чому.
Пол Білий 9

0

50 000 рядків, об'єднаних проти таблиці з мільйонними рядами, здається, багато для будь-якої таблиці без індексу.

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

Беручи приклад лише для того, що він говорить, чому б просто не поставити індекс на #Driver? Чи справді D.ID унікальний? Якщо це так, це семантично еквівалентно оператору EXISTS, який принаймні дозволить SQL Server знати, що ви не хочете продовжувати пошук S за дублікатами значень D:

SELECT S.*
INTO #Results
FROM SampleTable S
WHERE EXISTS (SELECT * #Driver D WHERE S.ID = D.ID);

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

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