如何强制带有 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);

第一个明显的变体是使用两个显式语句: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,但要确保 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,它应该知道行数。


事实上,即使您使用两个明确的 DELETEINSERT 语句而不是单个 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);