【问题标题】:Date comparison slow for large number of joined rows大量连接行的日期比较慢
【发布时间】:2022-01-21 23:42:25
【问题描述】:

以下查询将Snippets 分组为ChannelId 并返回UnreadSnippetCount

为了确定UnreadSnippetCount,将Channel 加入ChannelUsers 以获取User 上次读取Channel 的日期,并使用此LastReadDate 将计数限制为sn 所在的行-p 是在用户最后一次阅读频道之后创建的。

    SELECT c.Id, COUNT(s.Id) as [UnreadSnippetCount] 
    FROM Channels c
    INNER JOIN ChannelUsers cu
        ON cu.ChannelId = c.Id
    LEFT JOIN Snippets s
        ON cu.ChannelId = s.ChannelId
        AND s.CreatedByUserId <> @UserId
    WHERE cu.UserId = @UserId
    AND (cu.LastReadDate IS NULL OR s.CreatedDate > cu.LastReadDate)
    AND c.Id IN (select value from STRING_SPLIT(@ChannelIds, ','))

    GROUP BY c.Id

查询在逻辑上运行良好,但对于具有大量 Snippets (97691) 的 Channels,查询可能需要 10 分钟或更长时间才能返回。

创建以下索引:

CREATE NONCLUSTERED INDEX [IX_Snippets_CreatedDate] ON [dbo].[Snippets]
(
    [CreatedDate] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, DROP_EXISTING = OFF, ONLINE = OFF, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
GO

更新:

查询执行计划(原始查询):

https://www.brentozar.com/pastetheplan/?id=B19sI105F

更新 2

按照建议将where 子句移入join

    SELECT c.Id, COUNT(s.Id) as [UnreadSnippetCount] 
    FROM Channels c
    INNER JOIN ChannelUsers cu
        ON cu.ChannelId = c.Id
    LEFT JOIN Snippets s
        ON cu.ChannelId = s.ChannelId
        AND s.CreatedByUserId <> @UserId
        AND s.CreatedDate > cu.LastReadDate
    WHERE cu.UserId = @UserId
    AND c.Id IN (select value from STRING_SPLIT(@ChannelIds, ',')

产生这个执行计划:

https://www.brentozar.com/pastetheplan/?id=HkqwFk0ct

我可以使用更好的日期比较方法吗?

更新 3 - 解决方案

索引


CREATE NONCLUSTERED INDEX [IX_Snippet_Created] ON [dbo].[Snippets]
  (ChannelId ASC, CreatedDate ASC) INCLUDE (CreatedByUserId);

存储过程

ALTER PROCEDURE [dbo].[GetUnreadSnippetCounts2]
(
    @ChannelIds ChannelIdsType READONLY,
    @UserId nvarchar(36)
)
AS
    SET NOCOUNT ON

SELECT
    c.Id,
    COUNT(s.Id) as [UnreadSnippetCount] 
FROM Channels c
JOIN @ChannelIds cid
    ON cid.Id = c.Id
INNER JOIN ChannelUsers cu
    ON cu.ChannelId = c.Id
    AND cu.UserId = @UserId
JOIN Snippets s
    ON cu.ChannelId = s.ChannelId
    AND s.CreatedByUserId <> @UserId
    AND (cu.LastReadDate IS NULL OR s.CreatedDate > cu.LastReadDate)
GROUP BY c.Id;

这会在逻辑上给出正确的结果并快速返回。

产生的执行计划:

https://www.brentozar.com/pastetheplan/?id=S1GwRCCcK

【问题讨论】:

  • 考虑从string_split(@channelIds) 输出创建一个临时表(带有聚集索引),而不是IN。然后内部加入临时表。而不是使用IN 子句
  • 旁注,不要使用单引号 (') 作为别名。单引号用于文字字符串,而不是分隔标识对象名称。一些为别名使用单引号的方法已被弃用,仅在您定义它们时才有效,在其他地方无效; ORDER BY 'Quantity'不会按别名为 'Quantity' 的列排序。坚持使用不需要分隔标识的对象和别名,如果您必须对它们进行分隔标识,请使用 T-SQL 标识符、方括号 ([]) 或 ANSI-SQL 的双引号 ( ")。
  • 看看这个子句,你可能会在ChannelIdCreatedDateINCLUDECreatedByUserId上使用INDEX会更好;因为它使用的是&lt;&gt;,所以不太可能使用搜索。如果Id 也不是您的CLUSTERED INDEX,那么也将其包含在INCLUDE 中。
  • STRING_SPLIT 肯定会影响性能。同样AND (cu.LastReadDate IS NULL OR s.CreatedDate &gt; cu.LastReadDate) 似乎也不正确,如果LastReadDate 不为空,这将导致INNER JOIN 效果。您可能应该将第二个条件移至 ON
  • @PrebenHuybrechts 该链接不相关,因为STRING_SPLIT 是 TVF 而不是标量 UDF。它仍然存在问题,但原因不同:缺乏统计、排序和唯一性保证

标签: sql sql-server


【解决方案1】:

我可以在查询计划中看到许多效率低下的地方。

  1. 使用STRING_SPLIT 意味着编译器不知道返回了多少值,或者它们是唯一的,并且数据类型不匹配。理想情况下,你会传入一个表值参数,但是如果你不能这样做,那么另一种解决方案是将它们转储到表变量中

    DECLARE @tmp TABLE (Id int PRIMARY KEY);
    INSERT @tmp (Id)
    select value
    from STRING_SPLIT(@ChannelIds, ',')
    
  2. 您需要对Snippets 进行更好的索引。我会建议以下

    CREATE NONCLUSTERED INDEX [IX_Snippet_Created] ON [dbo].[Snippets]
      (ChannelId ASC, CreatedDate ASC) INCLUDE (CreatedByUserId);
    

    CreatedByUserId 放在键中是没有意义的,因为它是一个不等式。保存在INCLUDE

  3. 正如您已经被告知的,最好将条件(对于左连接表)移至ON 子句。我不知道你是否还需要cu.LastReadDate IS NULL 支票,我已经把它留在里面了。

  4. 我必须说,我不清楚你的架构,但INNER JOIN ChannelUsers cu 在这里感觉不对,也许应该是LEFT JOIN?如果没有看到您的完整设置和所需的输出,我不能再多说。

SELECT
  c.Id,
  COUNT(s.Id) as [UnreadSnippetCount] 
FROM Channels c
JOIN @tmp t
    ON t.Id = c.Id
INNER JOIN ChannelUsers cu
    ON cu.ChannelId = c.Id
    AND cu.UserId = @UserId
LEFT JOIN Snippets s
    ON cu.ChannelId = s.ChannelId
    AND s.CreatedByUserId <> @UserId
    AND (cu.LastReadDate IS NULL OR s.CreatedDate > cu.LastReadDate)
GROUP BY c.Id;

【讨论】:

  • 只有在有 @UserId 的 ChannelUser 记录时才应该返回频道(即用户是频道的成员)。然而,用户可能没有读过频道,所以LastReadDate 将为空。如果他们从未读过频道,则所有Snippets 都未读。如果他们已经读过频道,那么在LastReadDate 之后创建的所有Snippets 都是未读的。这就是为什么我需要LastReadDate IS NULL OR,否则如果LastReadDate 为空,Snippets 将不会包含在count 中,对吧?
  • TableVariables 也很糟糕,它们总是会估计 1 行。建议使用临时表,然后你有统计数据。
  • @DanCook 是的,听起来不错。但是您可能不需要INNER JOIN,因为您没有从ChannelUsers 获取任何数据
  • 进行了最后的调整。存储过程现在很火,谢谢大家
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-02-08
相关资源
最近更新 更多