J'ai une base de données Azure SQL qui alimente une application API .NET Core. La navigation dans les rapports de présentation des performances dans le portail Azure suggère que la majorité de la charge (utilisation des DTU) sur mon serveur de base de données provient du processeur, et une requête en particulier:
Comme nous pouvons le voir, la requête 3780 est responsable de la quasi-totalité de l'utilisation du processeur sur le serveur.
Cela a un certain sens, car la requête 3780 (voir ci-dessous) est essentiellement le nœud de l'application et est appelée assez souvent par les utilisateurs. Il s'agit également d'une requête assez complexe avec de nombreuses jointures nécessaires pour obtenir le jeu de données approprié. La requête provient d'un sproc qui finit par ressembler à ceci:
-- @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
)
)
Si vous vous en souciez, la source complète de cette base de données peut être trouvée sur GitHub ici . Sources de la requête ci-dessus:
- https://github.com/bitwarden/core/blob/master/src/Sql/dbo/Stored%20Procedures/CipherDetails_ReadByUserId.sql
- https://github.com/bitwarden/core/blob/master/src/Sql/dbo/Functions/UserCipherDetails.sql
- https://github.com/bitwarden/core/blob/master/src/Sql/dbo/Functions/CipherDetails.sql
J'ai passé du temps sur cette requête au cours des mois à ajuster le plan d'exécution du mieux que je sais, pour finir avec son état actuel. Les requêtes avec ce plan d'exécution sont rapides sur des millions de lignes (<1 sec), mais comme indiqué ci-dessus, consomment de plus en plus de CPU du serveur à mesure que l'application grandit.
J'ai joint le plan de requête réel ci-dessous (je ne suis pas sûr d'une autre façon de partager cela ici sur l'échange de pile), qui montre une exécution du sproc en production contre un ensemble de données renvoyé de ~ 400 résultats.
Quelques points sur lesquels je souhaite des éclaircissements:
Index Seek on
[IX_Cipher_UserId_Type_IncludeAll]
prend 57% du coût total du plan. Ma compréhension du plan est que ce coût est lié aux E / S, ce qui fait que la table Cipher contient des millions d'enregistrements. Cependant, les rapports de performances d'Azure SQL me montrent que mes problèmes proviennent du processeur de cette requête, pas des E / S, donc je ne sais pas si c'est réellement un problème ou non. De plus, il effectue déjà une recherche d'index ici, donc je ne suis pas vraiment sûr qu'il y ait une marge d'amélioration.Les opérations Hash Match de toutes les jointures semblent être ce qui montre une utilisation significative du processeur dans le plan (je pense?), Mais je ne suis pas vraiment sûr de savoir comment cela pourrait être amélioré. La nature complexe de la façon dont j'ai besoin d'obtenir les données nécessite de nombreuses jointures sur plusieurs tables. Je court-circuite déjà plusieurs de ces jointures si possible (sur la base des résultats d'une jointure précédente) dans leurs
ON
clauses.
Téléchargez le plan d'exécution complet ici: https://www.dropbox.com/s/lua1awsc0uz1lo9/CipherDetails_ReadByUserId.sqlplan?dl=0
Je sens que je peux obtenir de meilleures performances CPU de cette requête, mais je suis à un stade où je ne suis pas sûr de savoir comment continuer le réglage du plan d'exécution. Quelles autres optimisations pourraient être nécessaires pour réduire la charge du processeur? Quelles opérations du plan d'exécution sont les pires contrevenants à l'utilisation du processeur?
UNION ALL
(une pourC.[UserId] = @UserId
et une pourC.[UserId] IS NULL AND ...
). Cela a réduit les ensembles de résultats de jointure et supprimé le besoin de correspondances de hachage (faisant désormais des boucles imbriquées sur les petits ensembles de jointures). La requête est désormais bien meilleure sur CPU. Je vous remercie!Réponse du wiki communautaire :
Vous pouvez essayer de diviser cela en deux requêtes et de les
UNION ALL
ré-assembler.Votre
WHERE
clause se produit tout à la fin, mais si vous la divisez en:C.[UserId] = @UserId
C.[UserId] IS NULL AND OU.[Status] = 2 AND O.[Enabled] = 1
... chacun pourrait avoir un plan suffisamment bon pour que cela en vaille la peine.
Si chaque requête applique le prédicat au début du plan, vous n'auriez pas à joindre autant de lignes qui sont finalement filtrées.
la source