Kut*_*ike 74 sql-server t-sql merge
我正在尝试使用MERGE语句从表中插入或删除行,但我只想对这些行的一个子集进行操作。的文档MERGE有一个措辞非常强烈的警告:
仅指定目标表中用于匹配目的的列很重要。也就是说,指定目标表中与源表的相应列进行比较的列。不要试图通过在 ON 子句中过滤掉目标表中的行来提高查询性能,例如通过指定 AND NOT target_table.column_x = value。这样做可能会返回意外和不正确的结果。
但这正是我必须做的事情才能完成我的MERGE工作。
我拥有的数据是一个标准的项目到类别的多对多连接表(例如,哪些项目包含在哪些类别中),如下所示:
CategoryId ItemId
========== ======
1 1
1 2
1 3
2 1
2 3
3 5
3 6
4 5
Run Code Online (Sandbox Code Playgroud)
我需要做的是用新的项目列表有效地替换特定类别中的所有行。我最初的尝试是这样的:
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 ;
Run Code Online (Sandbox Code Playgroud)
这似乎在我的测试中有效,但我正在做 MSDN 明确警告我不要做的事情。这让我担心以后会遇到意想不到的问题,但我看不到任何其他方法可以使我MERGE只影响具有特定字段值 ( CategoryId = 2) 的行并忽略来自其他类别的行。
有没有“更正确”的方法来达到同样的结果?MSDN 警告我的“意外或不正确的结果”是什么?
Pau*_*ite 109
该MERGE语句具有复杂的语法和更复杂的实现,但本质上的想法是连接两个表,过滤到需要更改(插入、更新或删除)的行,然后执行请求的更改。给定以下示例数据:
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);
Run Code Online (Sandbox Code Playgroud)
目标
???????????????????????
? CategoryId ? ItemId ?
???????????????????????
? 1 ? 1 ?
? 2 ? 1 ?
? 1 ? 2 ?
? 1 ? 3 ?
? 2 ? 3 ?
? 3 ? 5 ?
? 4 ? 5 ?
? 3 ? 6 ?
???????????????????????
Run Code Online (Sandbox Code Playgroud)
来源
???????????????????????
? CategoryId ? ItemId ?
???????????????????????
? 2 ? 2 ?
???????????????????????
Run Code Online (Sandbox Code Playgroud)
期望的结果是用来自源的数据替换目标中的数据,但仅限于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
;
Run Code Online (Sandbox Code Playgroud)
这给出了以下结果:
?????????????????????????????????
? $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 ?
???????????????????????
Run Code Online (Sandbox Code Playgroud)
执行计划是:

请注意,两个表都被完全扫描。我们可能认为这效率低下,因为只有CategoryId = 2目标表中的行会受到影响。这就是 Books Online 中警告的来源。 一种试图优化以仅触及目标中必要行的错误尝试是:
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
;
Run Code Online (Sandbox Code Playgroud)
ON子句中的逻辑作为连接的一部分应用。在这种情况下,联接是完全外部联接(有关原因,请参阅此联机丛书条目)。作为外部联接的一部分,在目标行上应用类别 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 ?
???????????????????????
Run Code Online (Sandbox Code Playgroud)
根本原因与谓词在外连接ON子句中的行为与在子句中指定时的行为不同的原因相同WHERE。该MERGE语法(并根据规定的条款中加入执行)只是使它很难看到,这是如此。
联机丛书中的指南(在优化性能条目中进行了扩展)提供的指南将确保使用MERGE语法表达正确的语义,而用户不必了解所有实现细节,或说明优化器可能合法地重新排列的方式出于执行效率原因的事情。
该文档提供了三种实现早期过滤的潜在方法:
在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
;
Run Code Online (Sandbox Code Playgroud)
这会以更优化的计划产生正确的结果(不重复):

该计划仅从目标表中读取类别 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;
Run Code Online (Sandbox Code Playgroud)