Ho un database SQL di Azure che supporta un'app per le API .NET Core. L'esplorazione dei rapporti sulla panoramica delle prestazioni nel portale di Azure suggerisce che la maggior parte del carico (utilizzo DTU) sul mio server di database proviene dalla CPU e una query in particolare:
Come possiamo vedere, la query 3780 è responsabile di quasi tutto l'utilizzo della CPU sul server.
Questo in qualche modo ha senso, poiché la query 3780 (vedi sotto) è sostanzialmente l'intero punto cruciale dell'applicazione e viene chiamata dagli utenti abbastanza spesso. È anche una query piuttosto complessa con molti join necessari per ottenere il set di dati corretto necessario. La query proviene da uno sproc che finisce così:
-- @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
)
)
Se ti interessa, la fonte completa per questo database può essere trovata su GitHub qui . Fonti dalla query sopra:
- 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
Ho passato un po 'di tempo su questa query nel corso dei mesi, ottimizzando il piano di esecuzione come meglio so, finendo con il suo stato attuale. Le query con questo piano di esecuzione sono veloci su milioni di righe (<1 secondo), ma come notato sopra, stanno consumando sempre più CPU del server man mano che l'applicazione aumenta di dimensioni.
Ho allegato il piano di query effettivo di seguito (non sono sicuro di nessun altro modo per condividerlo qui sullo scambio di stack), che mostra un'esecuzione dello sproc in produzione su un set di dati restituito di ~ 400 risultati.
Alcuni punti cerco chiarimenti su:
La ricerca dell'indice
[IX_Cipher_UserId_Type_IncludeAll]
richiede il 57% del costo totale del piano. La mia comprensione del piano è che questo costo è legato all'IO, il che rende la tabella Cipher che contiene milioni di record. Tuttavia, i report sulle prestazioni di Azure SQL mi mostrano che i miei problemi derivano dalla CPU in questa query, non da I / O, quindi non sono sicuro che si tratti effettivamente di un problema o meno. Inoltre sta già cercando un indice qui, quindi non sono sicuro che ci sia spazio per miglioramenti.Le operazioni di Hash Match di tutti i join sembrano essere ciò che sta mostrando un utilizzo significativo della CPU nel piano (penso?), Ma non sono sicuro di come ciò possa essere migliorato. La natura complessa di come devo ottenere i dati richiede molti join su più tabelle. Ho già messo in corto circuito molti di questi join, se possibile (in base ai risultati di un join precedente) nelle loro
ON
clausole.
Scarica il piano di esecuzione completo qui: https://www.dropbox.com/s/lua1awsc0uz1lo9/CipherDetails_ReadByUserId.sqlplan?dl=0
Sento di poter ottenere migliori prestazioni della CPU da questa query, ma sono in una fase in cui non sono sicuro di come procedere ulteriormente sulla messa a punto del piano di esecuzione. Quali altre ottimizzazioni potrebbero essere necessarie per ridurre il carico della CPU? Quali operazioni nel piano di esecuzione sono i peggiori trasgressori dell'uso della CPU?
UNION ALL
(una perC.[UserId] = @UserId
e una perC.[UserId] IS NULL AND ...
). Ciò ha ridotto i set di risultati dei join e rimosso del tutto la necessità di corrispondenze hash (ora eseguendo cicli nidificati su set di join piccoli). La query ora è molto meglio sulla CPU. Grazie!