【问题标题】:How to force MERGE statement with DELETE to use index seek?如何使用 DELETE 强制 MERGE 语句使用索引查找?
【发布时间】:2016-04-21 00:56:52
【问题描述】:

我已经为我的 Facebook 应用程序创建了 MS SQL 2014 数据库,可以与朋友一起使用。我在数据库中为我的所有用户保留朋友,并在应用程序启动时从 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);

【问题讨论】:

  • 您是否尝试过指定 MERGE 提示(MERGE UserFriends WITH(INDEX(<index to seek>)) ... 或 MERGE UserFriends WITH(FORCESEEK(<index to seek>)) ...?
  • 我试过 FORCESEEK。查询将失败 - 优化器无法找到可用的索引。
  • 尝试将 OPTION (RECOMPILE) 添加到合并语句的末尾。 SQL 优化器将使用您第一次运行它时指定的第一个 @UserId 来生成缓存计划。选项(重新编译)将使用实际值来生成计划,请注意,每次运行语句时重新编译都会产生少量开销。
  • 我尝试了多种变体,包括强制索引、强制搜索和 OPTION(RECOMPILE)。由于优化器已经使用索引,我希望它使用(聚集主键)强制它什么都不改变。强迫它去 SEEK 使它失败。 RECOMPILE 没有任何改变。在我看来,要么需要更改语法以让 oprimizer 知道它是那种查询,要么 SQL 服务器没有实现该选项。
  • 还有其他建议吗? :)

标签: sql sql-server indexing merge seek


【解决方案1】:

尝试使用公用表表达式 (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);

【讨论】:

  • 感谢您分享指向 Paul White 答案的链接。我今天学了些新东西。似乎 CTE 不能保证 MERGE 的正确结果,但只能通过视图进行更新。或者,当然有单独的DELETE 声明。
  • 在MERGE 语句中,由于您已经为UserFriends_CTE (MERGE UserFriends_CTE AS target) 设置了别名,因此您需要在后面的代码中使用target。对于 2 查询解决方案,我的问题中的解决方案(使用 MERGE 而不是 INSERT)更快。除此之外,您的 CTE 解决方案完美运行,我开始认为它无法完成 :)。赏金是你的。
【解决方案2】:

第一个明显的变体是使用两个显式语句: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 可能更快,因为它可能只需要连接两个表一次。

【讨论】:

  • 嘿@vladimir 如果内部查询列为NULLABLE,您是否希望不存在
  • @NeerajPrasadSharma,问题是表 UserFriends 具有聚集的主键 (UserId, FriendUserId),因此 UserId 和 FriendUserId 都必须不为 NULL。最有可能的是,如果您重写查询并使用EXISTS 而不是NOT IN,优化器无论如何都会生成相同的执行计划。
  • 在没有完全阅读问题的情况下就直接回答了问题,你就在现场
  • @Vladimir Baranov 用INSERT 代替MERGE 很好。我在测试中使用了MERGE,因此我可以确认DELETE 是导致扫描的部分。
  • @Vladimir Baranov UserIds 是集群的,PRIMARY KEY 在 UserId 上。关于#temp 表,我使用表变量,因为它是存储过程的参数。我真的不能用#temp 表替换它(我只是想看看它并没有改变图表,但确实改变了一些可以忽略不计的统计数据)。
猜你喜欢
  • 2012-01-01
  • 2017-05-22
  • 2018-10-18
  • 1970-01-01
  • 2019-10-07
  • 1970-01-01
  • 1970-01-01
  • 2013-06-30
  • 2012-05-23
相关资源
最近更新 更多