Leistungsproblem bei rekursiven SQL-Abfragen [geschlossen]

9

Dies ist das sechste Mal, dass ich versuche, diese Frage zu stellen, und es ist auch die kürzeste. Alle vorherigen Versuche führten zu etwas, das eher einem Blog-Beitrag ähnelt als der Frage selbst, aber ich versichere Ihnen, dass mein Problem real ist, es betrifft nur ein großes Thema und ohne all diese Details wird es diese Frage enthalten nicht klar, was mein Problem ist. Also los geht's ...

Abstrakt

Ich habe eine Datenbank, die das Speichern von Daten auf ausgefallene Weise ermöglicht und einige nicht standardmäßige Funktionen bietet, die für meinen Geschäftsprozess erforderlich sind. Die Funktionen sind die folgenden:

  1. Zerstörungsfreie und nicht blockierende Aktualisierungen / Löschungen, die über einen Nur-Einfügen-Ansatz implementiert wurden und Datenwiederherstellung und automatische Protokollierung ermöglichen (jede Änderung ist an den Benutzer gebunden, der diese Änderung vorgenommen hat).
  2. Multiversionsdaten (es können mehrere Versionen derselben Daten vorhanden sein)
  3. Berechtigungen auf Datenbankebene
  4. Eventuelle Übereinstimmung mit der ACID-Spezifikation und transaktionssicheres Erstellen / Aktualisieren / Löschen
  5. Sie können Ihre aktuelle Datenansicht jederzeit zurückspulen oder vorspulen.

Möglicherweise habe ich andere Funktionen vergessen.

Datenbankstruktur

Alle Benutzerdaten werden in der ItemsTabelle als JSON-codierte Zeichenfolge ( ntext) gespeichert . Alle Datenbankoperationen werden über zwei gespeicherte Prozeduren ausgeführt GetLatestund InsertSnashotermöglichen die Verarbeitung von Daten, die der Funktionsweise von Quelldateien durch GIT ähneln.

Die resultierenden Daten werden im Frontend zu einem vollständig verknüpften Diagramm verknüpft (verbunden), sodass in den meisten Fällen keine Datenbankabfragen erforderlich sind.

Es ist auch möglich, Daten in regulären SQL-Spalten zu speichern, anstatt sie in Json-codierter Form zu speichern. Dies erhöht jedoch die Gesamtkomplexitätsbelastung.

Daten lesen

GetLatestErgebnisse mit Daten in Form von Anweisungen, betrachten Sie das folgende Diagramm zur Erklärung:

Strukturdiagramm

Das Diagramm zeigt eine Entwicklung von Änderungen, die jemals an einem einzelnen Datensatz vorgenommen wurden. Die Pfeile im Diagramm zeigen die Version an, auf deren Grundlage die Bearbeitung erfolgt (Stellen Sie sich vor, der Benutzer aktualisiert einige Daten offline, parallel zu Aktualisierungen, die vom Online-Benutzer vorgenommen wurden. In diesem Fall würde es zu Konflikten kommen, bei denen es sich im Grunde um zwei Datenversionen handelt statt eins).

Das Aufrufen GetLatestinnerhalb der folgenden Eingabezeiträume führt zu folgenden Datensatzversionen:

GetLatest 0, 15  => 1       <= The data is created upon it's first occurance
GetLatest 0, 25  => 2       <= Inserting another version on top of first one overwrites the existing version
GetLatest 0, 30  => 3       <= The overwrite takes place as soon as the data is inserted
GetLatest 0, 45  => 3, 4    <= This is where the conflict is introduced in the system
GetLatest 0, 55  => 4, 5    <= You can still edit all the versions
GetLatest 0, 65  => 4, 6    <= You can still edit all the versions
GetLatest 0, 75  => 4, 6, 7 <= You can also create additional conflicts
GetLatest 0, 85  => 4, 7, 8 <= You can still edit records
GetLatest 0, 95  => 7, 8, 9 <= You can still edit records
GetLatest 0, 105 => 7, 8    <= Inserting a record with `Json` equal to `NULL` means that the record is deleted
GetLatest 0, 115 => 8       <= Deleting the conflicting versions is the only conflict-resolution scenario
GetLatest 0, 125 => 8, X    <= The conflict can be based on the version that was already deleted.
GetLatest 0, 135 => 8, Y    <= You can delete such version too and both undelete another version on parallel within one Snapshot (or in several Snapshots).
GetLatest 0, 145 => 8       <= You can delete the undeleted versions by inserting NULL.
GetLatest 0, 155 => 8, Z    <= You can again undelete twice-deleted versions
GetLatest 0, 165 => 8       <= You can again delete three-times deleted versions
GetLatest 0, 10000 => 8     <= This means that in order to fast-forward view from moment 0 to moment `10000` you just have to expose record 8 to the user.
GetLatest 55, 115  => 8, [Remove 4], [Remove 5] <= At moment 55 there were two versions [4, 5] so in order to fast-forward to moment 115 the user has to delete versions 4 and 5 and introduce version 8. Please note that version 7 is not present in results since at moment 110 it got deleted.

Um für eine GetLatestsolche effiziente Schnittstelle jeder Datensatz speziellen Dienst zu unterstützen Attribute enthalten sollte BranchId, RecoveredOn, CreatedOn, UpdatedOnPrev, UpdatedOnCurr, UpdatedOnNext, , UpdatedOnNextIddie durch Verwendung von GetLatestum herauszufinden , ob der Datensatz angemessen in die Zeitspanne fällt vorgesehen GetLatestArgumente

Daten einfügen

Um die eventuelle Konsistenz, Transaktionssicherheit und Leistung zu unterstützen, werden Daten über ein spezielles mehrstufiges Verfahren in die Datenbank eingefügt.

  1. Die Daten werden nur in die Datenbank eingefügt, ohne dass sie von einer GetLatestgespeicherten Prozedur abgefragt werden können.

  2. Die Daten werden für die GetLatestgespeicherte Prozedur zur Verfügung gestellt , die Daten werden im normalisierten (dh denormalized = 0) Zustand zur Verfügung gestellt. Während die Daten in normalisierten Zustand sind, die Service - Felder BranchId, RecoveredOn, CreatedOn, UpdatedOnPrev, UpdatedOnCurr, UpdatedOnNext, UpdatedOnNextIdberechnet werden , die wirklich langsam ist.

  3. Um die Arbeit zu beschleunigen, werden die Daten denormalisiert, sobald sie für GetLatestgespeicherte Prozeduren verfügbar sind .

    • Da die Schritte 1, 2, 3 innerhalb verschiedener Transaktionen ausgeführt werden, ist es möglich, dass in der Mitte jeder Operation ein Hardwarefehler auftritt. Daten in einem Zwischenzustand belassen. Eine solche Situation ist normal und selbst wenn dies passieren sollte, werden die Daten innerhalb des folgenden InsertSnapshotAnrufs geheilt . Der Code für diesen Teil befindet sich zwischen den Schritten 2 und 3 der InsertSnapshotgespeicherten Prozedur.

Das Problem

Eine neue Funktion (vom Unternehmen erforderlich) zwang mich, die spezielle DenormalizerAnsicht zu überarbeiten , die alle Funktionen miteinander verbindet und für beide GetLatestund verwendet wird InsertSnapshot. Danach habe ich Leistungsprobleme. Wenn es ursprünglich SELECT * FROM Denormalizernur in Sekundenbruchteilen ausgeführt wurde, dauert es jetzt fast 5 Minuten, um 10000 Datensätze zu verarbeiten.

Ich bin kein DB-Profi und habe fast sechs Monate gebraucht, um die aktuelle Datenbankstruktur herauszubringen. Und ich habe zwei Wochen damit verbracht, zuerst die Refactorings durchzuführen und dann herauszufinden, was die Hauptursache für mein Leistungsproblem ist. Ich kann es einfach nicht finden. Ich stelle die Datenbanksicherung bereit (die Sie hier finden), da das Schema (mit allen Indizes) ziemlich groß ist, um in SqlFiddle zu passen. Die Datenbank enthält auch veraltete Daten (über 10000 Datensätze), die ich zu Testzwecken verwende . Außerdem stelle ich den Text für die DenormalizerAnsicht bereit, der überarbeitet und schmerzhaft langsam wurde:

ALTER VIEW [dbo].[Denormalizer]
AS
WITH Computed AS
(
    SELECT  currItem.Id,
            nextOperation.id AS NextId,
            prevOperation.FinishedOn AS PrevComputed,
            currOperation.FinishedOn AS CurrComputed,
            nextOperation.FinishedOn AS NextComputed

    FROM Items currItem 
    INNER JOIN dbo.Operations AS currOperation ON currItem.OperationId = currOperation.Id

    LEFT OUTER JOIN dbo.Items AS prevItem ON currItem.PreviousId = prevItem.Id
    LEFT OUTER JOIN dbo.Operations AS prevOperation ON prevItem.OperationId = prevOperation.Id 
    LEFT OUTER JOIN
    (
        SELECT MIN(I.id) as id, S.PreviousId, S.FinishedOn
        FROM Items I
        INNER JOIN
        (
            SELECT I.PreviousId, MIN(nxt.FinishedOn) AS FinishedOn
            FROM dbo.Items I
            LEFT OUTER JOIN dbo.Operations AS nxt ON I.OperationId = nxt.Id
            GROUP BY I.PreviousId
        ) AS S ON I.PreviousId = S.PreviousId 
        GROUP BY S.PreviousId, S.FinishedOn
    ) AS nextOperation ON nextOperation.PreviousId = currItem.Id

    WHERE currOperation.Finished = 1 AND currItem.Denormalized = 0
),

RecursionInitialization AS
(
    SELECT  currItem.Id,
            currItem.PreviousId,
            currItem.UUID,
            currItem.Json,
            currItem.TableName,
            currItem.OperationId,
            currItem.PermissionId,
            currItem.Denormalized,
            currItem.Id AS BranchID,
            COALESCE (C.PrevComputed, C.CurrComputed) AS CreatedOn,
            COALESCE (C.PrevComputed, CAST(0 AS BIGINT)) AS RecoveredOn,
            COALESCE (C.PrevComputed, CAST(0 AS BIGINT)) AS UpdatedOnPrev,
            C.CurrComputed AS UpdatedOnCurr,
            COALESCE (C.NextComputed, CAST(8640000000000000 AS BIGINT)) AS UpdatedOnNext,
            C.NextId AS UpdatedOnNextId,

            0 AS RecursionLevel

    FROM Items AS currItem
    INNER JOIN Computed AS C ON currItem.Id = C.Id
    WHERE currItem.Denormalized = 0

    UNION ALL

    SELECT  currItem.Id,
            currItem.PreviousId,
            currItem.UUID,
            currItem.Json,
            currItem.TableName,
            currItem.OperationId,
            currItem.PermissionId,
            currItem.Denormalized,
            currItem.BranchId,
            currItem.CreatedOn,
            currItem.RecoveredOn,
            currItem.UpdatedOnPrev,
            currItem.UpdatedOnCurr,
            currItem.UpdatedOnNext,
            currItem.UpdatedOnNextId,

            0 AS RecursionLevel

    FROM Items AS currItem
    WHERE currItem.Denormalized = 1
),
Recursion AS
(
    SELECT *
    FROM RecursionInitialization AS currItem

    UNION ALL

    SELECT  currItem.Id,
            currItem.PreviousId,
            currItem.UUID,
            currItem.Json,
            currItem.TableName,
            currItem.OperationId,
            currItem.PermissionId,
            currItem.Denormalized,

            CASE
                WHEN prevItem.UpdatedOnNextId = currItem.Id
                THEN prevItem.BranchID
                ELSE currItem.Id
            END AS BranchID,

            prevItem.CreatedOn AS CreatedOn,

            CASE
                WHEN prevItem.Json IS NULL
                THEN CASE
                            WHEN currItem.Json IS NULL
                            THEN prevItem.RecoveredOn
                            ELSE C.CurrComputed
                        END
                ELSE prevItem.RecoveredOn
            END AS RecoveredOn,

            prevItem.UpdatedOnCurr AS UpdatedOnPrev,

            C.CurrComputed AS UpdatedOnCurr,

            COALESCE (C.NextComputed, CAST(8640000000000000 AS BIGINT)) AS UpdatedOnNext,

            C.NextId,

            prevItem.RecursionLevel + 1 AS RecursionLevel
    FROM Items currItem
    INNER JOIN Computed C ON currItem.Id = C.Id
    INNER JOIN Recursion AS prevItem ON currItem.PreviousId = prevItem.Id
    WHERE currItem.Denormalized = 0
)
SELECT  item.Id,
        item.PreviousId,
        item.UUID,
        item.Json,
        item.TableName,
        item.OperationId,
        item.PermissionId,
        item.Denormalized,
        item.BranchID,
        item.CreatedOn,
        item.RecoveredOn,
        item.UpdatedOnPrev,
        item.UpdatedOnCurr,
        item.UpdatedOnNext,
        item.UpdatedOnNextId

FROM Recursion AS item
INNER JOIN
(
    SELECT Id, MAX(RecursionLevel) AS Recursion
    FROM Recursion AS item
    GROUP BY Id
) AS nested ON item.Id = nested.Id AND item.RecursionLevel = nested.Recursion
GO

Die Fragen)

Es werden zwei Szenarien berücksichtigt, die denormalisierten und die normalisierten Fälle:

  1. Wenn SELECT * FROM Denormalizerich auf die ursprüngliche Sicherung schaue, was das so schmerzhaft langsam macht, habe ich das Gefühl, dass es ein Problem mit dem rekursiven Teil der Denormalizer-Ansicht gibt. Ich habe versucht, die Leistung einzuschränken, denormalized = 1aber keine meiner Aktionen hat sie beeinträchtigt.

  2. Nach dem Ausführen UPDATE Items SET Denormalized = 0würde es machen GetLatestund SELECT * FROM Denormalizerläuft in (ursprünglich gedacht sein) langsam Szenario gibt es eine Möglichkeit , um Dinge zu beschleunigen , wenn wir Service Felder Berechnung BranchId, RecoveredOn, CreatedOn, UpdatedOnPrev, UpdatedOnCurr, UpdatedOnNext,UpdatedOnNextId

Danke im Voraus

PS

Ich versuche, mich an Standard-SQL zu halten, um die Abfrage für die Zukunft problemlos auf andere Datenbanken wie MySQL / Oracle / SQLite portierbar zu machen. Wenn es jedoch keine Standard-SQL gibt, die mir helfen könnte, kann ich mich an datenbankspezifische Konstrukte halten.

Lu4
quelle
1
In Bezug auf Standard-SQL und die von Ihnen aufgelisteten DBs: Sie verwenden hier einen CTE und diese werden von mySQL nicht unterstützt, und es gibt einige Syntaxvariationen zwischen den Hauptimplementierungen. Außerdem sind sie derzeit ein Optimierungszaun für Postgres, der ein großes Leistungsproblem darstellen könnte. Nichts davon sollte Sie davon abhalten, sie zu verwenden. Mit einem Baum in Form einer "Adjazenzliste" sind sie normalerweise das richtige Werkzeug für den Job. Wenn jedoch Kompatibilität ein Problem darstellt, sollten Sie diese Punkte beachten, bevor sie Sie beißen, damit Sie es können vorbereitet für zusätzliche Arbeiten, die erforderlich sind, wenn die Migration zu anderen DBMS Realität wird.
David Spillett
Vielen Dank, ich versuche, mich so weit wie möglich an Standard-SQL zu halten. Der Grund dafür ist, dass ich glaube, dass dies die Anzahl der Probleme in Zukunft verringern sollte, wenn vorhandener Code in andere Datenbanken migriert werden muss. Es ist nicht immer möglich. Es gibt auch einen Zeitfaktor, der Teil der Gleichung ist. Ich habe ein halbes Jahr damit verbracht, die aktuelle Datenbankstruktur festzulegen ... Ich möchte, dass sie nur Standardkonstrukte enthält, aber wenn es noch 10 Jahre dauert, ist es nicht der richtige Weg ... Wenn Sie also sehen, dass es mehr gibt Standard Refactoring möglich Ich werde es gerne akzeptieren ...
Lu4
1
Nein, das klingt nach einer pragmatischen Methode, um in solchen Fällen mit den unterschiedlichen Prioritäten und Komplikationen umzugehen. Ich habe nur die Probleme, die mir in den Sinn kamen, für den Fall, dass Sie noch nicht auf sie gestoßen sind, auf den Kopf gestellt (besser jetzt zu wissen, auch wenn es momentan nicht möglich / praktisch ist, etwas dagegen zu tun, als sich in der Produktion überraschen zu lassen! ).
David Spillett

Antworten:

9

@ Lu4 .. Ich habe dafür gestimmt, diese Frage als "Tip of Iceberg" zu schließen, aber wenn Sie einen Abfragehinweis verwenden, können Sie sie unter 1 Sek. Ausführen. Diese Abfrage kann überarbeitet und verwendet werden CROSS APPLY, ist jedoch ein Beratungsauftritt und keine Antwort auf eine Q & A-Site.

Ihre Abfrage wird auf meinem Server mit 4 CPUs und 16 GB RAM mehr als 13 Minuten lang ausgeführt.

Geben Sie hier die Bildbeschreibung ein

Ich habe Ihre Abfrage geändert OPTION(MERGE JOIN)und sie lief unter 1 Sekunde

set nocount on 
set statistics io on
set statistics time on
;WITH Computed AS
(
    SELECT  currItem.Id,
            nextOperation.id AS NextId,
            prevOperation.FinishedOn AS PrevComputed,
            currOperation.FinishedOn AS CurrComputed,
            nextOperation.FinishedOn AS NextComputed

    FROM Items currItem 
    INNER JOIN dbo.Operations AS currOperation ON currItem.OperationId = currOperation.Id

    LEFT OUTER JOIN dbo.Items AS prevItem ON currItem.PreviousId = prevItem.Id
    LEFT OUTER JOIN dbo.Operations AS prevOperation ON prevItem.OperationId = prevOperation.Id 
    LEFT OUTER JOIN
    (
        SELECT MIN(I.id) as id, S.PreviousId, S.FinishedOn
        FROM Items I
        INNER JOIN
        (
            SELECT I.PreviousId, MIN(nxt.FinishedOn) AS FinishedOn
            FROM dbo.Items I
            LEFT OUTER JOIN dbo.Operations AS nxt ON I.OperationId = nxt.Id
            GROUP BY I.PreviousId
        ) AS S ON I.PreviousId = S.PreviousId 
        GROUP BY S.PreviousId, S.FinishedOn
    ) AS nextOperation ON nextOperation.PreviousId = currItem.Id

    WHERE currOperation.Finished = 1 AND currItem.Denormalized = 0
),

RecursionInitialization AS
(
    SELECT  currItem.Id,
            currItem.PreviousId,
            currItem.UUID,
            currItem.Json,
            currItem.TableName,
            currItem.OperationId,
            currItem.PermissionId,
            currItem.Denormalized,
            currItem.Id AS BranchID,
            COALESCE (C.PrevComputed, C.CurrComputed) AS CreatedOn,
            COALESCE (C.PrevComputed, CAST(0 AS BIGINT)) AS RecoveredOn,
            COALESCE (C.PrevComputed, CAST(0 AS BIGINT)) AS UpdatedOnPrev,
            C.CurrComputed AS UpdatedOnCurr,
            COALESCE (C.NextComputed, CAST(8640000000000000 AS BIGINT)) AS UpdatedOnNext,
            C.NextId AS UpdatedOnNextId,

            0 AS RecursionLevel

    FROM Items AS currItem
    INNER JOIN Computed AS C ON currItem.Id = C.Id
    WHERE currItem.Denormalized = 0

    UNION ALL

    SELECT  currItem.Id,
            currItem.PreviousId,
            currItem.UUID,
            currItem.Json,
            currItem.TableName,
            currItem.OperationId,
            currItem.PermissionId,
            currItem.Denormalized,
            currItem.BranchId,
            currItem.CreatedOn,
            currItem.RecoveredOn,
            currItem.UpdatedOnPrev,
            currItem.UpdatedOnCurr,
            currItem.UpdatedOnNext,
            currItem.UpdatedOnNextId,

            0 AS RecursionLevel

    FROM Items AS currItem
    WHERE currItem.Denormalized = 1
),
Recursion AS
(
    SELECT *
    FROM RecursionInitialization AS currItem

    UNION ALL

    SELECT  currItem.Id,
            currItem.PreviousId,
            currItem.UUID,
            currItem.Json,
            currItem.TableName,
            currItem.OperationId,
            currItem.PermissionId,
            currItem.Denormalized,

            CASE
                WHEN prevItem.UpdatedOnNextId = currItem.Id
                THEN prevItem.BranchID
                ELSE currItem.Id
            END AS BranchID,

            prevItem.CreatedOn AS CreatedOn,

            CASE
                WHEN prevItem.Json IS NULL
                THEN CASE
                            WHEN currItem.Json IS NULL
                            THEN prevItem.RecoveredOn
                            ELSE C.CurrComputed
                        END
                ELSE prevItem.RecoveredOn
            END AS RecoveredOn,

            prevItem.UpdatedOnCurr AS UpdatedOnPrev,

            C.CurrComputed AS UpdatedOnCurr,

            COALESCE (C.NextComputed, CAST(8640000000000000 AS BIGINT)) AS UpdatedOnNext,

            C.NextId,

            prevItem.RecursionLevel + 1 AS RecursionLevel
    FROM Items currItem
    INNER JOIN Computed C ON currItem.Id = C.Id
    INNER JOIN Recursion AS prevItem ON currItem.PreviousId = prevItem.Id
    WHERE currItem.Denormalized = 0
)
SELECT  item.Id,
        item.PreviousId,
        item.UUID,
        item.Json,
        item.TableName,
        item.OperationId,
        item.PermissionId,
        item.Denormalized,
        item.BranchID,
        item.CreatedOn,
        item.RecoveredOn,
        item.UpdatedOnPrev,
        item.UpdatedOnCurr,
        item.UpdatedOnNext,
        item.UpdatedOnNextId

FROM Recursion AS item
INNER JOIN
(
    SELECT Id, MAX(RecursionLevel) AS Recursion
    FROM Recursion AS item
    GROUP BY Id
) AS nested ON item.Id = nested.Id AND item.RecursionLevel = nested.Recursion
OPTION (MERGE JOIN)

set nocount oFF 
set statistics io OFF
set statistics time OFF

Geben Sie hier die Bildbeschreibung ein

Beachten Sie, dass Sie in einer Ansicht keine Abfragehinweise verwenden können. Sie müssen daher eine Alternative finden, um Ihre Ansicht als SP oder als Problemumgehung zu erstellen

Kin Shah
quelle
1
Vielen Dank dafür, wenn man bedenkt, dass die Frage weit von den Stackoverflow-Standards entfernt ist und Ihre Bemühungen für mich doppelt wertvoll sind. Ich werde meine Hausaufgaben bei CROSS APPLY machen und versuchen, die OPTION (MERGE JOIN) herauszufinden. Es ist jetzt nicht offensichtlich, was das Problem mit dieser Abfrage zu sein scheint, aber ich bin mir ziemlich sicher, dass ich es herausfinden werde, danke noch einmal
Lu4
@ Lu4 Das Problem ist, dass der Abfrageoptimierer nicht den besten Ausführungsplan auswählt (oder generiert). Der Abfragehinweis in diesem Fall ermutigt den Optimierer, eine bestimmte Strategie zum Implementieren des Joins zu verwenden. Weitere Informationen finden Sie unter Join-Hinweise (Transact-SQL) .
Kenny Evitt
CROSS APPLYist großartig, aber ich würde vorschlagen, sich über Ausführungspläne und deren Analyse zu informieren, bevor Sie versuchen, Abfragehinweise zu verwenden.
Kenny Evitt