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


71

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が警告している「予期しないまたは誤った結果」とは何ですか?


はい、「予期しない、誤った結果」の具体的な例があれば、ドキュメントはより有用です。
AK

3
@AlexKuznetsov ここに例があります
ポールホワイト

@SQLKiwiのリンクに感謝します-IMOのドキュメントは、元のページから参照された場合、はるかに優れています。
AK

1
@AlexKuznetsov同意しました。残念ながら、2012年のBOLの再編成は、他の多くのことの中でも特にそれを破りました。これは、2008 R2のドキュメントで非常にうまくリンクされていました。
ポールホワイト

回答:


103

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 = 2MERGE上記の説明に従って、キーのみでソースとターゲットを結合するクエリを記述し、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 
╚════════════╩════════╝

実行計画は次のとおりです。 統合計画

両方のテーブルが完全にスキャンされていることに注意してください。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ガイダンスパフォーマンス最適化エントリで拡張)は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
;

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

マージプラン2

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

時には、MERGE個別のDML操作として記述する方が簡単です。このアプローチは、1つよりも優れたパフォーマンスを発揮でき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;

これは本当に古い質問です...しかし、「共通のテーブル式を使用すると、ON句に述語を追加する場合と同様のリスクがありますが、理由が少し異なります」について詳しく説明できます。BOLには、「この方法はON句で追加の検索条件を指定することに似ており、誤った結果を生成する可能性があります。この方法の使用を避けることをお勧めします...」CTEメソッドは私のユースケースを解決するように見えますが、検討していないシナリオがあるのではないかと思っています。
ヘンリーリー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.