我为与朋友合作的Facebook应用程序制作了MS SQL 2014数据库。我正在为DB中的所有用户保留朋友,并在应用开始时从Facebook更新它们。为此,我使用了MERGE语句(表变量@FriendUserIds包含朋友ID的列表;表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);
答案 0 :(得分:4)
尝试使用公用表格表达式(CTE)作为“目标”:
;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。使用CTE 可以导致问题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);
答案 1 :(得分:0)
第一个明显的变体是使用两个显式语句: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
,但要确保表变量具有主键/集群唯一索引。它可能有助于优化器。
表类型的定义如下所示:
CREATE TYPE [dbo].[UserIdsTableType] AS TABLE
(
[UserId] [int] NOT NULL,
PRIMARY KEY CLUSTERED
(
[UserId] ASC
))
第三种变体是使用#temp表而不是表变量再次使用主键/聚簇唯一索引。它可能会进一步帮助优化器,因为表变量的基数估计值与普通表或临时表的基数估计值不同。它通常为1,即优化器不知道表变量中有多少行,并假设它总是1行。对于临时表,它应该知道行数。
事实上,即使您使用两个明确的DELETE
和INSERT
语句而不是单个MERGE
,第三个变体也有意义。
使用临时表和使用临时表的两个单独语句的实际计划来查看MERGE
的实际执行计划会很有趣。理论上单个MERGE
可能更快,因为它可能只需要连接两个表一次。