如何强制带有 DELETE 的 MERGE 语句使用索引查找?
How to force MERGE statement with DELETE to use index seek?
我为我的与朋友一起使用的 Facebook 应用程序制作了 MS SQL 2014 数据库。我在数据库中为我的所有用户保留朋友,并在应用程序启动时从 Facebook 更新他们。为此,我使用了 MERGE 语句(table 变量@FriendUserIds 包含朋友 ID 列表;table UserFriends 具有集群主键(UserId,FriendUserId)):
MERGE UserFriends
USING (
SELECT
UserId
FROM @FriendUserIds
) AS source (FriendUserId)
ON UserFriends.UserId = @UserId
AND UserFriends.FriendUserId = source.FriendUserId
WHEN NOT MATCHED BY TARGET
THEN INSERT (UserId, FriendUserId)
VALUES (@UserId, source.FriendUserId)
WHEN NOT MATCHED BY SOURCE
AND UserFriends.UserId = @UserId
THEN DELETE;
问题是查询优化器无法识别它可以对 UserFriends 使用 INDEX SEEK。它改用 SCAN,我不知道强制 SEEK 的方法。
现在我通过将操作分成两个查询(MERGE 用于添加新朋友和 DELETE 用于删除不再是朋友)来绕过这个问题,这仍然比单个 MERGE 语句(没有 DELETE 语句的 MERGE 使用 SEEK)快得多:
DELETE
FROM UserFriends
WHERE UserFriends.UserId = @UserId
AND UserFriends.FriendUserId NOT IN (
SELECT
UF.UserId
FROM @FriendUserIds UF
)
MERGE UserFriends
USING (
SELECT
UserId
FROM @FriendUserIds
) AS source (FriendUserId)
ON UserFriends.UserId = @UserId
AND UserFriends.FriendUserId = source.FriendUserId
WHEN NOT MATCHED BY TARGET
THEN INSERT (UserId, FriendUserId)
VALUES (@UserId, source.FriendUserId);
第一个明显的变体是使用两个显式语句:DELETE
和 INSERT
。您从不更新现有行,因此您可以使用传统的 INSERT
而不是 MERGE
.
DELETE FROM UserFriends
WHERE
UserFriends.UserId = @UserId
AND UserFriends.FriendUserId NOT IN
(
SELECT UF.UserId
FROM @FriendUserIds AS UF
)
;
INSERT INTO UserFriends(UserId, FriendUserId)
SELECT @UserId, UF.UserId
FROM @FriendUserIds AS UF
WHERE
UF.UserId NOT IN
(
SELECT UserFriends.FriendUserId
FROM UserFriends
WHERE UserFriends.UserId = @UserId
)
;
将其包装在事务中并 TRY ... CATCH
进行适当的错误处理。
第二种变体是尝试保持单一 MERGE
,但要确保 table 变量具有主 key/clustered 唯一索引。它可能有助于优化器。
table 类型的定义如下所示:
CREATE TYPE [dbo].[UserIdsTableType] AS TABLE
(
[UserId] [int] NOT NULL,
PRIMARY KEY CLUSTERED
(
[UserId] ASC
))
第三个变体是再次使用#temp table 而不是 table 变量,主 key/clustered 唯一索引。它可能进一步帮助优化器,因为 table 变量的基数估计与正常或临时 tables 的基数估计不同。它通常为 1,即优化器不知道 table 变量中有多少行,并假定它始终为 1 行。对于临时 tables,它应该知道行数。
事实上,即使您使用两个明确的 DELETE
和 INSERT
语句而不是单个 MERGE
.
,第三个变体也是有意义的
查看使用 temp table 的 MERGE
的实际执行计划与使用 temp table 的两个单独语句的实际计划会很有趣。理论上单个 MERGE
可能会更快,因为它可能只需要连接两个 table 一次。
尝试使用通用 Table 表达式 (CTE) 作为您的 "target":
;WITH UserFriends_CTE
AS (SELECT [UserID],
[FriendUserID]
FROM [UserFriends]
WHERE [UserID] = @UserId)
MERGE UserFriends_CTE
USING (SELECT [UserId]
FROM @FriendUserIds) AS source ([FriendUserId])
ON UserFriends_CTE.[UserId] = @UserId
AND UserFriends_CTE.[FriendUserId] = source.[FriendUserId]
WHEN NOT MATCHED BY TARGET THEN
INSERT ([UserId],
[FriendUserId])
VALUES (@UserId,
source.[FriendUserId])
WHEN NOT MATCHED BY SOURCE THEN
DELETE;
MERGE
语句往往比拆分成多条语句性能更差,there are a few known problems with MERGE. Using a CTE can cause issues according to Paul White in this answer,所以测试一下。
如果您确实使用拆分版本,我会按照以下方式实现它:
DELETE uf
FROM [UserFriends] uf
WHERE uf.[UserId] = @UserId
AND NOT EXISTS
(SELECT 1
FROM @FriendUserIds fu
WHERE uf.[FriendUserId] = fu.[FriendUserId]);
INSERT INTO [UserFriends]
([UserId],
[FriendUserId])
SELECT @UserId,
fu.[FriendUserId]
FROM @FriendUserIds fu
WHERE NOT EXISTS
(SELECT 1
FROM [UserFriends] uf
WHERE fu.[FriendUserId] = uf.[FriendUserId]
AND uf.[UserId] = @UserId);
我为我的与朋友一起使用的 Facebook 应用程序制作了 MS SQL 2014 数据库。我在数据库中为我的所有用户保留朋友,并在应用程序启动时从 Facebook 更新他们。为此,我使用了 MERGE 语句(table 变量@FriendUserIds 包含朋友 ID 列表;table UserFriends 具有集群主键(UserId,FriendUserId)):
MERGE UserFriends
USING (
SELECT
UserId
FROM @FriendUserIds
) AS source (FriendUserId)
ON UserFriends.UserId = @UserId
AND UserFriends.FriendUserId = source.FriendUserId
WHEN NOT MATCHED BY TARGET
THEN INSERT (UserId, FriendUserId)
VALUES (@UserId, source.FriendUserId)
WHEN NOT MATCHED BY SOURCE
AND UserFriends.UserId = @UserId
THEN DELETE;
问题是查询优化器无法识别它可以对 UserFriends 使用 INDEX SEEK。它改用 SCAN,我不知道强制 SEEK 的方法。 现在我通过将操作分成两个查询(MERGE 用于添加新朋友和 DELETE 用于删除不再是朋友)来绕过这个问题,这仍然比单个 MERGE 语句(没有 DELETE 语句的 MERGE 使用 SEEK)快得多:
DELETE
FROM UserFriends
WHERE UserFriends.UserId = @UserId
AND UserFriends.FriendUserId NOT IN (
SELECT
UF.UserId
FROM @FriendUserIds UF
)
MERGE UserFriends
USING (
SELECT
UserId
FROM @FriendUserIds
) AS source (FriendUserId)
ON UserFriends.UserId = @UserId
AND UserFriends.FriendUserId = source.FriendUserId
WHEN NOT MATCHED BY TARGET
THEN INSERT (UserId, FriendUserId)
VALUES (@UserId, source.FriendUserId);
第一个明显的变体是使用两个显式语句:DELETE
和 INSERT
。您从不更新现有行,因此您可以使用传统的 INSERT
而不是 MERGE
.
DELETE FROM UserFriends
WHERE
UserFriends.UserId = @UserId
AND UserFriends.FriendUserId NOT IN
(
SELECT UF.UserId
FROM @FriendUserIds AS UF
)
;
INSERT INTO UserFriends(UserId, FriendUserId)
SELECT @UserId, UF.UserId
FROM @FriendUserIds AS UF
WHERE
UF.UserId NOT IN
(
SELECT UserFriends.FriendUserId
FROM UserFriends
WHERE UserFriends.UserId = @UserId
)
;
将其包装在事务中并 TRY ... CATCH
进行适当的错误处理。
第二种变体是尝试保持单一 MERGE
,但要确保 table 变量具有主 key/clustered 唯一索引。它可能有助于优化器。
table 类型的定义如下所示:
CREATE TYPE [dbo].[UserIdsTableType] AS TABLE
(
[UserId] [int] NOT NULL,
PRIMARY KEY CLUSTERED
(
[UserId] ASC
))
第三个变体是再次使用#temp table 而不是 table 变量,主 key/clustered 唯一索引。它可能进一步帮助优化器,因为 table 变量的基数估计与正常或临时 tables 的基数估计不同。它通常为 1,即优化器不知道 table 变量中有多少行,并假定它始终为 1 行。对于临时 tables,它应该知道行数。
事实上,即使您使用两个明确的 DELETE
和 INSERT
语句而不是单个 MERGE
.
查看使用 temp table 的 MERGE
的实际执行计划与使用 temp table 的两个单独语句的实际计划会很有趣。理论上单个 MERGE
可能会更快,因为它可能只需要连接两个 table 一次。
尝试使用通用 Table 表达式 (CTE) 作为您的 "target":
;WITH UserFriends_CTE
AS (SELECT [UserID],
[FriendUserID]
FROM [UserFriends]
WHERE [UserID] = @UserId)
MERGE UserFriends_CTE
USING (SELECT [UserId]
FROM @FriendUserIds) AS source ([FriendUserId])
ON UserFriends_CTE.[UserId] = @UserId
AND UserFriends_CTE.[FriendUserId] = source.[FriendUserId]
WHEN NOT MATCHED BY TARGET THEN
INSERT ([UserId],
[FriendUserId])
VALUES (@UserId,
source.[FriendUserId])
WHEN NOT MATCHED BY SOURCE THEN
DELETE;
MERGE
语句往往比拆分成多条语句性能更差,there are a few known problems with MERGE. Using a CTE can cause issues according to Paul White in this answer,所以测试一下。
如果您确实使用拆分版本,我会按照以下方式实现它:
DELETE uf
FROM [UserFriends] uf
WHERE uf.[UserId] = @UserId
AND NOT EXISTS
(SELECT 1
FROM @FriendUserIds fu
WHERE uf.[FriendUserId] = fu.[FriendUserId]);
INSERT INTO [UserFriends]
([UserId],
[FriendUserId])
SELECT @UserId,
fu.[FriendUserId]
FROM @FriendUserIds fu
WHERE NOT EXISTS
(SELECT 1
FROM [UserFriends] uf
WHERE fu.[FriendUserId] = uf.[FriendUserId]
AND uf.[UserId] = @UserId);