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:
- 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).
- Multiversionsdaten (es können mehrere Versionen derselben Daten vorhanden sein)
- Berechtigungen auf Datenbankebene
- Eventuelle Übereinstimmung mit der ACID-Spezifikation und transaktionssicheres Erstellen / Aktualisieren / Löschen
- Sie können Ihre aktuelle Datenansicht jederzeit zurückspulen oder vorspulen.
Möglicherweise habe ich andere Funktionen vergessen.
Datenbankstruktur
Alle Benutzerdaten werden in der Items
Tabelle als JSON-codierte Zeichenfolge ( ntext
) gespeichert . Alle Datenbankoperationen werden über zwei gespeicherte Prozeduren ausgeführt GetLatest
und InsertSnashot
ermö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
GetLatest
Ergebnisse mit Daten in Form von Anweisungen, betrachten Sie das folgende Diagramm zur Erklärung:
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 GetLatest
innerhalb 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 GetLatest
solche effiziente Schnittstelle jeder Datensatz speziellen Dienst zu unterstützen Attribute enthalten sollte BranchId
, RecoveredOn
, CreatedOn
, UpdatedOnPrev
, UpdatedOnCurr
, UpdatedOnNext
, , UpdatedOnNextId
die durch Verwendung von GetLatest
um herauszufinden , ob der Datensatz angemessen in die Zeitspanne fällt vorgesehen GetLatest
Argumente
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.
Die Daten werden nur in die Datenbank eingefügt, ohne dass sie von einer
GetLatest
gespeicherten Prozedur abgefragt werden können.Die Daten werden für die
GetLatest
gespeicherte Prozedur zur Verfügung gestellt , die Daten werden im normalisierten (dhdenormalized = 0
) Zustand zur Verfügung gestellt. Während die Daten in normalisierten Zustand sind, die Service - FelderBranchId
,RecoveredOn
,CreatedOn
,UpdatedOnPrev
,UpdatedOnCurr
,UpdatedOnNext
,UpdatedOnNextId
berechnet werden , die wirklich langsam ist.Um die Arbeit zu beschleunigen, werden die Daten denormalisiert, sobald sie für
GetLatest
gespeicherte 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
InsertSnapshot
Anrufs geheilt . Der Code für diesen Teil befindet sich zwischen den Schritten 2 und 3 derInsertSnapshot
gespeicherten Prozedur.
- 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
Das Problem
Eine neue Funktion (vom Unternehmen erforderlich) zwang mich, die spezielle Denormalizer
Ansicht zu überarbeiten , die alle Funktionen miteinander verbindet und für beide GetLatest
und verwendet wird InsertSnapshot
. Danach habe ich Leistungsprobleme. Wenn es ursprünglich SELECT * FROM Denormalizer
nur 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 Denormalizer
Ansicht 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:
Wenn
SELECT * FROM Denormalizer
ich 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 = 1
aber keine meiner Aktionen hat sie beeinträchtigt.Nach dem Ausführen
UPDATE Items SET Denormalized = 0
würde es machenGetLatest
undSELECT * FROM Denormalizer
läuft in (ursprünglich gedacht sein) langsam Szenario gibt es eine Möglichkeit , um Dinge zu beschleunigen , wenn wir Service Felder BerechnungBranchId
,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.
Antworten:
@ 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.
Ich habe Ihre Abfrage geändert
OPTION(MERGE JOIN)
und sie lief unter 1 SekundeBeachten 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
quelle
CROSS APPLY
ist großartig, aber ich würde vorschlagen, sich über Ausführungspläne und deren Analyse zu informieren, bevor Sie versuchen, Abfragehinweise zu verwenden.