Elimina le prestazioni per i dati LOB in SQL Server


16

Questa domanda è collegata a questo thread del forum .

Esecuzione di SQL Server 2008 Developer Edition sulla mia workstation e un cluster di macchine virtuali a due nodi Enterprise Edition in cui mi riferisco a "cluster alfa".

Il tempo necessario per eliminare le righe con una colonna varbinary (max) è direttamente correlato alla lunghezza dei dati in quella colonna. All'inizio può sembrare intuitivo, ma dopo un'indagine, si scontra con la mia comprensione di come SQL Server elimini effettivamente le righe in generale e gestisca questo tipo di dati.

Il problema deriva da un problema di timeout di eliminazione (> 30 secondi) che stiamo riscontrando nella nostra applicazione Web .NET, ma l'ho semplificato per il bene di questa discussione.

Quando un record viene eliminato, SQL Server lo contrassegna come un fantasma da ripulire da un'attività di pulizia fantasma in un secondo momento dopo il commit della transazione (vedere il blog di Paul Randal ). In un test che elimina tre righe con 16 KB, 4 MB e 50 MB di dati in una colonna varbinary (max), rispettivamente, vedo che ciò accade sulla pagina con la parte in-line dei dati, così come nella transazione log.

Ciò che mi sembra strano è che i blocchi X sono posizionati su tutte le pagine di dati LOB durante l'eliminazione e che le pagine sono deallocate nel PFS. Vedo questo nel registro delle transazioni, nonché con sp_locke i risultati del dm_db_index_operational_statsDMV ( page_lock_count).

Questo crea un collo di bottiglia di I / O sulla mia workstation e sul nostro cluster alfa se quelle pagine non sono già nella cache del buffer. In effetti, lo page_io_latch_wait_in_msstesso DMV è praticamente l'intera durata dell'eliminazione e page_io_latch_wait_countcorrisponde al numero di pagine bloccate. Per il file da 50 MB sulla mia workstation, questo si traduce in oltre 3 secondi quando si inizia con una cache buffer vuota ( checkpoint/ dbcc dropcleanbuffers), e non ho dubbi che sarebbe più lungo per la frammentazione pesante e sotto carico.

Ho cercato di assicurarmi che non si occupasse solo di allocare spazio nella cache. Ho letto 2 GB di dati da altre righe prima di eseguire l'eliminazione anziché il checkpointmetodo, che è più di quanto assegnato al processo di SQL Server. Non sono sicuro che si tratti di un test valido o meno, dal momento che non so come SQL Server mescoli i dati. Ho pensato che avrebbe sempre spinto il vecchio a favore del nuovo.

Inoltre, non modifica nemmeno le pagine. Questo posso vedere con dm_os_buffer_descriptors. Le pagine sono pulite dopo l'eliminazione, mentre il numero di pagine modificate è inferiore a 20 per tutte e tre le eliminazioni di piccole, medie e grandi dimensioni. Ho anche confrontato l'output di DBCC PAGEper un campionamento delle pagine cercate e non ci sono state modifiche (solo il ALLOCATEDbit è stato rimosso da PFS). Li trasferisce semplicemente.

Per dimostrare ulteriormente che le ricerche di pagine / deallocazioni stanno causando il problema, ho provato lo stesso test usando una colonna filestream invece di vanilla varbinary (max). Le eliminazioni erano a tempo costante, indipendentemente dalle dimensioni del LOB.

Quindi, prima le mie domande accademiche:

  1. Perché SQL Server deve cercare tutte le pagine di dati LOB per bloccarle X? È solo un dettaglio di come i blocchi sono rappresentati in memoria (memorizzati in qualche modo con la pagina)? Questo fa sì che l'impatto I / O dipenda fortemente dalle dimensioni dei dati se non completamente memorizzato nella cache.
  2. Perché la X si blocca, solo per dislocarli? Non è sufficiente bloccare solo la foglia di indice con la parte in riga, poiché la deallocazione non deve modificare le pagine stesse? Esiste un altro modo per ottenere i dati LOB da cui protegge il blocco?
  3. Perché deallocare le pagine in anticipo, dato che esiste già un'attività in background dedicata a questo tipo di lavoro?

E forse più importante, la mia domanda pratica:

  • C'è un modo per far funzionare le eliminazioni in modo diverso? Il mio obiettivo è l'eliminazione costante del tempo, indipendentemente dalle dimensioni, simile al filestream, in cui ogni ripulitura avviene in background dopo il fatto. È una cosa di configurazione? Sto conservando le cose in modo strano?

Ecco come riprodurre il test descritto (eseguito tramite la finestra della query SSMS):

CREATE TABLE [T] (
    [ID] [uniqueidentifier] NOT NULL PRIMARY KEY,
    [Data] [varbinary](max) NULL
)

DECLARE @SmallID uniqueidentifier
DECLARE @MediumID uniqueidentifier
DECLARE @LargeID uniqueidentifier

SELECT @SmallID = NEWID(), @MediumID = NEWID(), @LargeID = NEWID()
-- May want to keep these IDs somewhere so you can use them in the deletes without var declaration

INSERT INTO [T] VALUES (@SmallID, CAST(REPLICATE(CAST('a' AS varchar(max)), 16 * 1024) AS varbinary(max)))
INSERT INTO [T] VALUES (@MediumID, CAST(REPLICATE(CAST('a' AS varchar(max)), 4 * 1024 * 1024) AS varbinary(max)))
INSERT INTO [T] VALUES (@LargeID, CAST(REPLICATE(CAST('a' AS varchar(max)), 50 * 1024 * 1024) AS varbinary(max)))

-- Do this before test
CHECKPOINT
DBCC DROPCLEANBUFFERS
BEGIN TRAN

-- Do one of these deletes to measure results or profile
DELETE FROM [T] WHERE ID = @SmallID
DELETE FROM [T] WHERE ID = @MediumID
DELETE FROM [T] WHERE ID = @LargeID

-- Do this after test
ROLLBACK

Ecco alcuni risultati della profilazione delle eliminazioni sulla mia workstation:

| Tipo di colonna | Elimina dimensione | Durata (ms) | Legge | Scrive | CPU |
-------------------------------------------------- ------------------
| VarBinary | 16 KB | 40 | 13 | 2 | 0 |
| VarBinary | 4 MB | 952 | 2318 | 2 | 0 |
| VarBinary | 50 MB | 2976 | 28594 | 1 | 62 |
-------------------------------------------------- ------------------
| FileStream | 16 KB | 1 | 12 | 1 | 0 |
| FileStream | 4 MB | 0 | 9 | 0 | 0 |
| FileStream | 50 MB | 1 | 9 | 0 | 0 |

Non possiamo necessariamente usare filestream invece perché:

  1. La nostra distribuzione delle dimensioni dei dati non lo garantisce.
  2. In pratica, aggiungiamo dati in molti blocchi e filestream non supporta gli aggiornamenti parziali. Dovremmo progettare attorno a questo.

Aggiornamento 1

Ho testato una teoria secondo cui i dati vengono scritti nel registro delle transazioni come parte dell'eliminazione e questo non sembra essere il caso. Sto testando per questo in modo errato? Vedi sotto.

SELECT MAX([Current LSN]) FROM fn_dblog(NULL, NULL)
--0000002f:000001d9:0001

BEGIN TRAN
DELETE FROM [T] WHERE ID = @ID

SELECT
    SUM(
        DATALENGTH([RowLog Contents 0]) +
        DATALENGTH([RowLog Contents 1]) +
        DATALENGTH([RowLog Contents 3]) +
        DATALENGTH([RowLog Contents 4])
    ) [RowLog Contents Total],
    SUM(
        DATALENGTH([Log Record])
    ) [Log Record Total]
FROM fn_dblog(NULL, NULL)
WHERE [Current LSN] > '0000002f:000001d9:0001'

Per un file di dimensioni superiori a 5 MB, questo è tornato 1651 | 171860.

Inoltre, mi aspetto che le pagine stesse siano sporche se i dati fossero scritti nel registro. Sembra che siano registrate solo le deallocazioni, che corrispondono a ciò che è sporco dopo l'eliminazione.

Aggiornamento 2

Ho ricevuto una risposta da Paul Randal. Ha affermato il fatto che deve leggere tutte le pagine per attraversare l'albero e trovare le pagine da deallocare, e ha dichiarato che non c'è altro modo di cercare quali pagine. Questa è una mezza risposta a 1 e 2 (anche se non spiega la necessità di blocchi sui dati fuori fila, ma si tratta di piccole patate).

La domanda 3 è ancora aperta: perché deallocare le pagine in primo piano se esiste già un'attività in background per eseguire la pulizia per le eliminazioni?

E, naturalmente, la domanda fondamentale: esiste un modo per mitigare direttamente (cioè non aggirare) questo comportamento di eliminazione dipendente dalla dimensione? Penserei che questo sarebbe un problema più comune, a meno che non siamo davvero i soli a archiviare ed eliminare righe da 50 MB in SQL Server? Tutti gli altri là fuori lavorano intorno a questo con una qualche forma di un lavoro di raccolta dei rifiuti?


Vorrei che ci fosse una soluzione migliore, ma non ne ho trovata una. Ho una situazione di registrazione di grandi volumi di righe di dimensioni variabili, fino a 1 MB +, e ho un processo di "eliminazione" per eliminare i vecchi record. Poiché le eliminazioni erano così lente, ho dovuto dividerlo in due passaggi: prima rimuovi i riferimenti tra le tabelle (che è molto veloce), quindi elimina le righe orfane. Il processo di eliminazione è stato in media ~ 2,2 secondi / MB per eliminare i dati. Quindi, ovviamente, ho dovuto ridurre la contesa, quindi ho una procedura memorizzata con "DELETE TOP (250)" all'interno di un ciclo fino a quando non vengono più cancellate righe.
Abaco

Risposte:


5

Non posso dire perché esattamente sarebbe molto più inefficiente eliminare un VARBINARY (MAX) rispetto al flusso di file, ma un'idea che potresti considerare se stai solo cercando di evitare i timeout dalla tua applicazione web quando elimini questi LOBS. È possibile memorizzare i valori VARBINARY (MAX) in una tabella separata (chiamiamola tblLOB) a cui fa riferimento la tabella originale (chiamiamola tblParent).

Da qui quando si elimina un record, è possibile eliminarlo dal record principale e quindi disporre di un processo di garbage collection occasionale per accedere e ripulire i record nella tabella LOB. Durante questo processo di garbage collection potrebbe essere presente attività aggiuntiva sul disco rigido, ma almeno sarà separata dal Web front-end e potrà essere eseguita durante le ore non di punta.


Grazie. Questa è esattamente una delle nostre opzioni alla lavagna. La tabella è un file system e al momento stiamo separando i dati binari in un database completamente separato dalla meta gerarchia. Potremmo fare come hai detto ed eliminare la riga della gerarchia e fare in modo che un processo GC ripulisca le righe LOB orfane. O avere un timestamp di eliminazione con i dati per raggiungere lo stesso obiettivo. Questo è il percorso che potremmo seguire se non ci fosse una risposta soddisfacente al problema.
Jeremy Rosenberg,

1
Sarei cauto di avere solo un timestamp per indicare che è stato eliminato. Funzionerà ma alla fine avrai molto spazio usato occupato nelle righe attive. Avrai bisogno di avere un qualche tipo di processo gc ad un certo punto, a seconda di quanto viene eliminato, e sarà meno impattante eliminare meno su base regolare piuttosto che lotti su base occasionale.
Ian Chamberland,
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.