SQL Server не оптимізує паралельне з'єднання злиття на двох рівномірно розподілених таблицях


21

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

У мене є дві таблиці, які розділені за допомогою тієї ж схеми розділів. Приєднуючи їх разом у стовпці, що використовується для розділення, я помітив, що SQL Server не в змозі оптимізувати паралельне з'єднання об'єднання стільки, скільки можна було очікувати, і, таким чином, вирішив замість цього використовувати HASH JOIN. У цьому конкретному випадку я можу вручну змоделювати набагато оптимальніший паралельний MERGE JOIN, розділивши запит на 10 розрізнених діапазонів на основі функції розділу та запустивши кожен із цих запитів одночасно в SSMS. Використовуючи WAITFOR для запуску їх усіх в один і той же час, результат полягає в тому, що всі запити виконуються в ~ 40% від загального часу, використовуваного оригінальною паралельною HASH JOIN.

Чи є спосіб змусити SQL Server зробити цю оптимізацію самостійно у випадку таблиць, що мають рівноцінне розподілення? Я розумію, що SQL Server, як правило, може мати великі накладні витрати, щоб зробити ПЕРЕГЛЯДНИЙ ПРИЄДНАЙТЕСЬ паралельно, але, здається, існує дуже природний метод заточування з мінімальними накладними витратами в цьому випадку. Можливо, це просто спеціалізований випадок, який оптимізатор ще недостатньо розумний, щоб розпізнати?

Ось SQL для налаштування спрощеного набору даних для відтворення цієї проблеми:

/* Create the first test data table */
CREATE TABLE test_transaction_properties 
    ( transactionID INT NOT NULL IDENTITY(1,1)
    , prop1 INT NULL
    , prop2 FLOAT NULL
    )

/* Populate table with pseudo-random data (the specific data doesn't matter too much for this example) */
;WITH E1(N) AS (
    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 
    UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
)
, E2(N) AS (SELECT 1 FROM E1 a CROSS JOIN E1 b)
, E4(N) AS (SELECT 1 FROM E2 a CROSS JOIN E2 b)
, E8(N) AS (SELECT 1 FROM E4 a CROSS JOIN E4 b)
INSERT INTO test_transaction_properties WITH (TABLOCK) (prop1, prop2)
SELECT TOP 10000000 (ABS(CAST(CAST(NEWID() AS VARBINARY) AS INT)) % 5) + 1 AS prop1
                , ABS(CAST(CAST(NEWID() AS VARBINARY) AS INT)) * rand() AS prop2
FROM E8

/* Create the second test data table */
CREATE TABLE test_transaction_item_detail
    ( transactionID INT NOT NULL
    , productID INT NOT NULL
    , sales FLOAT NULL
    , units INT NULL
    )

 /* Populate the second table such that each transaction has one or more items
     (again, the specific data doesn't matter too much for this example) */
INSERT INTO test_transaction_item_detail WITH (TABLOCK) (transactionID, productID, sales, units)
SELECT t.transactionID, p.productID, 100 AS sales, 1 AS units
FROM test_transaction_properties t
JOIN (
    SELECT 1 as productRank, 1 as productId
    UNION ALL SELECT 2 as productRank, 12 as productId
    UNION ALL SELECT 3 as productRank, 123 as productId
    UNION ALL SELECT 4 as productRank, 1234 as productId
    UNION ALL SELECT 5 as productRank, 12345 as productId
) p
    ON p.productRank <= t.prop1

/* Divides the transactions evenly into 10 partitions */
CREATE PARTITION FUNCTION [pf_test_transactionId] (INT)
AS RANGE RIGHT
FOR VALUES
(1,1000001,2000001,3000001,4000001,5000001,6000001,7000001,8000001,9000001)

CREATE PARTITION SCHEME [ps_test_transactionId]
AS PARTITION [pf_test_transactionId]
ALL TO ( [PRIMARY] )

/* Apply the same partition scheme to both test data tables */
ALTER TABLE test_transaction_properties
ADD CONSTRAINT PK_test_transaction_properties
PRIMARY KEY (transactionID)
ON ps_test_transactionId (transactionID)

ALTER TABLE test_transaction_item_detail
ADD CONSTRAINT PK_test_transaction_item_detail
PRIMARY KEY (transactionID, productID)
ON ps_test_transactionId (transactionID)

Тепер ми нарешті готові відтворити неоптимальний запит!

/* This query produces a HASH JOIN using 20 threads without the MAXDOP hint,
    and the same behavior holds in that case.
    For simplicity here, I have limited it to 10 threads. */
SELECT COUNT(*)
FROM test_transaction_item_detail i
JOIN test_transaction_properties t
    ON t.transactionID = i.transactionID
OPTION (MAXDOP 10)

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

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

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

SELECT COUNT(*)
FROM test_transaction_item_detail i
INNER MERGE JOIN test_transaction_properties t
    ON t.transactionID = i.transactionID
WHERE t.transactionID BETWEEN 1 AND 1000000
OPTION (MAXDOP 1)

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

Відповіді:


18

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

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

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

Існують способи досягнення цілих планів запитів на декількох потоках у ексклюзивних діапазонах наборів даних, але вони потребують хитрощів, якими не всі будуть задоволені (і Майкрософт не буде підтримуватися або гарантовано працювати в майбутньому). Один із таких підходів полягає в перегляді розділів таблиці, що розділяється, і кожному потоку дається завдання створити підсумковий підсумок. Результатом є SUMкількість рядків, що повертаються кожним незалежним потоком:

Отримати номери розділів досить просто з метаданих:

DECLARE @P AS TABLE
(
    partition_number integer PRIMARY KEY
);

INSERT @P (partition_number)
SELECT
    p.partition_number
FROM sys.partitions AS p 
WHERE 
    p.[object_id] = OBJECT_ID(N'test_transaction_properties', N'U')
    AND p.index_id = 1;

Потім ми використовуємо ці числа для керування відповідним приєднанням ( APPLY) та $PARTITIONфункцією обмеження кожного потоку до поточного номера розділу:

SELECT
    row_count = SUM(Subtotals.cnt)
FROM @P AS p
CROSS APPLY
(
    SELECT
        cnt = COUNT_BIG(*)
    FROM dbo.test_transaction_item_detail AS i
    JOIN dbo.test_transaction_properties AS t ON
        t.transactionID = i.transactionID
    WHERE 
        $PARTITION.pf_test_transactionId(t.transactionID) = p.partition_number
        AND $PARTITION.pf_test_transactionId(i.transactionID) = p.partition_number
) AS SubTotals;

План запитів показує MERGEоб'єднання, яке виконується для кожного рядка таблиці @P. Властивості кластеризованого сканування індексу підтверджують, що на кожній ітерації обробляється лише один розділ:

Застосовуйте серійний план

На жаль, це призводить лише до послідовної послідовної обробки розділів. На наданому вами наборі даних мій 4-ядерний (з гіперпотоком до 8) ноутбук повертає правильний результат за 7 секунд із усіма даними в пам'яті.

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

SELECT
    row_count = SUM(Subtotals.cnt)
FROM @P AS p
CROSS APPLY
(
    SELECT
        cnt = COUNT_BIG(*)
    FROM dbo.test_transaction_item_detail AS i
    JOIN dbo.test_transaction_properties AS t ON
        t.transactionID = i.transactionID
    WHERE 
        $PARTITION.pf_test_transactionId(t.transactionID) = p.partition_number
        AND $PARTITION.pf_test_transactionId(i.transactionID) = p.partition_number
) AS SubTotals
OPTION (QUERYTRACEON 8649);

Тепер план запитів показує номери розділів від @Pрозподілу між потоками на круговій основі. Кожен потік виконує внутрішню сторону вкладених циклів з'єднання для одного розділу, досягаючи нашої мети одночасно обробляти суміжні дані. Цей же результат тепер повертається за 3 секунди на моїх 8 гіпер-ядрах, при цьому всі вісім при 100% -ному використанні.

Паралельне ЗАСТОСУВАННЯ

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

Докладнішу інформацію див. У моїй статті Удосконалення ефективності приєднання до розділеної таблиці .

Стовпчик

Бачачи, як ви використовуєте SQL Server 2012 (і припускаючи, що це Enterprise), ви також можете скористатися індексом зберігання стовпців. Це показує потенціал приєднання хеш-режиму, коли є достатня кількість пам'яті:

CREATE NONCLUSTERED COLUMNSTORE INDEX cs 
ON dbo.test_transaction_properties (transactionID);

CREATE NONCLUSTERED COLUMNSTORE INDEX cs 
ON dbo.test_transaction_item_detail (transactionID);

За допомогою цих індексів розміщуйте запит ...

SELECT
    COUNT_BIG(*)
FROM dbo.test_transaction_properties AS ttp
JOIN dbo.test_transaction_item_detail AS ttid ON
    ttid.transactionID = ttp.transactionID;

... призводить до отримання наступного плану виконання від оптимізатора без жодних хитрощів:

План стовпчика 1

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

SELECT
    COUNT_BIG(*)
FROM dbo.test_transaction_properties AS ttp
JOIN dbo.test_transaction_item_detail AS ttid ON
    ttid.transactionID = ttp.transactionID
GROUP BY
    ttp.transactionID % 1;

Оптимізований стовпчик

Оптимізований запит на зберігання стовпців працює за 851 мс .

Джефф Паттерсон створив звіт про помилки Partition Wise Joins, але він був закритий, оскільки не виправлено.


5
Чудовий досвід навчання тут. Дякую тобі. +1
Едвард Дортленд

1
Спасибі Пол! Тут чудова інформація, і вона, безумовно, детально вирішує питання.
Джефф Паттерсон

2
Спасибі Пол! Тут чудова інформація, і вона, безумовно, детально вирішує питання. Ми перебуваємо в змішаному середовищі SQL 2008/2012, але я буду розглядати питання зберігання стовпців для подальшого майбутнього. Звичайно, я все ще хочу, щоб SQL Server міг ефективно використовувати паралельне з'єднання об'єднання - і набагато менші вимоги до пам'яті, які він може мати - у моєму випадку використання :) Я подав наступну проблему з підключенням, якщо хтось не зацікавився, щоб подивитися та прокоментувати або проголосуй за нього: connect.microsoft.com/SQLServer/feedback/details/759266/…
Джефф Паттерсон

0

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

В цьому випадку, OPTION (MERGE JOIN)

Або ви можете піти цілу свиню і використовувати USE PLAN


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

Цікавим є те, що використання OPTION (MERGE JOIN) призводить до набагато гіршого плану. Оптимізатор недостатньо розумний, щоб зрозуміти, що MERGE JOIN може бути відшарований функцією розділу, і застосування цього підказки змушує запит зайняти ~ 46 секунд. Дуже засмучує!

@ gbn, імовірно, чому оптимізатор в першу чергу збирається приєднати хеш?

@gpatterson Як прикро! :)

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