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が警告している「予期しない結果または正しくない結果」とは何ですか?
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 ║
╚════════════╩════════╝
実行計画は次のとおりです。
両方のテーブルが完全にスキャンされていることに注意してください。ターゲットテーブルで影響を受けるのは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
;
これにより、より最適な計画で正しい結果(繰り返されない)が生成されます。
プランは、ターゲットテーブルからカテゴリ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;