Вибачте заздалегідь за дуже детальне запитання. Я включив запити для створення повного набору даних для відтворення проблеми, і я запускаю 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)