提前为非常详细的问题道歉。我已经包含查询以生成完整的数据集以重现问题,并且我在 32 核机器上运行 SQL Server 2012。但是,我不认为这特定于 SQL Server 2012,并且我已为此特定示例强制 MAXDOP 为 10。
我有两个使用相同分区方案分区的表。当在用于分区的列上将它们连接在一起时,我注意到 SQL Server 无法像预期的那样优化并行合并连接,因此选择使用 HASH JOIN。在这种特殊情况下,我可以通过根据分区函数将查询拆分为 10 个不相交的范围并在 SSMS 中同时运行每个查询来手动模拟更优化的并行 MERGE JOIN。使用 WAITFOR 在完全相同的时间运行它们,结果是所有查询在原始并行 HASH JOIN 使用的总时间的约 40% 内完成。
在等效分区表的情况下,有什么方法可以让 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)
你是对的,SQL Server 优化器不喜欢生成并行
MERGE
连接计划(这种替代方案的成本非常高)。并行MERGE
总是需要在两个连接输入上重新分区交换,更重要的是,它要求在这些交换中保留行顺序。当每个线程可以独立运行时,并行性是最有效的;订单保留通常会导致频繁的同步等待,并最终可能导致交换溢出以
tempdb
解决查询内死锁条件。这些问题可以通过在每个线程上运行整个查询的多个实例来规避,每个线程处理专有范围的数据。然而,这不是优化器本机考虑的策略。实际上,用于并行性的原始 SQL Server 模型会在交换时中断查询,并在多个线程上运行由这些拆分形成的计划段。
有一些方法可以实现在多个线程上在专有数据集范围内运行整个查询计划,但它们需要一些技巧,并不是每个人都会满意(并且不会得到 Microsoft 的支持或保证将来可以工作)。一种这样的方法是遍历分区表的分区,并为每个线程分配生成小计的任务。结果是
SUM
每个独立线程返回的行数:从元数据中获取分区号很容易:
然后我们使用这些数字来驱动一个关联连接 (
APPLY
),以及将$PARTITION
每个线程限制为当前分区号的函数:查询计划显示
MERGE
为 table 中的每一行执行连接@P
。聚集索引扫描属性确认每次迭代只处理一个分区:不幸的是,这只会导致分区的顺序串行处理。在您提供的数据集上,我的 4 核(超线程到 8)笔记本电脑在7 秒内返回正确的结果,所有数据都在内存中。
为了让
MERGE
子计划同时运行,我们需要一个并行计划,其中分区 ID 分布在可用线程 (MAXDOP
) 上,每个MERGE
子计划使用一个分区中的数据在单个线程上运行。不幸的是,优化器经常MERGE
以成本为由决定反对并行,并且没有记录的方法来强制并行计划。有一种未记录(且不受支持)的方式,使用跟踪标志 8649:现在,查询计划显示分区号
@P
以循环方式分布在线程之间。每个线程都在嵌套循环连接的内侧运行单个分区,从而实现我们同时处理不相交数据的目标。现在,我的 8 个超核在3 秒内返回相同的结果,所有 8 个超核的利用率均为 100%。我不建议您一定要使用这种技术 - 请参阅我之前的警告 - 但它确实解决了您的问题。
有关详细信息,请参阅我的文章提高分区表连接性能。
列存储
看到您使用的是 SQL Server 2012(并假设它是 Enterprise),您还可以选择使用列存储索引。这显示了在有足够内存可用的情况下批处理模式哈希连接的潜力:
有了这些索引,查询...
...从优化器中得到以下执行计划,没有任何技巧:
在2 秒内正确结果,但消除标量聚合的行模式处理有助于更多:
优化的列存储查询在851ms中运行。
Geoff Patterson 创建了错误报告Partition Wise Joins,但由于无法修复而被关闭。
使优化器以您认为更好的方式工作的方法是通过查询提示。
在这种情况下,
OPTION (MERGE JOIN)
或者你可以全力以赴并使用
USE PLAN