Questa è la sesta volta che sto cercando di porre questa domanda ed è anche la più breve. Tutti i tentativi precedenti erano risultati con qualcosa di più simile a un post sul blog piuttosto che alla domanda stessa, ma ti assicuro che il mio problema è reale, è solo che riguarda un argomento di grandi dimensioni e senza tutti quei dettagli che questa domanda contiene sarà non è chiaro quale sia il mio problema. Quindi ecco qui ...
Astratto
Ho un database, permette di archiviare i dati in un modo un po 'elaborato e offre diverse funzionalità non standard richieste dal mio processo aziendale. Le caratteristiche sono le seguenti:
- Aggiornamenti / eliminazioni non distruttivi e non bloccanti implementati tramite approccio di solo inserimento, che consente il recupero dei dati e la registrazione automatica (ogni modifica è legata all'utente che ha apportato tale modifica)
- Dati multiversione (possono esistere più versioni degli stessi dati)
- Autorizzazioni a livello di database
- L'eventuale coerenza con le specifiche ACID e crea / aggiorna / elimina le transazioni sicure
- Possibilità di riavvolgere o avanzare rapidamente la visualizzazione corrente dei dati in qualsiasi momento.
Potrebbero esserci altre funzionalità che ho dimenticato di menzionare.
Struttura del database
Tutti i dati dell'utente sono memorizzati nella Itemstabella come stringa codificata JSON ( ntext). Tutte le operazioni del database sono condotte tramite due procedure memorizzate GetLateste InsertSnashotconsentono di operare su dati simili a come GIT gestisce i file di origine.
I dati risultanti sono collegati (JOINed) sul frontend in un grafico completamente collegato, quindi nella maggior parte dei casi non è necessario effettuare query sul database.
È anche possibile archiviare i dati in normali colonne SQL anziché archiviarli in forma codificata Json. Tuttavia, ciò aumenta la tensione complessiva della complessità.
Lettura dei dati
GetLatestrisultati con dati in forma di istruzioni, considerare il diagramma seguente per una spiegazione:
Il diagramma mostra un'evoluzione delle modifiche mai apportate a un singolo record. Le frecce sul diagramma mostrano la versione in base alla quale è avvenuta la modifica (Immagina che l'utente stia aggiornando alcuni dati offline, parallelamente agli aggiornamenti effettuati dall'utente online, un caso del genere introdurrebbe un conflitto, che è fondamentalmente due versioni dei dati invece di uno).
Pertanto, la chiamata GetLatestnei seguenti intervalli di tempo di input risulterà con le seguenti versioni di record:
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.
Affinché GetLatestsostenere tale interfaccia efficiente ogni record dovrebbe contenere attributi di servizio speciale BranchId, RecoveredOn, CreatedOn, UpdatedOnPrev, UpdatedOnCurr, UpdatedOnNext, UpdatedOnNextIdche vengono utilizzati per GetLatestcapire se il record cade adeguatamente nel periodo previsto GetLatestargomenti
Inserimento di dati
Al fine di supportare l'eventuale coerenza, sicurezza e prestazioni delle transazioni, i dati vengono inseriti nel database tramite una speciale procedura multistadio.
I dati vengono appena inseriti nel database senza poter essere interrogati dalla
GetLatestprocedura memorizzata.I dati sono resi disponibili per la
GetLatestprocedura memorizzata, i dati sono resi disponibili in stato normalizzato (cioèdenormalized = 0). Mentre i dati sono in stato normalizzato, i campi di servizioBranchId,RecoveredOn,CreatedOn,UpdatedOnPrev,UpdatedOnCurr,UpdatedOnNext,UpdatedOnNextIdvengono calcolate che è molto lento.Al fine di accelerare le cose, i dati vengono denormalizzati non appena resi disponibili per la
GetLatestprocedura memorizzata.- Poiché le fasi 1,2,3 sono state condotte all'interno di transazioni diverse, è possibile che si verifichi un errore hardware nel mezzo di ciascuna operazione. Lasciando i dati in uno stato intermedio. Tale situazione è normale e anche se accadrà, i dati verranno guariti durante la
InsertSnapshotchiamata successiva . Il codice per questa parte si trova tra i passaggi 2 e 3 dellaInsertSnapshotprocedura memorizzata.
- Poiché le fasi 1,2,3 sono state condotte all'interno di transazioni diverse, è possibile che si verifichi un errore hardware nel mezzo di ciascuna operazione. Lasciando i dati in uno stato intermedio. Tale situazione è normale e anche se accadrà, i dati verranno guariti durante la
Il problema
Una nuova funzionalità (richiesta dalle aziende) mi ha costretto a refactoring Denormalizervista speciale che lega tutte le funzionalità insieme e viene utilizzata per entrambi GetLateste InsertSnapshot. Successivamente ho iniziato a riscontrare problemi di prestazioni. Se originariamente SELECT * FROM Denormalizereseguito solo in frazioni di secondo, ora ci vogliono quasi 5 minuti per elaborare 10000 record.
Non sono un professionista del DB e mi ci sono voluti quasi sei mesi solo per uscire con l'attuale struttura del database. E ho trascorso due settimane prima a fare i refactoring e poi cercando di capire qual è la causa principale del mio problema di prestazioni. Non riesco proprio a trovarlo. Sto fornendo il backup del database (che puoi trovare qui) perché lo schema (con tutti gli indici) è piuttosto grande per adattarsi a SqlFiddle, il database contiene anche dati obsoleti (oltre 10000 record) che sto usando a scopo di test . Inoltre sto fornendo il testo per la Denormalizervista che è stato refactored e è diventato dolorosamente lento:
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
Le domande)
Esistono due scenari presi in considerazione, i casi denormalizzati e normalizzati:
Guardando al backup originale, ciò che rende
SELECT * FROM Denormalizercosì dolorosamente lento, mi sembra che ci sia un problema con la parte ricorsiva della vista Denormalizer, ho provato a limitaredenormalized = 1le prestazioni senza influire sulle mie azioni.Dopo l'esecuzione
UPDATE Items SET Denormalized = 0sarebbe fareGetLatesteSELECT * FROM Denormalizercorrere in (originariamente pensato per essere) scenario lento, c'è un modo per accelerare le cose fino quando siamo Computing campi di servizioBranchId,RecoveredOn,CreatedOn,UpdatedOnPrev,UpdatedOnCurr,UpdatedOnNext,UpdatedOnNextId
Grazie in anticipo
PS
Sto cercando di attenermi all'SQL standard per rendere la query facilmente trasportabile su altri database come MySQL / Oracle / SQLite per il futuro, ma se non ci sono sql standard che potrebbero aiutare, sto bene con l'adesione a costrutti specifici del database.


