Problema di prestazioni della query sql ricorsivo [chiuso]


9

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:

  1. 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)
  2. Dati multiversione (possono esistere più versioni degli stessi dati)
  3. Autorizzazioni a livello di database
  4. L'eventuale coerenza con le specifiche ACID e crea / aggiorna / elimina le transazioni sicure
  5. 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:

Diagramma di struttura

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.

  1. I dati vengono appena inseriti nel database senza poter essere interrogati dalla GetLatestprocedura memorizzata.

  2. 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 servizio BranchId, RecoveredOn, CreatedOn, UpdatedOnPrev, UpdatedOnCurr, UpdatedOnNext, UpdatedOnNextIdvengono calcolate che è molto lento.

  3. 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 della InsertSnapshotprocedura memorizzata.

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:

  1. 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 limitare denormalized = 1le prestazioni senza influire sulle mie azioni.

  2. Dopo l'esecuzione UPDATE Items SET Denormalized = 0sarebbe fare GetLateste SELECT * FROM Denormalizercorrere in (originariamente pensato per essere) scenario lento, c'è un modo per accelerare le cose fino quando siamo Computing campi di servizio BranchId, 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.


1
Per quanto riguarda SQL standard e i DB che elenchi: stai usando un CTE qui e non sono supportati da mySQL e ci sono alcune variazioni di sintassi tra le principali implementazioni. Inoltre, attualmente rappresentano un recinto di ottimizzazione in Postgres che potrebbe rappresentare un grosso problema per le prestazioni. Niente di tutto ciò dovrebbe impedirti di usarli, con una struttura ad albero nella "lista di adiacenza" di solito sono lo strumento giusto per il lavoro, ma per quanto riguarda la compatibilità, questi punti sono cose da tenere presente prima che ti mordano, così puoi essere preparato per qualsiasi lavoro extra necessario quando la migrazione ad altri DBMS diventa realtà.
David Spillett,

Grazie, sto cercando di attenermi il più possibile allo standard SQL. La ragione di ciò è che credo che dovrebbe ridurre la quantità di problemi in futuro, quando sarà necessario migrare il codice esistente su altri database. Non è sempre possibile. C'è anche un fattore tempo che fa parte dell'equazione. Ho trascorso un anno e mezzo a sistemare l'attuale struttura del database ... Vorrei che contenesse solo costrutti standard, ma se ci vorranno altri 10 anni, non è la strada da percorrere ... Quindi se vedi che c'è di più possibile il refactoring standard Sarò felice di accettarlo ...
Lu4

1
No, sembra un modo pragmatico per affrontare le diverse priorità e complicazioni in casi come questo. Ho appena scaricato le problematiche che mi sono venute in mente nel caso in cui non le avessi ancora incontrate (meglio sapere ora, anche se non è possibile / pratico fare qualcosa al momento, piuttosto che essere morso di sorpresa nella produzione! ).
David Spillett,

Risposte:


9

@ Lu4 .. Ho votato per chiudere questa domanda come "Suggerimento di Iceberg" ma usando il suggerimento per la query sarai in grado di eseguirlo in meno di 1 secondo. Questa query può essere riformattata e può essere utilizzata CROSS APPLY, ma sarà un concerto di consulenza e non una risposta in un sito di domande e risposte.

La tua query così com'è funzionerà per 13+ minuti sul mio server con 4 CPU e 16 GB di RAM.

inserisci qui la descrizione dell'immagine

Ho cambiato la tua query da usare OPTION(MERGE JOIN)ed è durata meno di 1 secondo

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

inserisci qui la descrizione dell'immagine

Si noti che non è possibile utilizzare i suggerimenti di query in una vista, quindi è necessario capire un'alternativa di rendere la vista come SP o qualsiasi soluzione alternativa


1
la ringrazio molto per questo, tenendo conto del fatto che la domanda è lontana dagli standard di StackOverflow, per me il tuo sforzo è due volte prezioso. Farò i miei compiti su CROSS APPLY e cercherò di capire l'OPZIONE (MERGE JOIN). Non è ovvio ora quale sia il problema con quella query, tuttavia sono abbastanza sicuro che lo
capirò

@ Lu4 Il problema è che Query Optimizer non sta scegliendo (o generando) il miglior piano di esecuzione. Il suggerimento per la query in questo caso "incoraggia" l'ottimizzatore a utilizzare una strategia specifica per implementare il join. Vedere Suggerimenti di join (Transact-SQL) per ulteriori dettagli.
Kenny Evitt,

CROSS APPLYè fantastico, ma suggerirei di leggere i piani di esecuzione e come analizzarli prima di provare a utilizzare i suggerimenti per le query.
Kenny Evitt,
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.