Dav*_*ack 2 sql-server index-tuning performance-tuning
我继承了一个相当复杂的存储过程,它导致我们的生产环境超时。我使用 SQL Sentry Plan Explorer 来帮助我查看一些问题。我已经确定了一些,但我很难想出可以进行的优化。我们正在使用 SQL Server 2019 和COMPATIBILITY_VERSION = 150
.
该存储过程通过 API 调用将数据返回到 Web 客户端,因此页面加载的性能非常重要。通过多个应用程序分析会话,此存储过程已被确定为瓶颈。
以下是已发现的一些问题:
tempdb
溢出数据警告INSERT INTO @ValidRows....
但我不知道如何识别它们。所涉及的表上有索引,从我看来它们似乎足够了。但是,我看到许多索引扫描在计划中被称为问题区域(黄色突出显示)
以下是实际执行计划的链接:https://www.brentozar.com/pastetheplan/ ?id=S1TN4NOS9
CREATE VIEW [dbo].[DailyNotePublishedContentView]
WITH SCHEMABINDING
AS
SELECT
T.Id AS DailyNoteContentId,
T.DateModified,
T.ModifiedBy,
T.Region,
T.DateAdded,
T.CreatedBy,
V.Id AS VersionId,
V.DateAdded AS VersionDateAdded,
V.CreatedBy as VersionCreatedBy,
V.ContentType,
V.DateDue,
V.IsPrivate,
V.ProjectId,
V.PublishDate,
V.AuthorTeamId,
V.ContactEmail,
V.Content,
V.ContentText,
V.Summary,
V.SummaryText,
V.AllowAddendums,
StoreTeamsList = (SELECT STRING_AGG(CONVERT(VARCHAR(max),STA.[Name]), ', ')
FROM
[dbo].[DailyNoteContentStoreTeam] (NOLOCK) ST
INNER JOIN [dbo].[Audiences] (NOLOCK) STA ON STA.Id = ST.AudienceId AND STA.IsActive = 1
WHERE
ST.DailyNoteContentVersionId = V.Id
),
AssignedToList = (SELECT STRING_AGG(CONVERT(VARCHAR(max),TAA.[Name]), ', ')
FROM
[dbo].[DailyNoteContentAssignedTo] (NOLOCK) TA
INNER JOIN [dbo].[Audiences] (NOLOCK) TAA ON TAA.Id = TA.AudienceId AND TAA.IsActive = 1
WHERE
TA.DailyNoteContentVersionId = V.Id
)
FROM
[dbo].[DailyNoteContent] (NOLOCK) T
CROSS APPLY(
SELECT TOP 1
V.Id,
V.DateAdded,
V.CreatedBy,
V.ContentType,
V.DateDue,
V.IsPrivate,
V.ProjectId,
V.PublishDate,
V.AuthorTeamId,
V.ContactEmail,
V.Content,
V.ContentText,
V.Summary,
V.SummaryText,
V.AllowAddendums
FROM
[dbo].[DailyNoteContentVersion] (NOLOCK) V
WHERE
V.DailyNoteContentId = T.Id
ORDER BY V.DateAdded DESC
) AS V
-- Only inlcude published content
INNER JOIN [dbo].[DailyNotePublishStatus] (NOLOCK) PS ON
PS.PublishDate = V.PublishDate
AND PS.RegionId = T.Region
AND PS.IsReadyToPublish = 1
-- TODO: Account for user defined publish time/timezones?
AND PS.PublishDate <= CONVERT(DATE, GETUTCDATE())
WHERE
T.IsDeleted = 0
WITH CHECK OPTION;
GO
Run Code Online (Sandbox Code Playgroud)
查询的 11.5 秒运行时间中的 9.308 秒被提供的执行计划中节点 7 处的批处理模式哈希匹配右连接消耗。对于约 4M 构建侧行和约 13k 探针侧行来说,这是意想不到的。
最可能的解释是 ~30k 哈希溢出页面到tempdb,但即使这样也是不合理的性能。由于原始 I/O 性能不足或分配争用,您的tempdb很可能面临严重压力。如果可以的话,您应该调查并修复它。系统可能过度使用表变量和其他因素,给tempdb带来压力。
也就是说,泄漏应该是可以避免的。我从计划中注意到你禁用了内存授予反馈,也许是有充分理由的。尽管如此,一个根本原因是表变量的基数估计不准确。
您正在使用表变量延迟编译,但这只有这么多帮助。最初的估计很容易被证明是过时的,或者在计划的后续执行中不具有代表性。例如,@UserAudiencesTable 的估计为 20 行,但运行时遇到 157 行。
交叉联接(导致缺少联接谓词警告)是由优化器引入的,因为它试图OR EXISTS
在用“可选:针对用户位置”进行注释的查询部分中实现复杂的相关子句。不准确的基数估计会通过这些连接相乘,从而导致散列连接处的溢出。
这让我想到了另一个问题。该查询是一种经常被称为“厨房水槽”查询的类型 - 它尝试使用可选组件来容纳许多查询。表达式的存在IS [NOT] NULL OR
是这里的赠品。这些使得基数估计变得非常困难,从而影响最终计划的质量。如果将复杂的视图作为具有多个外连接、应用和子查询的查询的一部分,您就会得到糟糕的结果。
作为一个快速实验,我建议您添加OPTION (RECOMPILE)
到INSERT
查询中。这将为优化器提供一定的范围,通过嵌入变量的运行时值来简化事情,并使用表变量的实际运行时基数。与执行时间相比,该语句的编译时间为 318 毫秒,并不算过多,因此您甚至可能会发现目前这是一个“足够好”的解决方案。
使用重新编译提示的结果至少可以告诉您未来可能选择进行的改进的总体方向,例如使用精心编写的动态 SQL 只生成语句所需的部分。
如果这些表上的统计信息可能有利于计划质量,您还可以考虑在此处使用临时表而不是表变量。更好的索引(替换或添加到现有的聚集索引)也可能值得考虑。提供额外的索引可以使优化器更容易找到明显有效的计划。
剩余 I/O 不一定是问题,但它确实表明必须在查找操作之后应用谓词以进一步限定行。通过更好的索引,这可能可以避免,也可能无法避免。
总之,调整此查询和工作负载将是一项咨询活动,并且问题中没有足够的信息来冒险超出上面的一般评论,但它可能会给您一些可以追求的想法。
最终,使用表变量和在复杂视图上编写复杂查询与可靠的良好执行计划和系统性能并不是特别兼容。
您还应该考虑对当前 CU8(2020 年 10 月版本)之外的实例进行修补。我不能保证这会提高性能,但您可能会从过去 18 个月左右发布的一些改进和修复中受益。
哦,尝试消除这些NOLOCK
提示,它们不是神奇的性能增强器。如果您可以忍受读取脏数据等,请在 READ UNCOMMITTED 隔离下运行您的工作负载。
一定要调查一下你的tempdb情况。