SQL Server不会在两个等效分区的表上优化并行合并联接


21

非常抱歉,非常详细的问题。我已包含查询以生成用于重现该问题的完整数据集,并且我在32核计算机上运行SQL Server 2012。但是,我不认为这是特定于SQL Server 2012的,对于此特定示例,我已将MAXD​​OP强制设置为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)

在此处输入图片说明 在此处输入图片说明

Answers:


18

没错,SQL Server优化器不希望生成并行联接MERGE计划(这种替代方案的成本很高)。并行MERGE始终要求对两个联接输入进行重分区交换,更重要的是,它要求在这些交换之间保留行顺序。

当每个线程可以独立运行时,并行性是最有效的。订单保留通常会导致频繁的同步等待,并最终可能导致交换溢出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;

然后,我们使用这些数字来驱动相关联的join(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子计划可以同时运行,我们需要一个并行计划,其中分区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);

现在,查询计划显示了分区编号,@P这些编号是基于循环在线程之间分配的。每个线程都在嵌套循环的内侧运行,以连接单个分区,从而实现了我们同时处理不连续数据的目标。现在,我的8个超级核心在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;

优化的列存储

优化的列存储查询的运行时间为851ms

Geoff Patterson创建了错误报告Partition Wise Joins,但由于无法修复而关闭。


5
优秀的学习经验。谢谢。+1
爱德华·多特兰

1
谢谢保罗!这里提供了很多有用的信息,当然可以详细地解决这个问题。
杰夫·帕特森

2
谢谢保罗!这里提供了很多有用的信息,当然可以详细地解决这个问题。我们处于混合SQL 2008/2012环境中,但是我将考虑在将来进一步探索列存储。当然,我仍然希望SQL Server在我的用例中可以有效地利用并行合并联接,以及它可能具有的更低的内存需求:)我提出了以下Connect问题,以防有人在意一下并发表评论或对其投票:connect.microsoft.com/SQLServer/feedback/details/759266/…–
Geoff Patterson

0

通过查询提示使优化器以您认为更好的方式工作。

在这种情况下, OPTION (MERGE JOIN)

或者您可以全程使用 USE PLAN


我个人不会这样做:提示仅对当前数据量和分发有用。
gbn 2012年

有趣的是,使用OPTION(MERGE JOIN)会导致糟糕得多的计划。优化器不够聪明,无法意识到MERGE JOIN可以通过分区函数进行分片,而应用此提示会使查询花费〜46秒。很沮丧!

@gbn这可能是为什么优化器首先要进行哈希联接的原因?

@gpatterson真烦人!:)

如果您通过联合强制手动进行分区(即:您的简短查询与其他类似查询联合)会发生什么情况?
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.