Geo*_*son 22 join sql-server partitioning
提前为非常详细的问题道歉。我已经包含了生成完整数据集以重现问题的查询,并且我正在 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)
Run Code Online (Sandbox Code Playgroud)
现在我们终于准备好重现次优查询了!
/* 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)
Run Code Online (Sandbox Code Playgroud)


但是,使用单个线程来处理每个分区(下面第一个分区的示例)将导致更有效的计划。我通过在同一时刻对 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)
Run Code Online (Sandbox Code Playgroud)

Pau*_*ite 19
您是对的,SQL Server 优化器不喜欢生成并行MERGE连接计划(这种选择的成本非常高)。ParallelMERGE总是需要在两个连接输入上重新分区交换,更重要的是,它要求在这些交换中保留行顺序。
当每个线程可以独立运行时,并行是最有效的;顺序保留通常会导致频繁的同步等待,并可能最终导致交换溢出tempdb以解决查询内死锁情况。
这些问题可以通过在一个线程上运行整个查询的多个实例来规避,每个线程处理一个独占范围的数据。然而,这不是优化器本机考虑的策略。实际上,用于并行的原始 SQL Server 模型在交换处中断查询,并在多个线程上运行由这些拆分形成的计划段。
有一些方法可以在独占数据集范围内的多个线程上运行整个查询计划,但它们需要的技巧不是每个人都会满意的(并且不会得到 Microsoft 的支持或保证在未来工作)。一种这样的方法是迭代分区表的分区,并为每个线程分配生成小计的任务。结果是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;
Run Code Online (Sandbox Code Playgroud)
然后我们使用这些数字来驱动相关联 ( 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;
Run Code Online (Sandbox Code Playgroud)
查询计划显示MERGE对 table 中的每一行执行的连接@P。聚集索引扫描属性确认每次迭代只处理一个分区:

不幸的是,这只会导致分区的顺序串行处理。在您提供的数据集上,我的 4 核(超线程到 8 核)笔记本电脑在7 秒内返回正确结果,所有数据都在内存中。
为了让MERGE子计划并发运行,我们需要一个并行计划,其中分区 ID 分布在可用线程 ( 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);
Run Code Online (Sandbox Code Playgroud)
现在查询计划显示分区号@P在循环的基础上分布在线程中。每个线程运行单个分区的嵌套循环连接的内侧,实现我们并发处理不相交数据的目标。现在在我的 8 个超核上3 秒内返回相同的结果,所有 8 个都处于 100% 的利用率。

我不建议您一定要使用这种技术 - 请参阅我之前的警告 - 但它确实解决了您的问题。
有关更多详细信息,请参阅我的文章提高分区表连接性能。
鉴于您使用的是 SQL Server 2012(并假设它是企业版),您还可以选择使用列存储索引。这显示了在有足够内存可用的情况下批处理模式散列连接的潜力:
CREATE NONCLUSTERED COLUMNSTORE INDEX cs
ON dbo.test_transaction_properties (transactionID);
CREATE NONCLUSTERED COLUMNSTORE INDEX cs
ON dbo.test_transaction_item_detail (transactionID);
Run Code Online (Sandbox Code Playgroud)
有了这些索引,查询...
SELECT
COUNT_BIG(*)
FROM dbo.test_transaction_properties AS ttp
JOIN dbo.test_transaction_item_detail AS ttid ON
ttid.transactionID = ttp.transactionID;
Run Code Online (Sandbox Code Playgroud)
...从优化器中得出以下执行计划,没有任何技巧:

在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;
Run Code Online (Sandbox Code Playgroud)

优化的列存储查询运行时间为851 毫秒。
Geoff Patterson 创建了错误报告Partition Wise Joins,但它因无法修复而关闭。
| 归档时间: |
|
| 查看次数: |
6320 次 |
| 最近记录: |