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
我需要做的是有效地用新的项目列表替换特定类别中的所有行。我最初尝试执行以下操作:
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向我警告的“意外或错误结果”是什么?
#1 楼
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);
目标
╔════════════╦════════╗
║ 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
所在的行。这是联机丛书中的警告出现的地方。一种错误的尝试来优化以仅接触目标中的必要行是: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
子句中的逻辑是作为联接的一部分应用。在这种情况下,该联接是完全外部联接(有关原因,请参阅此联机丛书条目)。作为外部联接的一部分,在目标行上应用类别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
语法(以及依赖于指定子句的join实现)只会使我们更是如此。在线丛书中的指南(在“优化性能”条目中进行了扩展)提供了一些指南,这些指南将确保使用
MERGE
语法表达正确的语义,而用户不必了解所有实现细节,也不必考虑优化器可能合法使用的方式。出于执行效率的原因而重新安排内容。该文档提供了三种实现早期筛选的可能方法:从源表和目标表读取和处理的行超过了严格必要的行(如第一个示例所示)。
通过包含过滤条件的视图进行更新也可以保证正确的结果(因为更改了行)必须可以通过该视图访问以进行更新),但这确实需要一个专用视图,并且该视图必须遵循更新视图的特殊条件。
使用通用表与将谓词添加到
WHEN
子句中相比,e表达式具有类似的风险,但是原因略有不同。在许多情况下,这是安全的,但需要对执行计划进行专家分析以确认这一点(以及广泛的实际测试)。例如: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的行。如果目标表很大,这可能是重要的性能考虑因素,但是使用
ON
语法很容易出错。有时,将
MERGE
编写为单独的DML操作更容易。这种方法甚至比单一的MERGE
效果更好,这一事实常常使人们感到惊讶。评论
我知道这是一个非常老的问题……但是您可以详细说明“使用公用表表达式与将谓词添加到ON子句具有相似的风险,但原因略有不同。”我知道BOL也有类似的模糊警告:“此方法类似于在ON子句中指定其他搜索条件,并且可能会产生不正确的结果。建议您避免使用此方法...”。 CTE方法似乎可以解决我的用例,但是我想知道是否存在我没有考虑的情况。
–李亨利
19年2月15日在21:54
评论
是的,如果文档具有“意外和错误结果”的具体示例,则该文档将更加有用。@SQLKiwi谢谢您的链接-如果从原始页面引用该文档,IMO文档会更好。
@ A-K mssqltips.com/sqlservertip/3074 / ...