Cosa sta causando un elevato utilizzo della CPU da questo piano di query / esecuzione?


9

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:

inserisci qui la descrizione dell'immagine

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:

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 ONclausole.

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?

Risposte:


4

È possibile visualizzare la CPU a livello di operatore e le metriche del tempo trascorso in SQL Server Management Studio, anche se non posso dire quanto siano affidabili per le query che finiscono con la stessa velocità della tua. Il piano ha solo operatori in modalità riga, quindi le metriche temporali si applicano a tale operatore e agli operatori nella sottostruttura sottostante. Utilizzando come esempio il join del ciclo nidificato, SQL Server indica che l'intero sottostruttura impiegava 60 ms di tempo CPU e 80 ms di tempo trascorso:

costi di sottostruttura

La maggior parte del tempo di sottotree viene impiegato nella ricerca dell'indice. L'indice cerca anche la CPU. Sembra che il tuo indice abbia esattamente le colonne necessarie, quindi non è chiaro come potresti ridurre i costi della CPU di quell'operatore. Oltre alla ricerca, la maggior parte del tempo della CPU nel piano viene impiegato per le partite di hash che implementano i tuoi join.

Questa è un'enorme semplificazione eccessiva, ma la CPU presa da quei join hash dipenderà dalla dimensione dell'input per la tabella hash e dal numero di righe elaborate sul lato del probe. Osservando alcune cose su questo piano di query:

  • Al massimo 461 righe restituite hanno C.[UserId] = @UserId. Queste righe non si preoccupano affatto dei join.
  • Per le righe che necessitano dei join, SQL Server non è in grado di applicare anticipatamente alcun filtro (ad eccezione di OU.[UserId] = @UserId).
  • Quasi tutte le righe elaborate vengono eliminate vicino alla fine del piano di query (lettura da destra a sinistra) dal filtro: [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)

Sarebbe più naturale scrivere la tua query come a UNION ALL. La prima metà di UNION ALLpuò includere righe dove C.[UserId] = @UserIde la seconda metà può includere righe dove C.[UserId] IS NULL. Stai già facendo due ricerche sull'indice [dbo].[Cipher](una per @UserIde una per NULL), quindi sembra improbabile che la UNION ALLversione sia più lenta. Scrivere le query separatamente ti consentirà di eseguire alcuni dei filtri in anticipo, sia sul lato build che su quello del probe. Le query possono essere più veloci se devono elaborare meno dati intermedi.

Non so se la tua versione di SQL Server lo supporta, ma se ciò non ti aiuta potresti provare ad aggiungere un indice columnstore alla tua query per rendere i tuoi hash join idonei per la modalità batch . Il mio modo preferito è quello di creare una tabella vuota con un CCI su di essa e di unire a sinistra a quella tabella. I join hash possono essere molto più efficienti quando vengono eseguiti in modalità batch rispetto alla modalità riga.


Come suggerito, sono stato in grado di riscrivere lo sproc con 2 query che UNION ALL(una per C.[UserId] = @UserIde una per C.[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!
kspearrin,

0

Risposta wiki della community :

Potresti provare a dividerlo in due query e UNION ALLrimetterle insieme.

La tua WHEREclausola sta accadendo tutto alla fine, ma se la dividi in:

  • Una query dove C.[UserId] = @UserId
  • Un altro dove C.[UserId] IS NULL AND OU.[Status] = 2 AND O.[Enabled] = 1

... ognuno potrebbe avere un piano abbastanza buono per far valere la pena.

Se ogni query applica il predicato all'inizio del piano, non dovresti unire così tante righe che alla fine vengono filtrate.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.