SQL Serverは、2つの同等にパーティション分割されたテーブルでの並列マージ結合を最適化しません


21

非常に詳細な質問をおApびします。問題を再現するための完全なデータセットを生成するクエリを含め、32コアマシンでSQL Server 2012を実行しています。ただし、これはSQL Server 2012に固有のものではないと思い、この特定の例ではMAXDOPを10に強制しました。

同じパーティション構成を使用してパーティション化された2つのテーブルがあります。パーティショニングに使用される列でそれらを結合すると、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)

ここに画像の説明を入力してください ここに画像の説明を入力してください

回答:


18

SQL Serverオプティマイザーが並列MERGE結合プランを生成しないことを好むのは正しいことです(この代替案のコストは非常に高くなります)。パラレルでは、MERGE常に両方の結合入力で交換を再パーティション化する必要があります。さらに重要なことは、これらの交換全体で行の順序を保持する必要があることです。

並列処理は、各スレッドが独立して実行できる場合に最も効率的です。順序の保存は、頻繁に同期待機を頻繁に引き起こし、最終的には、エクストラを交換してtempdbクエリ内デッドロック状態を解決する可能性があります。

これらの問題は、クエリ全体の複数のインスタンスをそれぞれ1つのスレッドで実行し、各スレッドが排他的な範囲のデータを処理することで回避できます。ただし、これはオプティマイザーがネイティブに考慮する戦略ではありません。そのままでは、並列処理のための元のSQL Serverモデルは、交換時にクエリを中断し、それらの分割によって形成されたプランセグメントを複数のスレッドで実行します。

排他的なデータセットの範囲で複数のスレッドでクエリプラン全体を実行する方法はありますが、だれもが満足できるわけではないというトリックが必要です(Microsoftによってサポートされることも将来的に動作することも保証されません)。そのようなアプローチの1つは、パーティションテーブルのパーティションを反復処理し、各スレッドに小計を生成するタスクを与えることです。結果は、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;

次に、これらの数値を使用して相関結合(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;

クエリプランは、MERGEtableの各行に対して実行される結合を示しています@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を使用している(そしてそれがエンタープライズであると想定している)ことを確認するには、列ストアインデックスを使用するオプションもあります。これは、十分なメモリが利用可能なバッチモードハッシュ結合の可能性を示しています。

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を作成しましたが、Wo n't Fixとしてクローズされました。


5
ここでの優れた学習経験。ありがとうございました。+1
エドワード・ドートランド

1
ポールありがとう!ここに素晴らしい情報があり、それは確かに詳細に質問に対処します。
ジェフパターソン

2
ポールありがとう!ここに素晴らしい情報があり、それは確かに詳細に質問に対処します。SQL 2008/2012が混在する環境にいますが、今後、列ストアをさらに検討することを検討します。もちろん、SQL Serverが並列マージ結合を効果的に活用できることを望みます-そして、それが持つことができるはるかに低いメモリ要件-私のユースケースで:)またはそれに投票:connect.microsoft.com/SQLServer/feedback/details/759266/...
ジェフ・パターソン

0

オプティマイザーを適切に動作させる方法は、クエリヒントを使用することです。

この場合、 OPTION (MERGE JOIN)

または、すべてを独り占めして使用することができます USE PLAN


個人的にはこれを行いません。ヒントは、現在のデータ量と分布にのみ役立ちます。
gbn

興味深いことに、OPTION(MERGE JOIN)を使用すると、計画がはるかに悪くなります。オプティマイザーは、MERGE JOINがパーティション関数によってシャーディングできることを認識するほど賢くなく、このヒントを適用すると、クエリに最大46秒かかります。とてもイライラする!

@gbnこれはおそらく、オプティマイザーがハッシュ結合を最初に行う理由でしょうか?

@gpattersonなんてイライラする!:)

ユニオンを介して手動でパーティション分割を強制すると(つまり、他の同様のクエリと短いクエリが結合された場合)、どうなりますか?
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.