web-dev-qa-db-ja.com

ターゲットテーブルのサブセットをマージします

MERGEステートメントを使用してテーブルに行を挿入または削除しようとしていますが、それらの行のサブセットのみを操作します。 MERGEのドキュメントには、非常に強力な警告が含まれています。

照合の目的で使用されるターゲットテーブルの列のみを指定することが重要です。つまり、ソース表の対応する列と比較されるターゲット表の列を指定します。 AND NOT target_table.column_x = valueを指定するなど、ON句でターゲットテーブルの行を除外してクエリのパフォーマンスを向上させないでください。これを行うと、予期しない不正な結果が返される場合があります。

これは、MERGEを機能させるために私がしなければならないこととまったく同じです。

私が持っているデータは、次のように、項目とカテゴリ(たとえば、どの項目がどのカテゴリに含まれるか)の標準の多対多結合テーブルです。

CategoryId   ItemId
==========   ======
1            1
1            2
1            3
2            1
2            3
3            5
3            6
4            5

特定のカテゴリのすべての行を新しいアイテムのリストで効果的に置き換える必要があります。これを行う最初の試みは次のようになります。

MERGE INTO CategoryItem AS TARGET
USING (
  SELECT ItemId FROM SomeExternalDataSource WHERE CategoryId = 2
) AS SOURCE
ON SOURCE.ItemId = TARGET.ItemId AND TARGET.CategoryId = 2
WHEN NOT MATCHED BY TARGET THEN
    INSERT ( CategoryId, ItemId )
    VALUES ( 2, ItemId )
WHEN NOT MATCHED BY SOURCE AND TARGET.CategoryId = 2 THEN
    DELETE ;

これは表示がテストで機能することを示していますが、MSDNが明示的に警告しないようにしています。これにより、後で予期しない問題が発生するのではないかと心配になりますが、MERGEを特定のフィールド値(CategoryId = 2)と他のカテゴリの行を無視します。

これと同じ結果を達成するための「より正確な」方法はありますか?そして、MSDNが警告している「予期しない結果または正しくない結果」とは何ですか?

73
KutuluMike

MERGEステートメントには複雑な構文とさらに複雑な実装がありますが、基本的には、2つのテーブルを結合し、変更(挿入、更新、または削除)する必要がある行にフィルターをかけ、次に要求された変更を実行します。次のサンプルデータがあるとします。

DECLARE @CategoryItem AS TABLE
(
    CategoryId  integer NOT NULL,
    ItemId      integer NOT NULL,

    PRIMARY KEY (CategoryId, ItemId),
    UNIQUE (ItemId, CategoryId)
);

DECLARE @DataSource AS TABLE
(
    CategoryId  integer NOT NULL,
    ItemId      integer NOT NULL

    PRIMARY KEY (CategoryId, ItemId)
);

INSERT @CategoryItem
    (CategoryId, ItemId)
VALUES
    (1, 1),
    (1, 2),
    (1, 3),
    (2, 1),
    (2, 3),
    (3, 5),
    (3, 6),
    (4, 5);

INSERT @DataSource
    (CategoryId, ItemId)
VALUES
    (2, 2);

ターゲット

╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║          1 ║      1 ║
║          2 ║      1 ║
║          1 ║      2 ║
║          1 ║      3 ║
║          2 ║      3 ║
║          3 ║      5 ║
║          4 ║      5 ║
║          3 ║      6 ║
╚════════════╩════════╝

ソース

╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║          2 ║      2 ║
╚════════════╩════════╝

望ましい結果は、ターゲットのデータをソースのデータで置き換えることですが、CategoryId = 2の場合のみです。上記のMERGEの説明に続いて、キーのみでソースとターゲットを結合するクエリを記述し、WHEN句の行のみをフィルター処理する必要があります。

MERGE INTO @CategoryItem AS TARGET
USING @DataSource AS SOURCE ON 
    SOURCE.ItemId = TARGET.ItemId 
    AND SOURCE.CategoryId = TARGET.CategoryId
WHEN NOT MATCHED BY SOURCE 
    AND TARGET.CategoryId = 2 
    THEN DELETE
WHEN NOT MATCHED BY TARGET 
    AND SOURCE.CategoryId = 2 
    THEN INSERT (CategoryId, ItemId)
        VALUES (CategoryId, ItemId)
OUTPUT 
    $ACTION, 
    ISNULL(INSERTED.CategoryId, DELETED.CategoryId) AS CategoryId,
    ISNULL(INSERTED.ItemId, DELETED.ItemId) AS ItemId
;

これにより、次の結果が得られます。

╔═════════╦════════════╦════════╗
║ $ACTION ║ CategoryId ║ ItemId ║
╠═════════╬════════════╬════════╣
║ DELETE  ║          2 ║      1 ║
║ INSERT  ║          2 ║      2 ║
║ DELETE  ║          2 ║      3 ║
╚═════════╩════════════╩════════╝
╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║          1 ║      1 ║
║          1 ║      2 ║
║          1 ║      3 ║
║          2 ║      2 ║
║          3 ║      5 ║
║          3 ║      6 ║
║          4 ║      5 ║
╚════════════╩════════╝

実行計画は次のとおりです。 Merge plan

両方のテーブルが完全にスキャンされていることに注意してください。ターゲットテーブルで影響を受けるのはCategoryId = 2の行のみであるため、これは非効率だと考えるかもしれません。これは、Books Onlineの警告が出てくる場所です。ターゲットの必要な行のみに触れるように最適化するための誤った試みの1つは、次のとおりです。

MERGE INTO @CategoryItem AS TARGET
USING 
(
    SELECT CategoryId, ItemId
    FROM @DataSource AS ds 
    WHERE CategoryId = 2
) AS SOURCE ON
    SOURCE.ItemId = TARGET.ItemId
    AND TARGET.CategoryId = 2
WHEN NOT MATCHED BY TARGET THEN
    INSERT (CategoryId, ItemId)
    VALUES (CategoryId, ItemId)
WHEN NOT MATCHED BY SOURCE THEN
    DELETE
OUTPUT 
    $ACTION, 
    ISNULL(INSERTED.CategoryId, DELETED.CategoryId) AS CategoryId,
    ISNULL(INSERTED.ItemId, DELETED.ItemId) AS ItemId
;

ON句のロジックは、結合の一部として適用されます。この場合、結合は完全外部結合です(理由は このBooks Onlineエントリ を参照)。外部結合の一部としてターゲット行のカテゴリ2のチェックを適用すると、最終的には異なる値の行が削除されます(ソースと一致しないため)。

╔═════════╦════════════╦════════╗
║ $ACTION ║ CategoryId ║ ItemId ║
╠═════════╬════════════╬════════╣
║ DELETE  ║          1 ║      1 ║
║ DELETE  ║          1 ║      2 ║
║ DELETE  ║          1 ║      3 ║
║ DELETE  ║          2 ║      1 ║
║ INSERT  ║          2 ║      2 ║
║ DELETE  ║          2 ║      3 ║
║ DELETE  ║          3 ║      5 ║
║ DELETE  ║          3 ║      6 ║
║ DELETE  ║          4 ║      5 ║
╚═════════╩════════════╩════════╝

╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║          2 ║      2 ║
╚════════════╩════════╝

根本的な原因は、ON句で指定された場合とは異なる、外部結合WHERE句での述語の動作が異なる同じ理由です。 MERGE構文(および指定された句に応じた結合の実装)では、これがそうであることがわかりにくくなっています。

Books OnlineのガイダンスOptimizing Performance エントリで展開)は、ユーザーが必ずしも必要なく、MERGE構文を使用して正しいセマンティクスが表現されることを保証するガイダンスを提供しますすべての実装の詳細を理解するか、オプティマイザが実行効率の理由から正当に再配置する方法を説明する必要があります。

ドキュメントには、早期フィルタリングを実装するための3つの潜在的な方法が示されています。

WHEN句でフィルター条件を指定すると正しい結果が保証されますが、ソースとターゲットからより多くの行が読み取られて処理される可能性があります厳密に必要なテーブルよりも(最初の例で見られるように)。

フィルター条件を含むビューを介した更新でも正しい結果が保証されます(ビューを介して更新するには変更された行にアクセスできる必要があるため)が、これには専用のビュー、およびビューを更新するための奇妙な条件に従うもの。

共通テーブル式を使用すると、ON句に述語を追加するのと同様のリスクがありますが、理由は少し異なります。多くの場合それは安全ですが、これを確認するには実行計画の専門家による分析が必要です(そして広範な実用的なテスト)。例えば:

WITH TARGET AS 
(
    SELECT * 
    FROM @CategoryItem
    WHERE CategoryId = 2
)
MERGE INTO TARGET
USING 
(
    SELECT CategoryId, ItemId
    FROM @DataSource
    WHERE CategoryId = 2
) AS SOURCE ON
    SOURCE.ItemId = TARGET.ItemId
    AND SOURCE.CategoryId = TARGET.CategoryId
WHEN NOT MATCHED BY TARGET THEN
    INSERT (CategoryId, ItemId)
    VALUES (CategoryId, ItemId)
WHEN NOT MATCHED BY SOURCE THEN
    DELETE
OUTPUT 
    $ACTION, 
    ISNULL(INSERTED.CategoryId, DELETED.CategoryId) AS CategoryId,
    ISNULL(INSERTED.ItemId, DELETED.ItemId) AS ItemId
;

これにより、より最適な計画で正しい結果(繰り返されない)が生成されます。

Merge plan 2

プランは、ターゲットテーブルからカテゴリ2の行のみを読み取ります。これは、ターゲットテーブルが大きい場合に重要なパフォーマンスの考慮事項になる可能性がありますが、MERGE構文を使用してこれを間違ってしまうことは非常に簡単です。

場合によっては、MERGEを個別のDML操作として書く方が簡単です。このアプローチは、単一のMERGEよりもパフォーマンスが優れていることさえあります。

DELETE ci
FROM @CategoryItem AS ci
WHERE ci.CategoryId = 2
AND NOT EXISTS 
(
    SELECT 1 
    FROM @DataSource AS ds 
    WHERE 
        ds.ItemId = ci.ItemId
        AND ds.CategoryId = ci.CategoryId
);

INSERT @CategoryItem
SELECT 
    ds.CategoryId, 
    ds.ItemId
FROM @DataSource AS ds
WHERE
    ds.CategoryId = 2;
105
Paul White 9