如何使用DELETE强制MERGE语句使用索引查找?

时间:2016-01-15 12:18:03

标签: sql sql-server indexing merge seek

我为与朋友合作的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);

2 个答案:

答案 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)

第一个明显的变体是使用两个显式语句:DELETEINSERT。您永远不会更新现有行,因此您可以使用传统的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行。对于临时表,它应该知道行数。

事实上,即使您使用两个明确的DELETEINSERT语句而不是单个MERGE,第三个变体也有意义。

使用临时表和使用临时表的两个单独语句的实际计划来查看MERGE的实际执行计划会很有趣。理论上单个MERGE可能更快,因为它可能只需要连接两个表一次。