このクエリ/実行プランからCPU使用率が高くなっている原因は何ですか?


9

.NET Core APIアプリを強化するAzure SQLデータベースがあります。Azure Portalでパフォーマンス概要レポートを参照すると、データベースサーバーの負荷(DTU使用量)の大部分がCPUからのものであり、具体的には1つのクエリが原因であることがわかります。

ここに画像の説明を入力してください

ご覧のように、クエリ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
        )
)

気になれば、このデータベースの完全なソースはGitHubこちらにあります。上記のクエリのソース:

私はこのクエリに数か月かけていくつかの時間を費やして実行計画を調整しましたが、それが現在の状態に至るまでの最良の方法です。この実行プランのクエリは、数百万行(<1秒)にわたって高速ですが、前述のように、アプリケーションのサイズが大きくなるにつれて、サーバーのCPUをどんどん消費しています。

以下の実際のクエリプランを添付しました(ここでスタック交換でそれを共有する他の方法がわからない)。これは、返された約400件のデータセットに対して本番環境でのsprocの実行を示しています。

明確化を求めているいくつかのポイント:

  • Index Seek on [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使用率の最悪の違反者ですか?

回答:


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。あなたはすでに2つのインデックスシーク[dbo].[Cipher](1 @UserIdつはNULL、もう1つは)を実行しているため、UNION ALLバージョンが遅くなる可能性は低いと思われます。クエリを個別に作成すると、ビルド側とプローブ側の両方で、フィルタリングの一部を早期に実行できます。より少ない中間データを処理する必要がある場合、クエリはより高速になります。

SQL Serverのバージョンがこれをサポートしているかどうかはわかりませんが、それでも問題が解決しない場合は、クエリに列ストアインデックスを追加して、ハッシュ結合をバッチモードで使用できるようにしてみてください。私が好む方法は、CCIを含む空のテーブルを作成し、そのテーブルに左結合することです。ハッシュ結合は、行モードと比較して、バッチモードで実行した方がはるかに効率的です。


提案したように、2つのクエリUNION ALL(1 つはとC.[UserId] = @UserId1つC.[UserId] IS NULL AND ...)でsprocを書き直すことができました。これにより、結合結果セットが減り、ハッシュ一致の必要性が完全になくなりました(小さな結合セットでネストされたループを実行するようになりました)。クエリはCPUではるかに優れています。ありがとうございました!
kspearrin

0

コミュニティWikiの回答

これを2つのクエリに分割し、UNION ALLそれらを一緒に戻してみてください。

あなたのWHERE条項は最後に起こっていますが、それを次のように分割した場合:

  • 1つのクエリ C.[UserId] = @UserId
  • 別の場所 C.[UserId] IS NULL AND OU.[Status] = 2 AND O.[Enabled] = 1

...それぞれに、それを価値のあるものにするのに十分な計画があるかもしれません。

各クエリが計画の早い段階で述語を適用する場合、最終的にフィルターで除外されるほど多くの行を結合する必要はありません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.