Was verursacht eine hohe CPU-Auslastung durch diesen Abfrage- / Ausführungsplan?

9

Ich habe eine Azure SQL-Datenbank, die eine .NET Core API-App unterstützt. Das Durchsuchen der Leistungsübersichtsberichte im Azure-Portal weist darauf hin, dass der Großteil der Last (DTU-Nutzung) auf meinem Datenbankserver von der CPU stammt, und eine Abfrage speziell:

Geben Sie hier die Bildbeschreibung ein

Wie wir sehen können, ist die Abfrage 3780 für fast die gesamte CPU-Auslastung auf dem Server verantwortlich.

Dies ist etwas sinnvoll, da die Abfrage 3780 (siehe unten) im Grunde genommen den gesamten Kern der Anwendung darstellt und von Benutzern häufig aufgerufen wird. Es ist auch eine ziemlich komplexe Abfrage mit vielen Verknüpfungen, die erforderlich sind, um den richtigen Datensatz zu erhalten. Die Abfrage stammt von einem Sproc, der am Ende folgendermaßen aussieht:

-- @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
        )
)

Wenn Sie sich interessieren, finden Sie die vollständige Quelle für diese Datenbank auf GitHub hier . Quellen aus der obigen Abfrage:

Ich habe im Laufe der Monate einige Zeit mit dieser Abfrage verbracht, um den Ausführungsplan so gut wie möglich zu optimieren und den aktuellen Status zu erhalten. Abfragen mit diesem Ausführungsplan sind über Millionen von Zeilen (<1 Sek.) Schnell, verbrauchen jedoch, wie oben erwähnt, die Server-CPU immer mehr, je größer die Anwendung wird.

Ich habe den tatsächlichen Abfrageplan unten angehängt (ich bin mir nicht sicher, wie ich ihn hier beim Stapeltausch teilen kann), der eine Ausführung des Sproc in der Produktion anhand eines zurückgegebenen Datensatzes von ~ 400 Ergebnissen zeigt.

Einige Punkte, die ich klären möchte:

  • Index Seek on [IX_Cipher_UserId_Type_IncludeAll]übernimmt 57% der Gesamtkosten des Plans. Mein Verständnis des Plans ist, dass diese Kosten mit E / A zusammenhängen, was bedeutet, dass die Verschlüsselungstabelle Millionen von Datensätzen enthält. Azure SQL-Leistungsberichte zeigen mir jedoch, dass meine Probleme von der CPU bei dieser Abfrage und nicht von E / A herrühren. Daher bin ich mir nicht sicher, ob dies tatsächlich ein Problem ist oder nicht. Außerdem wird hier bereits eine Indexsuche durchgeführt, sodass ich nicht sicher bin, ob Verbesserungspotenzial besteht.

  • Die Hash-Match-Operationen aller Joins scheinen die signifikante CPU-Auslastung im Plan zu zeigen (glaube ich?), Aber ich bin mir nicht sicher, wie dies verbessert werden könnte. Die Komplexität, wie ich die Daten abrufen muss, erfordert viele Verknüpfungen über mehrere Tabellen hinweg. Ich habe bereits viele dieser Verknüpfungen, wenn möglich, (basierend auf den Ergebnissen einer vorherigen Verknüpfung) in ihren ONKlauseln kurzgeschlossen.

Laden Sie den vollständigen Ausführungsplan hier herunter: https://www.dropbox.com/s/lua1awsc0uz1lo9/CipherDetails_ReadByUserId.sqlplan?dl=0

Ich habe das Gefühl, dass ich mit dieser Abfrage eine bessere CPU-Leistung erzielen kann, bin jedoch in einem Stadium, in dem ich nicht sicher bin, wie ich den Ausführungsplan weiter optimieren soll. Welche anderen Optimierungen könnten erforderlich sein, um die CPU-Auslastung zu verringern? Welche Vorgänge im Ausführungsplan sind die schlimmsten Straftäter der CPU-Auslastung?

kspearrin
quelle

Antworten:

4

Sie können CPU- und abgelaufene Zeitmetriken auf Bedienerebene in SQL Server Management Studio anzeigen, obwohl ich nicht sagen kann, wie zuverlässig sie für Abfragen sind, die so schnell wie Ihre abgeschlossen werden. Ihr Plan enthält nur Zeilenmodusoperatoren, sodass die Zeitmetriken sowohl für diesen Operator als auch für die Operatoren im darunter liegenden Teilbaum gelten. Am Beispiel des Joins für verschachtelte Schleifen teilt SQL Server Ihnen mit, dass der gesamte Teilbaum 60 ms CPU-Zeit und 80 ms verstrichene Zeit benötigt hat:

Teilbaumkosten

Der größte Teil dieser Teilbaumzeit wird für die Indexsuche aufgewendet. Index sucht auch CPU. Es sieht so aus, als ob Ihr Index genau die benötigten Spalten enthält, sodass nicht klar ist, wie Sie die CPU-Kosten dieses Operators senken können. Abgesehen von den Suchvorgängen wird der größte Teil der CPU-Zeit im Plan für die Hash-Übereinstimmungen aufgewendet, die Ihre Joins implementieren.

Dies ist eine enorme Vereinfachung, aber die von diesen Hash-Joins belegte CPU hängt von der Größe der Eingabe für die Hash-Tabelle und der Anzahl der auf der Testseite verarbeiteten Zeilen ab. Beachten Sie einige Dinge zu diesem Abfrageplan:

  • Höchstens 461 zurückgegebene Zeilen haben C.[UserId] = @UserId. Diese Zeilen kümmern sich überhaupt nicht um die Verknüpfungen.
  • Für die Zeilen, für die Verknüpfungen erforderlich sind, kann SQL Server keine Filterung frühzeitig anwenden (außer OU.[UserId] = @UserId).
  • Nahezu alle verarbeiteten Zeilen werden gegen Ende des Abfrageplans (Lesen von rechts nach links) vom Filter entfernt: [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)

Es wäre natürlicher, Ihre Anfrage als zu schreiben UNION ALL. Die erste Hälfte der UNION ALLkann Zeilen enthalten, in denen C.[UserId] = @UserIdund die zweite Hälfte kann Zeilen enthalten, in denen C.[UserId] IS NULL. Sie führen bereits zwei Indexsuchen durch [dbo].[Cipher](eine für @UserIdund eine für NULL), sodass es unwahrscheinlich ist, dass die UNION ALLVersion langsamer ist. Wenn Sie die Abfragen separat ausschreiben, können Sie einen Teil der Filterung sowohl auf der Build- als auch auf der Testseite frühzeitig durchführen. Abfragen können schneller sein, wenn weniger Zwischendaten verarbeitet werden müssen.

Ich weiß nicht, ob Ihre Version von SQL Server dies unterstützt, aber wenn dies nicht hilft, können Sie versuchen, Ihrer Abfrage einen Columnstore-Index hinzuzufügen, damit Ihre Hash-Joins für den Batch-Modus geeignet sind . Meine bevorzugte Methode besteht darin, eine leere Tabelle mit einer CCI zu erstellen und diese Tabelle links zu verknüpfen. Hash-Joins können im Batch-Modus wesentlich effizienter sein als im Zeilenmodus.

Joe Obbish
quelle
Wie vorgeschlagen, konnte ich den Sproc mit zwei Abfragen neu schreiben UNION ALL(eine für C.[UserId] = @UserIdund eine für C.[UserId] IS NULL AND ...). Dies reduzierte die Join-Ergebnismengen und beseitigte die Notwendigkeit von Hash-Übereinstimmungen insgesamt (jetzt werden verschachtelte Schleifen für kleine Join-Mengen ausgeführt). Die Abfrage ist jetzt auf der CPU viel besser. Vielen Dank!
Kspearrin
0

Antwort des Community-Wikis :

Sie können versuchen, dies in zwei Abfragen aufzuteilen und UNION ALLsie wieder zusammenzufügen.

Ihre WHEREKlausel passiert alles am Ende, aber wenn Sie sie aufteilen in:

  • Eine Abfrage wo C.[UserId] = @UserId
  • Ein anderer wo C.[UserId] IS NULL AND OU.[Status] = 2 AND O.[Enabled] = 1

... jeder könnte einen Plan bekommen, der gut genug ist, damit es sich lohnt.

Wenn jede Abfrage das Prädikat zu Beginn des Plans anwendet, müssen Sie nicht so viele Zeilen verbinden, die letztendlich herausgefiltert werden.

user126897
quelle