是什么导致此查询/执行计划的CPU使用率过高?


9

我有一个支持.NET Core API应用程序的Azure SQL数据库。浏览Azure门户中的性能概述报告表明,我的数据库服务器上的大部分负载(DTU使用情况)来自CPU,特别是一个查询:

在此处输入图片说明

如我们所见,查询3780几乎负责服务器上的所有CPU使用率。

这有点说得通,因为查询3780(见下文)基本上是应用程序的整个关键所在,并且用户经常调用它。这也是一个相当复杂的查询,需要许多联接才能获得所需的正确数据集。该查询来自一个存储库,最终看起来像这样:

-- @UserId UNIQUEIDENTIFIER

SELECT
    C.[Id],
    C.[UserId],
    C.[OrganizationId],
    C.[Type],
    C.[Data],
    C.[Attachments],
    C.[CreationDate],
    C.[RevisionDate],
    CASE
        WHEN
            @UserId IS NULL
            OR C.[Favorites] IS NULL
            OR JSON_VALUE(C.[Favorites], CONCAT('$."', @UserId, '"')) IS NULL
        THEN 0
        ELSE 1
    END [Favorite],
    CASE
        WHEN
            @UserId IS NULL
            OR C.[Folders] IS NULL
        THEN NULL
        ELSE TRY_CONVERT(UNIQUEIDENTIFIER, JSON_VALUE(C.[Folders], CONCAT('$."', @UserId, '"')))
    END [FolderId],
    CASE 
        WHEN C.[UserId] IS NOT NULL OR OU.[AccessAll] = 1 OR CU.[ReadOnly] = 0 OR G.[AccessAll] = 1 OR CG.[ReadOnly] = 0 THEN 1
        ELSE 0
    END [Edit],
    CASE 
        WHEN C.[UserId] IS NULL AND O.[UseTotp] = 1 THEN 1
        ELSE 0
    END [OrganizationUseTotp]
FROM
    [dbo].[Cipher] C
LEFT JOIN
    [dbo].[Organization] O ON C.[UserId] IS NULL AND O.[Id] = C.[OrganizationId]
LEFT JOIN
    [dbo].[OrganizationUser] OU ON OU.[OrganizationId] = O.[Id] AND OU.[UserId] = @UserId
LEFT JOIN
    [dbo].[CollectionCipher] CC ON C.[UserId] IS NULL AND OU.[AccessAll] = 0 AND CC.[CipherId] = C.[Id]
LEFT JOIN
    [dbo].[CollectionUser] CU ON CU.[CollectionId] = CC.[CollectionId] AND CU.[OrganizationUserId] = OU.[Id]
LEFT JOIN
    [dbo].[GroupUser] GU ON C.[UserId] IS NULL AND CU.[CollectionId] IS NULL AND OU.[AccessAll] = 0 AND GU.[OrganizationUserId] = OU.[Id]
LEFT JOIN
    [dbo].[Group] G ON G.[Id] = GU.[GroupId]
LEFT JOIN
    [dbo].[CollectionGroup] CG ON G.[AccessAll] = 0 AND CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId]
WHERE
    C.[UserId] = @UserId
    OR (
        C.[UserId] IS NULL
        AND OU.[Status] = 2
        AND O.[Enabled] = 1
        AND (
            OU.[AccessAll] = 1
            OR CU.[CollectionId] IS NOT NULL
            OR G.[AccessAll] = 1
            OR CG.[CollectionId] IS NOT NULL
        )
)

如果您愿意,可以在GitHub上找到此数据库的完整源代码。上面查询的来源:

在过去的几个月中,我花了一些时间在此查询上调整了执行计划,据我所知,它最终是当前状态。使用此执行计划的查询可以在数百万行(<1秒)内快速进行查询,但是如上所述,随着应用程序大小的增加,它们越来越消耗服务器CPU。

我在下面附加了实际的查询计划(不确定是否可以在堆栈交换中共享该计划),该计划显示了生产过程中存储过程对返回的约400个结果数据集的执行情况。

我需要澄清以下几点:

  • 寻求索引[IX_Cipher_UserId_Type_IncludeAll]的费用占计划总费用的57%。我对该计划的理解是,此成本与IO有关,这使得Cipher表包含数百万条记录。但是,Azure SQL性能报告向我显示我的问题是由该查询(而不是IO)上的CPU引起的,因此我不确定这是否确实是问题。另外,它已经在这里进行索引搜索,因此我不确定是否有任何改进的余地。

  • 所有联接的哈希匹配操作似乎表明计划中的CPU使用率很高(我认为?),但是我不确定如何将其改进。我需要获取数据的方式的复杂性要求在多个表之间进行大量联接。如果可能,我已经在其ON子句中短路了许多这些连接(基于先前连接的结果)。

在此处下载完整的执行计划:https : //www.dropbox.com/s/lua1awsc0uz1lo9/CipherDetails_ReadByUserId.sqlplan?dl=0

我觉得可以从该查询中获得更好的CPU性能,但是我目前尚不确定如何进一步调整执行计划。还有哪些其他优化措施可以减少CPU负载?执行计划中哪些操作最严重地影响了CPU使用率?

Answers:


4

您可以在SQL Server Management Studio中查看操作员级别的CPU和经过的时间指标,尽管我不能说它们对完成您的查询一样可靠。您的计划仅具有行模式运算符,因此时间指标适用于该运算符以及其下方子树中的运算符。以嵌套循环联接为例,SQL Server告诉您整个子树花费了60毫秒的CPU时间和80毫秒的经过时间:

子树成本

大部分子树时间都花在索引查找上。索引也要占用CPU。看来您的索引恰好具有所需的列,因此不清楚如何减少该运算符的CPU成本。除了寻找之外,计划中的大部分CPU时间都花在实现联接的哈希匹配上。

这是一个极大的简化,但是这些哈希联接所占用的CPU将取决于哈希表的输入大小以及在探测端处理的行数。观察有关此查询计划的一些事项:

  • 最多返回461行C.[UserId] = @UserId。这些行根本不关心联接。
  • 对于确实需要连接的行,SQL Server无法及早应用任何过滤(除外OU.[UserId] = @UserId)。
  • 过滤器几乎消除了查询计划末尾(从右到左读取)附近的所有已处理行: [vault].[dbo].[Cipher].[UserId] as [C].[UserId]=[@UserId] OR ([vault].[dbo].[OrganizationUser].[AccessAll] as [OU].[AccessAll]=(1) OR [vault].[dbo].[CollectionUser].[CollectionId] as [CU].[CollectionId] IS NOT NULL OR [vault].[dbo].[Group].[AccessAll] as [G].[AccessAll]=(1) OR [vault].[dbo].[CollectionGroup].[CollectionId] as [CG].[CollectionId] IS NOT NULL) AND [vault].[dbo].[Cipher].[UserId] as [C].[UserId] IS NULL AND [vault].[dbo].[OrganizationUser].[Status] as [OU].[Status]=(2) AND [vault].[dbo].[Organization].[Enabled] as [O].[Enabled]=(1)

将查询写为会更自然UNION ALL。的前半部分UNION ALL可以包含的行,C.[UserId] = @UserId而下半部分可以包含的行C.[UserId] IS NULL。您已经在执行两个索引查找[dbo].[Cipher](一个用于@UserIdNULL,一个用于NULL),因此该UNION ALL版本看起来不太可能变慢。分别写出查询将使您可以尽早在构建和探针方面进行一些过滤。如果查询需要处理较少的中间数据,则查询可能会更快。

我不知道您的SQL Server版本是否支持此功能,但是如果这样做没有帮助,您可以尝试向查询中添加列存储索引,以使您的哈希联接符合批处理模式。我的首选方法是创建一个带有CCI的空表,并使其与该表保持连接。与行模式相比,以联接方式运行时,哈希联接的效率更高。


如建议的那样,我能够使用2个查询UNION ALL(一个用于C.[UserId] = @UserId和一个用于C.[UserId] IS NULL AND ...)来重写该存储过程。这减少了联接结果集,并且完全不需要哈希匹配(现在在小型联接集上执行嵌套循环)。现在,查询在CPU上要好得多。谢谢!
kspearrin

0

社区Wiki答案

您可以尝试将其分为两个查询,然后将UNION ALL它们重新组合在一起。

您的WHERE子句会在末尾发生,但如果将其拆分为:

  • 一查询 C.[UserId] = @UserId
  • 另一个地方 C.[UserId] IS NULL AND OU.[Status] = 2 AND O.[Enabled] = 1

...每个人可能都有一个足够好的计划,值得花点时间。

如果每个查询都在计划的早期应用谓词,那么您不必连接太多最终被过滤掉的行。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.