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