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

ksp*_*rin 9 performance sql-server execution-plan azure-sql-database cpu query-performance

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

在此处输入图片说明

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

这在某种程度上是有道理的,因为查询 3780(见下文)基本上是整个应用程序的关键,并且经常被用户调用。这也是一个相当复杂的查询,需要许多连接才能获得所需的正确数据集。查询来自一个最终看起来像这样的 sproc:

-- @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
        )
)
Run Code Online (Sandbox Code Playgroud)

如果您关心,可以在此处的 GitHub 上找到此数据库的完整源代码。来自上述查询的来源:

几个月来,我花了一些时间在这个查询上,尽我所知调整执行计划,最终得到它的当前状态。使用此执行计划的查询在数百万行(< 1 秒)中速度很快,但如上所述,随着应用程序大小的增长,服务器 CPU 的消耗越来越大。

我在下面附上了实际的查询计划(不确定在堆栈交换中是否有任何其他方式可以在此处共享该计划),它显示了在生产中针对大约 400 个结果的返回数据集执行 sproc。

我正在寻求澄清的一些要点:

  • Index Seek on[IX_Cipher_UserId_Type_IncludeAll]占计划总成本的 57%。我对计划的理解是这个成本与IO有关,这使得Cipher表包含数百万条记录。但是,Azure SQL 性能报告显示我的问题源于此查询的 CPU,而不是 IO,所以我不确定这是否真的是一个问题。此外,它已经在这里进行了索引查找,所以我不确定是否有任何改进的余地。

  • 来自所有连接的哈希匹配操作似乎表明计划中 CPU 使用率很高(我认为?),但我不确定如何做得更好。我需要如何获取数据的复杂性质需要跨多个表进行大量连接。如果可能,我已经在它们的ON子句中短路了其中的许多连接(基于先前连接的结果)。

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

我觉得我可以从这个查询中获得更好的 CPU 性能,但我处于一个阶段,我不确定如何进一步调整执行计划。还可以进行哪些其他优化来降低 CPU 负载?执行计划中的哪些操作对 CPU 使用率影响最大?

Joe*_*ish 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可以包括行 where C.[UserId] = @UserId,后半部分可以包括行 where C.[UserId] IS NULL。您已经在进行两次索引搜索[dbo].[Cipher](一次针对 NULL @UserId,一次针对 NULL),因此该UNION ALL版本似乎不太可能会变慢。单独写出查询将允许您在构建和探测方面尽早进行一些过滤。如果需要处理较少的中间数据,查询会更快。

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