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