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_lock
e i risultati del dm_db_index_operational_stats
DMV ( 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_ms
stesso DMV è praticamente l'intera durata dell'eliminazione e page_io_latch_wait_count
corrisponde 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 checkpoint
metodo, 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 PAGE
per un campionamento delle pagine cercate e non ci sono state modifiche (solo il ALLOCATED
bit è 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:
- 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.
- 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?
- 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é:
- La nostra distribuzione delle dimensioni dei dati non lo garantisce.
- 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?