.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の実行を示しています。
明確化を求めているいくつかのポイント:
インデックスシーク[IX_Cipher_UserId_Type_IncludeAll]
は、計画の総コストの57%を占めます。プランについての私の理解は、このコストはIOに関連していると理解しています。これは、Cipherテーブルに数百万のレコードが含まれているためです。ただし、Azure SQLパフォーマンスレポートでは、この問題はIOではなく、このクエリのCPUに起因することが示されているため、これが実際に問題であるかどうかはわかりません。さらに、ここではすでにインデックスシークを行っているため、改善の余地があるかどうかは本当にわかりません。
すべての結合からのハッシュマッチ操作は、計画でかなりのCPU使用率を示しているように見えますが(私はそう思いますか?)、これをどのように改善できるかは本当にわかりません。データを取得する方法が複雑であるため、複数のテーブルにまたがる多くの結合が必要になります。 ON
句で、可能であれば(以前の結合の結果に基づいて)これらの結合の多くを短絡しています。
ここで完全な実行計画をダウンロードしてください: https://www.dropbox.com/s/lua1awsc0uz1lo9/CipherDetails_ReadByUserId.sqlplan?dl=
このクエリからCPUパフォーマンスを向上させることができるように感じますが、実行プランのチューニングをさらに進める方法がわからない段階にあります。 CPUの負荷を減らすために、他にどのような最適化を行う必要がありますか?実行計画のどの操作がCPU使用率の最悪の違反者ですか?
SQL Server Management StudioでオペレーターレベルのCPUと経過時間のメトリックを表示できますが、クエリの処理がユーザーの処理速度と同じくらい速く終了した場合の信頼性はわかりません。プランには行モードの演算子しかないため、時間メトリックはその演算子とその下のサブツリーの演算子に適用されます。ネストされたループ結合を例として使用すると、SQL Serverは、サブツリー全体が60ミリ秒のCPU時間と80ミリ秒の経過時間を要したことを示しています。
そのサブツリー時間のほとんどは、インデックスシークに費やされます。インデックスシークにはCPUも必要です。インデックスには必要な列が正確に含まれているようです。そのため、その演算子のCPUコストを削減する方法が明確ではありません。シーク以外では、プランのほとんどのCPU時間は、結合を実装するハッシュ一致に費やされます。
これは非常に単純化しすぎですが、それらのハッシュ結合によって使用されるCPUは、ハッシュテーブルの入力のサイズとプローブ側で処理される行数に依存します。このクエリプランについていくつかの点を確認します。
C.[UserId] = @UserId
_が含まれています。これらの行は結合をまったく気にしません。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]
_で2つのインデックスシーク(1つは_@UserId
_、もう1つはNULL)を既に実行しているため、_UNION ALL
_バージョンが遅くなる可能性は低いようです。クエリを個別に作成すると、ビルド側とプローブ側の両方で、フィルタリングの一部を早期に実行できます。より少ない中間データを処理する必要がある場合、クエリはより高速になります。
SQL Serverのバージョンがこれをサポートしているかどうかはわかりませんが、それでも問題が解決しない場合は、クエリに列ストアインデックスを追加してハッシュ結合を作成してみてください バッチモードの対象 。私が好む方法は、CCIを含む空のテーブルを作成し、そのテーブルに左結合することです。ハッシュ結合は、行モードと比較して、バッチモードで実行した方がはるかに効率的です。
これを2つのクエリに分割して、それらをUNION ALL
- ingしてみてください。
WHERE
句は最後にすべて発生しますが、次のように分割した場合:
C.[UserId] = @UserId
C.[UserId] IS NULL AND OU.[Status] = 2 AND O.[Enabled] = 1
...それぞれに、それを価値のあるものにするのに十分な計画があるかもしれません。
各クエリが計画の早い段階で述語を適用する場合、最終的にフィルターで除外されるほど多くの行を結合する必要はありません。