Probabilmente hai sentito molte volte prima che SQL Server fornisca una garanzia per le proprietà delle transazioni ACID. Questo articolo si concentra sulla parte D, che ovviamente sta per durabilità. Più specificamente, questo articolo è incentrato su un aspetto dell'architettura di registrazione di SQL Server che impone la durabilità delle transazioni:svuotamenti del buffer di registro. Parlo della funzione che svolge il buffer di registro, delle condizioni che obbligano SQL Server a svuotare il buffer di registro su disco, di cosa è possibile fare per ottimizzare le prestazioni delle transazioni, nonché di tecnologie correlate aggiunte di recente come la durabilità ritardata e la memoria di classe di archiviazione non volatile.
Svuotamento del buffer di registro
La parte D nelle proprietà della transazione ACID sta per durabilità. A livello logico significa che quando un'applicazione invia a SQL Server un'istruzione per eseguire il commit di una transazione (in modo esplicito o con una transazione con commit automatico), SQL Server normalmente restituisce il controllo al chiamante solo dopo che può garantire che la transazione sia durevole. In altre parole, una volta che il chiamante ha ripreso il controllo dopo aver eseguito il commit di una transazione, può essere sicuro che anche se un attimo dopo il server subisce un'interruzione di corrente, le modifiche apportate alla transazione sono state apportate al database. Finché il server si riavvia correttamente e i file di database non sono stati danneggiati, scoprirai che tutte le modifiche alle transazioni sono state applicate.
Il modo in cui SQL Server applica la durabilità delle transazioni, in parte, consiste nel garantire che tutte le modifiche della transazione vengano scritte nel registro delle transazioni del database su disco prima di restituire il controllo al chiamante. In caso di interruzione dell'alimentazione dopo il riconoscimento del commit di una transazione, sai che tutte queste modifiche sono state almeno scritte nel registro delle transazioni su disco. Questo è il caso anche se le relative pagine di dati sono state modificate solo nella cache di dati (il pool di buffer) ma non ancora scaricate nei file di dati su disco. Quando si riavvia SQL Server, durante la fase di ripristino del processo di ripristino, SQL Server utilizza le informazioni registrate nel registro per riprodurre le modifiche applicate dopo l'ultimo checkpoint e che non sono state apportate ai file di dati. C'è un po' di più nella storia a seconda del modello di ripristino che stai utilizzando e se le operazioni in blocco sono state applicate dopo l'ultimo checkpoint, ma ai fini della nostra discussione, è sufficiente concentrarsi sulla parte che prevede l'irrigidimento delle modifiche al registro delle transazioni.
La parte difficile dell'architettura di registrazione di SQL Server è che le scritture dei log sono sequenziali. Se SQL Server non avesse utilizzato una sorta di buffer di registro per alleviare le scritture dei registri su disco, i sistemi ad alta intensità di scrittura, in particolare quelli che coinvolgono molte piccole transazioni, incontrerebbero rapidamente terribili colli di bottiglia relativi alle prestazioni di scrittura dei registri.
Per ridurre l'impatto negativo sulle prestazioni delle frequenti scritture di log sequenziali su disco, SQL Server utilizza un buffer di log in memoria. Le scritture dei log vengono prima eseguite nel buffer di log e determinate condizioni causano lo svuotamento o l'indurimento del buffer di log su disco da parte di SQL ServerSQL Server. L'unità protetta (nota anche come blocco di registro) può variare da una dimensione minima di un settore (512 byte) a un massimo di 60 KB. Di seguito sono elencate le condizioni che attivano uno svuotamento del buffer di registro (ignora le parti che appaiono tra parentesi quadre per ora):
- SQL Server riceve una richiesta di commit di una transazione [completamente durevole] che modifica i dati [in un database diverso da tempdb]
- Il buffer di log si riempie, raggiungendo la sua capacità di 60 KB
- SQL Server deve rafforzare le pagine di dati sporche, ad esempio durante un processo di checkpoint, e i record di registro che rappresentano le modifiche a tali pagine non sono stati ancora protetti (registrazione in anticipo , o WAL in breve)
- Si richiede manualmente uno svuotamento del buffer di log eseguendo la procedura sys.sp_flush_log
- SQL Server scrive un nuovo valore di ripristino relativo alla cache della sequenza [in un database diverso da tempdb]
Le prime quattro condizioni dovrebbero essere abbastanza chiare, se per ora si ignorano le informazioni tra parentesi quadre. L'ultimo forse non è ancora chiaro, ma lo spiegherò in dettaglio più avanti nell'articolo.
Il tempo di attesa di SQL Server per il completamento di un'operazione di I/O che gestisce uno svuotamento del buffer di log viene riflesso dal tipo di attesa WRITELOG.
Allora, perché queste informazioni sono così interessanti e cosa ne facciamo? Comprendere le condizioni che attivano gli svuotamenti del buffer di registro può aiutarti a capire perché determinati carichi di lavoro presentano colli di bottiglia correlati. Inoltre, in alcuni casi è possibile intraprendere azioni per ridurre o eliminare tali colli di bottiglia. Tratterò una serie di esempi come una transazione di grandi dimensioni rispetto a molte transazioni di piccole dimensioni, transazioni completamente durevoli rispetto a quelle durevoli ritardate, database utente rispetto a tempdb e memorizzazione nella cache di oggetti in sequenza.
Una grande transazione contro molte piccole transazioni
Come accennato, una delle condizioni che attiva uno svuotamento del buffer di registro è quando si esegue il commit di una transazione per garantire la durata della transazione. Ciò significa che i carichi di lavoro che coinvolgono molte piccole transazioni, come i carichi di lavoro OLTP, possono potenzialmente subire colli di bottiglia legati alla scrittura dei log.
Anche se spesso non è così, se in una singola sessione vengono inviate molte piccole modifiche, un modo semplice ed efficace per ottimizzare il lavoro consiste nell'applicare le modifiche in un'unica grande transazione anziché in più piccole transazioni.
Considera il seguente esempio semplificato (scarica PerformanceV3 qui):
SET NOCOUNT ON; UTILIZZA PerformanceV3; ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY =Disabilitato; -- default DROP TABLE SE ESISTE dbo.T1; CREATE TABLE dbo.T1(col1 INT NOT NULL); DICHIARA @i COME INT =1; WHILE @i <=1000000BEGIN BEGIN TRAN INSERT INTO dbo.T1(col1) VALUES(@i); IMPEGNA TRAN; SET @i +=1;END;
Questo codice esegue 1.000.000 di piccole transazioni che modificano i dati in un database utente. Questo lavoro attiverà almeno 1.000.000 di svuotamenti del buffer di registro. Potresti ottenerne alcuni aggiuntivi a causa del riempimento del buffer di registro. Puoi utilizzare il seguente modello di test per contare il numero di svuotamenti del buffer di registro e misurare il tempo impiegato per completare il lavoro:
-- Modello di test -- ... La preparazione va qui ... -- Conta gli svuotamenti del registro e misura il tempoDECLARE @logflushes AS INT, @starttime AS DATETIME2, @duration AS INT; -- Stats beforeSET @logflushes =(SELEZIONARE cntr_value DA sys.dm_os_performance_counters WHERE counter_name ='Log Flushes/sec' AND instance_name =@db); SET @starttime =SYSDATETIME(); -- ... Il lavoro effettivo va qui ... -- Stats afterSET @duration =DATEDIFF(second, @starttime, SYSDATETIME());SET @logflushes =( SELECT cntr_value FROM sys.dm_os_performance_counters WHERE counter_name ='Log Flushes/sec ' E nome_istanza =@db ) - @logflushes; SELECT @duration AS durationinseconds, @logflushes AS logflushes;
Anche se il nome del contatore delle prestazioni è Log Flushes/sec, in realtà continua ad accumulare il numero di svuotamenti del buffer di registro finora. Quindi, il codice sottrae il conteggio prima del lavoro dal conteggio dopo il lavoro per calcolare il conteggio degli svuotamenti del registro generati dal lavoro. Questo codice misura anche il tempo in secondi impiegato dal lavoro per completare. Anche se non lo faccio qui, potresti, se lo desideri, calcolare in modo simile il numero di record di registro e la dimensione scritta nel registro dal lavoro interrogando gli stati prima e dopo il lavoro di fn_dblog funzione.
Per il nostro esempio sopra, la parte che devi inserire nella sezione di preparazione del modello di test è la seguente:
-- PreparazioneSET NOCOUNT ON;USE PerformanceV3; ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY =Disabilitato; DROP TABLE SE ESISTE dbo.T1; CREATE TABLE dbo.T1(col1 INT NOT NULL); DICHIARA @db AS sysname =N'PerformanceV3'; DICHIARA @logflushes AS INT, @starttime AS DATETIME2, @duration AS INT;
E di seguito è la parte che devi posizionare nella sezione di lavoro vera e propria:
-- Lavoro effettivoDECLARE @i AS INT =1; WHILE @i <=1000000BEGIN BEGIN TRAN INSERT INTO dbo.T1(col1) VALUES(@i); IMPEGNA TRAN; SET @i +=1;END;
Complessivamente, ottieni il seguente codice:
-- Esempio di test con molte piccole transazioni completamente durevoli nel database utente-- ... La preparazione va qui ... -- PreparationSET NOCOUNT ON;USE PerformanceV3; ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY =Disabilitato; DROP TABLE SE ESISTE dbo.T1; CREATE TABLE dbo.T1(col1 INT NOT NULL); DICHIARA @db AS sysname =N'PerformanceV3'; DICHIARA @logflushes AS INT, @starttime AS DATETIME2, @duration AS INT; -- Stats beforeSET @logflushes =(SELEZIONARE cntr_value DA sys.dm_os_performance_counters WHERE counter_name ='Log Flushes/sec' AND instance_name =@db); SET @starttime =SYSDATETIME(); -- ... Il lavoro effettivo va qui ... -- Il lavoro effettivoDECLARE @i AS INT =1; WHILE @i <=1000000BEGIN BEGIN TRAN INSERT INTO dbo.T1(col1) VALUES(@i); IMPEGNA TRAN; SET @i +=1;END; -- Statistiche afterSET @duration =DATEDIFF(secondo, @starttime, SYSDATETIME()); SET @logflushes =( SELEZIONA cntr_value DA sys.dm_os_performance_counters DOVE counter_name ='Log Flushes/sec' AND instance_name =@db ) - @logflushes; SELECT @duration AS durationinseconds, @logflushes AS logflushes;
Questo codice ha richiesto 193 secondi per essere completato sul mio sistema e ha attivato 1.000.036 di svuotamenti del buffer di registro. È molto lento, ma può essere spiegato a causa del gran numero di svuotamenti del registro.
Nei tipici carichi di lavoro OLTP, sessioni diverse inviano piccole modifiche in diverse piccole transazioni contemporaneamente, quindi non è che tu abbia davvero la possibilità di incapsulare molte piccole modifiche in un'unica grande transazione. Tuttavia, se la tua situazione è che tutte le piccole modifiche vengono inviate dalla stessa sessione, un modo semplice per ottimizzare il lavoro consiste nell'incapsularlo in un'unica transazione. Questo ti darà due vantaggi principali. Uno è che il tuo lavoro scriverà meno record di registro. Con 1.000.000 di piccole transazioni, ogni transazione scrive effettivamente tre record di registro:uno per l'inizio della transazione, uno per la modifica e uno per il commit della transazione. Quindi, stai esaminando circa 3.000.000 di record di registro delle transazioni rispetto a poco più di 1.000.000 quando eseguiti come una grande transazione. Ma ancora più importante, con una grande transazione, la maggior parte degli svuotamenti del registro vengono attivati solo quando il buffer del registro si riempie, più un altro svuotamento del registro alla fine della transazione quando viene eseguito il commit. La differenza di prestazioni può essere abbastanza significativa. Per testare il lavoro in un'unica grande transazione, utilizza il codice seguente nella parte di lavoro effettiva del modello di test:
-- Lavoro effettivoBEGIN TRAN; DICHIARA @i COME INT =1; WHILE @i <=1000000BEGIN INSERT INTO dbo.T1(col1) VALUES(@i); SET @i +=1; FINE; IMPEGNA TRAN;
Sul mio sistema, questo lavoro è stato completato in 7 secondi e ha attivato 1.758 svuotamenti del registro. Ecco un confronto tra le due opzioni:
Durata degli svuotamenti del log di #transazioni in secondi-------------- ------------ -------------- ------1000000 1000036 1931 1758 7
Ma ancora una volta, nei tipici carichi di lavoro OLTP, non hai davvero la possibilità di sostituire molte piccole transazioni inviate da sessioni diverse con una grande transazione inviata dalla stessa sessione.
Transazioni completamente durevoli rispetto a transazioni durevoli ritardate
A partire da SQL Server 2014 è possibile utilizzare una funzionalità denominata durabilità ritardata che consente di migliorare le prestazioni dei carichi di lavoro con molte piccole transazioni, anche se inviate da sessioni diverse, sacrificando la normale garanzia di durabilità completa. Quando si esegue il commit di una transazione durevole ritardata, SQL Server riconosce il commit non appena il record di registro del commit viene scritto nel buffer di registro, senza attivare uno svuotamento del buffer di registro. Il buffer di registro viene svuotato a causa di una qualsiasi delle altre condizioni sopra menzionate, ad esempio quando si riempie, ma non quando viene eseguita una transazione durevole ritardata.
Prima di utilizzare questa funzione, devi pensare molto attentamente se è appropriato per te. In termini di prestazioni, il suo impatto è significativo solo nei carichi di lavoro con molte piccole transazioni. Se all'inizio il tuo carico di lavoro comporta principalmente transazioni di grandi dimensioni, probabilmente non vedrai alcun vantaggio in termini di prestazioni. Ancora più importante, è necessario realizzare il potenziale di perdita di dati. Supponiamo che l'applicazione commetta una transazione durevole ritardata. Un record di commit viene scritto nel buffer di log e riconosciuto immediatamente (il controllo viene restituito al chiamante). Se in SQL Server si verifica un'interruzione di corrente prima che il buffer di registro venga svuotato, dopo il riavvio, il processo di ripristino annulla tutte le modifiche apportate dalla transazione, anche se l'applicazione ritiene che sia stato eseguito il commit.
Quindi, quando è possibile utilizzare questa funzione? Un caso ovvio è quando la perdita di dati non è un problema, come questo esempio di Melissa Connors di SentryOne. Un altro è quando dopo un riavvio hai i mezzi per identificare quali modifiche non sono state apportate al database e sei in grado di riprodurle. Se la tua situazione non rientra in una di queste due categorie, non utilizzare questa funzione nonostante la tentazione.
Per lavorare con transazioni durevoli ritardate, è necessario impostare un'opzione del database denominata DELAYED_DURABILITY. Questa opzione può essere impostata su uno dei tre valori:
- Disabilitato (impostazione predefinita):tutte le transazioni nel database sono completamente durevoli e quindi ogni commit attiva uno svuotamento del buffer di registro
- Forzato :tutte le transazioni nel database sono durevoli in ritardo e quindi i commit non attivano uno svuotamento del buffer di registro
- Consentito :se non diversamente indicato, le transazioni sono completamente durevoli e il loro commit attiva uno svuotamento del buffer di registro; tuttavia, se si utilizza l'opzione DELAYED_DURABILITY =ON in un'istruzione COMMIT TRAN o in un blocco atomico (di un processo compilato in modo nativo), quella particolare transazione è ritardata e duratura e quindi il commit non attiva uno svuotamento del buffer di log
Come test, usa il codice seguente nella sezione di preparazione del nostro modello di test (nota che l'opzione del database è impostata su Forced):
-- PreparazioneSET NOCOUNT ON;USE PerformanceV3; -- http://tsql.solidq.com/SampleDatabases/PerformanceV3.zip ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY =Forzato; DROP TABLE SE ESISTE dbo.T1; CREATE TABLE dbo.T1(col1 INT NOT NULL); DICHIARA @db AS sysname =N'PerformanceV3';
E usa il seguente codice nella sezione del lavoro effettivo (avviso, 1.000.000 di piccole transazioni):
-- Lavoro effettivoDECLARE @i AS INT =1; WHILE @i <=1000000BEGIN BEGIN TRAN INSERT INTO dbo.T1(col1) VALUES(@i); IMPEGNA TRAN; SET @i +=1;END;
In alternativa, puoi utilizzare la modalità Consentita a livello di database, quindi nel comando COMMIT TRAN, aggiungere WITH (DELAYED_DURABILITY =ON).
Sul mio sistema, il lavoro ha richiesto 22 secondi per essere completato e ha attivato 95.407 svuotamenti del registro. È più lungo che eseguire il lavoro come una grande transazione (7 secondi) poiché vengono generati più record di registro (ricorda, per transazione, uno per iniziare la transazione, uno per la modifica e uno per confermare la transazione); tuttavia, è molto più veloce dei 193 secondi necessari per completare il lavoro utilizzando 1.000.000 di transazioni completamente durevoli poiché il numero di svuotamenti dei registri è sceso da oltre 1.000.000 a meno di 100.000. Inoltre, con la durabilità ritardata, otterresti un aumento delle prestazioni anche se le transazioni vengono inviate da sessioni diverse in cui non è possibile utilizzare una transazione di grandi dimensioni.
Per dimostrare che non c'è alcun vantaggio nell'usare la durabilità ritardata quando si eseguono operazioni di grandi dimensioni, mantieni lo stesso codice nella parte di preparazione dell'ultimo test e usa il codice seguente nella parte di lavoro effettiva:
-- Lavoro effettivoBEGIN TRAN; DICHIARA @i COME INT =1; WHILE @i <=1000000BEGIN INSERT INTO dbo.T1(col1) VALUES(@i); SET @i +=1;END; IMPEGNA TRAN;
Ho ottenuto 8 secondi di autonomia (rispetto ai 7 di una grande transazione completamente durevole) e 1.759 svuotamenti del registro (rispetto a 1.758). I numeri sono essenzialmente gli stessi, ma con la transazione durevole ritardata si corre il rischio di perdita di dati.
Ecco un riepilogo dei numeri delle prestazioni per tutti e quattro i test:
durabilità #transazioni log durata svuotamenti in secondi------------------- -------------- ------ ------ -----------------------------------completo 1000000 1000036 193completo 1 1758 7ritardato 1000000 95407 22ritardato 1 1759 8
Memoria della classe di archiviazione
La funzionalità di durabilità ritardata può migliorare significativamente le prestazioni dei carichi di lavoro in stile OLTP che coinvolgono un numero elevato di piccole transazioni di aggiornamento che richiedono alta frequenza e bassa latenza. Il problema è che stai rischiando la perdita di dati. Cosa succede se non è possibile consentire alcuna perdita di dati, ma si desidera comunque un aumento delle prestazioni simile alla durata ritardata, in cui il buffer di registro non viene svuotato per ogni commit, piuttosto quando si riempie? A tutti noi piace mangiare la torta e mangiarla anche noi, giusto?
È possibile ottenere questo risultato in SQL Server 2016 SP1 o versioni successive usando la memoria della classe di archiviazione, nota anche come archiviazione non volatile NVDIMM-N. Questo hardware è essenzialmente un modulo di memoria che offre prestazioni di livello memoria, ma le informazioni sono persistenti e quindi non perse quando l'alimentazione viene interrotta. L'aggiunta in SQL Server 2016 SP1 consente di configurare il buffer di registro come persistente su tale hardware. A tale scopo, configurare SCM come volume in Windows e formattarlo come volume Direct Access Mode (DAX). Quindi aggiungere un file di registro al database utilizzando il normale comando ALTER DATABASE
Per ulteriori dettagli su questa funzionalità, inclusi i numeri delle prestazioni, vedere Accelerazione della latenza di Transaction Commit tramite Storage Class Memory in Windows Server 2016/SQL Server 2016 SP1 di Kevin Farlee.
Curiosamente, SQL Server 2019 migliora il supporto per la memoria della classe di archiviazione oltre al semplice scenario della cache di registro persistente. Supporta il posizionamento di file di dati, file di registro e file di checkpoint OLTP in memoria su tale hardware. Tutto quello che devi fare è esporlo come volume a livello di sistema operativo e formattarlo come unità DAX. SQL Server 2019 riconosce automaticamente questa tecnologia e funziona in modo illuminato modalità, accedendo direttamente al dispositivo, bypassando lo stack di archiviazione del sistema operativo. Benvenuto nel futuro!
Database utenti rispetto a tempdb
Il database tempdb viene ovviamente creato da zero come una nuova copia del database del modello ogni volta che si riavvia SQL Server. Pertanto, non è mai necessario recuperare i dati che scrivi in tempdb, sia che tu li scriva su tabelle temporanee, variabili di tabella o tabelle utente. È tutto finito dopo il riavvio. Sapendo questo, SQL Server può rilassare molti dei requisiti relativi alla registrazione. Ad esempio, indipendentemente dal fatto che si abiliti o meno l'opzione di durabilità ritardata, gli eventi di commit non attivano uno svuotamento del buffer di registro. Inoltre, la quantità di informazioni che devono essere registrate viene ridotta poiché SQL Server richiede solo informazioni sufficienti per supportare il rollback delle transazioni o l'annullamento del lavoro, se necessario, ma non il rollforward delle transazioni o il ripristino del lavoro. Di conseguenza, i record del registro delle transazioni che rappresentano le modifiche a un oggetto in tempdb tendono a essere più piccoli rispetto a quando la stessa modifica viene applicata a un oggetto in un database utente.
Per dimostrare ciò, eseguirai gli stessi test eseguiti in precedenza in PerformanceV3, solo questa volta in tempdb. Inizieremo con il test di molte piccole transazioni quando l'opzione del database DELAYED_DURABILITY è impostata su Disabilitato (impostazione predefinita). Utilizza il codice seguente nella sezione di preparazione del modello di test:
-- PreparazioneSET NOCOUNT ON;USE tempdb; ALTER DATABASE tempdb SET DELAYED_DURABILITY =Disabilitato; DROP TABLE SE ESISTE dbo.T1; CREATE TABLE dbo.T1(col1 INT NOT NULL); DICHIARA @db AS sysname =N'tempdb';
Utilizza il codice seguente nella sezione del lavoro effettivo:
-- Lavoro effettivoDECLARE @i AS INT =1; WHILE @i <=1000000BEGIN BEGIN TRAN INSERT INTO dbo.T1(col1) VALUES(@i); IMPEGNA TRAN; SET @i +=1;END;
Questo lavoro ha generato 5.095 svuotamenti del registro e il completamento ha richiesto 19 secondi. Questo viene confrontato con oltre un milione di svuotamenti di log e 193 secondi in un database utente con la massima durata. È anche meglio che con la durabilità ritardata in un database utente (95.407 scaricamenti di log e 22 secondi) a causa delle dimensioni ridotte dei record di log.
Per testare una transazione di grandi dimensioni, lascia invariata la sezione di preparazione e utilizza il codice seguente nella sezione di lavoro effettiva:
-- Lavoro effettivoBEGIN TRAN; DICHIARA @i COME INT =1; WHILE @i <=1000000BEGIN INSERT INTO dbo.T1(col1) VALUES(@i); SET @i +=1;END; IMPEGNA TRAN;
Ho ricevuto 1.228 svuotamenti del registro e 9 secondi di autonomia. Questo viene confrontato con 1.758 svuotamenti del registro e 7 secondi di runtime nel database dell'utente. Il tempo di esecuzione è simile, anche un po' più veloce nel database utente, ma potrebbero esserci piccole variazioni tra i test. Le dimensioni dei record di registro in tempdb vengono ridotte e quindi si ottengono meno svuotamenti del registro rispetto al database utente.
Puoi anche provare a eseguire i test con l'opzione DELAYED_DURABILITY impostata su Forced, ma ciò non avrà alcun impatto in tempdb poiché, come accennato, comunque gli eventi di commit non attivano uno svuotamento del registro in tempdb.
Di seguito sono riportate le misure delle prestazioni per tutti i test, sia nel database utente che in tempdb:
durata del database #transazioni log scarica la durata in secondi-------------- ------------------- ----- --------- ------------ --------------------PerformanceV3 completo 1000000 1000036 193PerformanceV3 completo 1 1758 7PerformanceV3 ritardato 1000000 95407 22PerformanceV3 ritardato 1 1759 8tempdb pieno 1000000 5095 19tempdb pieno 1 1228 9tempdb ritardato 1000000 5091 18tempdb ritardato 1 1226 9
Sequenza di memorizzazione nella cache degli oggetti
Forse un caso sorprendente che attiva lo svuotamento del buffer di registro è correlato all'opzione della cache degli oggetti della sequenza. Si consideri come esempio la seguente definizione di sequenza:
CREA SEQUENZA dbo.Seq1 COME BIGINT MINVALUE 1 CACHE 50; -- la dimensione della cache predefinita è 50;
Ogni volta che hai bisogno di un nuovo valore di sequenza, usi la funzione NEXT VALUE FOR, in questo modo:
SELEZIONA VALORE SUCCESSIVO PER dbo.Seq1;
La proprietà CACHE è una caratteristica delle prestazioni. Senza di essa, ogni volta che veniva richiesto un nuovo valore di sequenza, SQL Server avrebbe dovuto scrivere il valore corrente su disco per scopi di ripristino. In effetti, questo è il comportamento che si ottiene quando si utilizza la modalità NO CACHE. Al contrario, quando l'opzione è impostata su un valore maggiore di zero, SQL Server scrive un valore di ripristino sul disco solo una volta ogni numero di richieste della dimensione della cache. SQL ServerSQL Server mantiene due membri in memoria, delle dimensioni del tipo di sequenza, uno che contiene il valore corrente e uno che contiene il numero di valori rimasti prima che sia necessaria la successiva scrittura su disco del valore di ripristino. In caso di interruzione dell'alimentazione, al riavvio, SQL Server imposta il valore della sequenza corrente sul valore di ripristino.
Questo è probabilmente molto più facile da spiegare con un esempio. Considera la definizione di sequenza sopra con l'opzione CACHE impostata su 50 (impostazione predefinita). Si richiede per la prima volta un nuovo valore di sequenza eseguendo l'istruzione SELECT precedente. SQL Server imposta i suddetti membri sui seguenti valori:
Valore di ripristino su disco:50, Valore corrente in memoria:1, Valori in memoria rimasti:49, Ottieni:1
Altre 49 richieste non toccheranno il disco, ma aggiorneranno solo i membri della memoria. Dopo 50 richieste in totale, i membri vengono impostati sui seguenti valori:
Valore di ripristino su disco:50, Valore corrente in memoria:50, Valori in memoria rimasti:0, Ottieni:50
Effettua un'altra richiesta per un nuovo valore di sequenza e questo attiva una scrittura su disco del valore di ripristino 100. I membri vengono quindi impostati sui seguenti valori:
Valore di ripristino su disco:100, Valore corrente in memoria:51, Valori in memoria rimasti:49, Ottieni:51
Se a questo punto il sistema subisce un'interruzione di corrente, dopo il riavvio, il valore della sequenza corrente viene impostato su 100 (il valore recuperato dal disco). La successiva richiesta di un valore di sequenza produce 101 (scrivendo il valore di ripristino 150 su disco). Hai perso tutti i valori nell'intervallo da 52 a 100. Il massimo che puoi perdere a causa di un'interruzione non pulita del processo di SQL Server, come in caso di interruzione di corrente, è il numero massimo di valori pari alla dimensione della cache. Il compromesso è chiaro; maggiore è la dimensione della cache, minore è il numero di scritture su disco del valore di ripristino e quindi migliori sono le prestazioni. Allo stesso tempo, maggiore è il divario che può essere generato tra due valori di sequenza in caso di interruzione di corrente.
Tutto questo è piuttosto semplice e forse conosci molto bene come funziona. Ciò che potrebbe sorprendere è che ogni volta che SQL Server scrive un nuovo valore di ripristino su disco (ogni 50 richieste nel nostro esempio), indurisce anche il buffer di registro. Questo non è il caso della proprietà della colonna di identità, anche se SQL Server utilizza internamente la stessa funzionalità di memorizzazione nella cache per l'identità come per l'oggetto sequenza, semplicemente non ti consente di controllarne le dimensioni. È attivo per impostazione predefinita con dimensione 10000 per BIGINT e NUMERIC, 1000 per INT, 100 per SMALLINT e 10 PER TINYINT. Se lo desideri, puoi disattivarlo con il flag di traccia 272 o l'opzione di configurazione con ambito IDENTITY_CACHE (2017+). Il motivo per cui SQL Server non deve svuotare il buffer di registro durante la scrittura del valore di ripristino relativo alla cache di identità su disco è che un nuovo valore di identità può essere creato solo quando si inserisce una riga in una tabella. In caso di interruzione dell'alimentazione, una riga inserita in una tabella da una transazione che non ha eseguito il commit verrà estratta dalla tabella come parte del processo di ripristino del database al riavvio del sistema. Quindi, anche se dopo il riavvio SQL Server genera lo stesso valore di identità come quello creato nella transazione che non ha eseguito il commit, non ci sono possibilità di duplicati poiché la riga è stata estratta dalla tabella. Se la transazione fosse stata confermata, ciò avrebbe attivato uno svuotamento del registro, che avrebbe anche mantenuto la scrittura di un valore di ripristino relativo alla cache. Pertanto, Microsoft non si è sentita obbligata a svuotare il buffer di registro ogni volta che si verifica una scrittura su disco relativa alla cache di identità del valore di ripristino.
Con l'oggetto sequenza la situazione è diversa. Un'applicazione può richiedere un nuovo valore di sequenza e non archiviarlo nel database. In caso di interruzione dell'alimentazione dopo la creazione di un nuovo valore di sequenza in una transazione che non ha eseguito il commit, dopo il riavvio, non è possibile per SQL Server indicare all'applicazione di non fare affidamento su tale valore. Pertanto, per evitare di creare un nuovo valore di sequenza dopo il riavvio uguale a un valore di sequenza generato in precedenza, SQL ServerSQL Server forza uno svuotamento del registro ogni volta che un nuovo valore di ripristino correlato alla cache della sequenza viene scritto sul disco. Un'eccezione a questa regola è quando l'oggetto sequenza viene creato in tempdb, ovviamente non c'è bisogno di tali svuotamenti del registro poiché comunque dopo un riavvio del sistema tempdb viene creato di nuovo.
Un impatto negativo sulle prestazioni dei frequenti svuotamenti del registro è particolarmente evidente quando si utilizza una dimensione della cache di sequenza molto piccola e in una transazione si generano molti valori di sequenza, ad esempio quando si inseriscono molte righe in una tabella. Senza la sequenza, una tale transazione indurrebbe principalmente il buffer di registro quando si riempie, più un'altra volta quando la transazione viene eseguita. Ma con la sequenza, si ottiene uno svuotamento del registro ogni volta che si verifica una scrittura su disco di un valore di ripristino. Ecco perché vuoi evitare di utilizzare una piccola dimensione della cache, per non parlare della modalità NO CACHE.
Per dimostrarlo, usa il codice seguente nella sezione di preparazione del nostro modello di test:
-- PreparazioneSET NOCOUNT ON;USE PerformanceV3; -- prova PerformanceV3, tempdb ALTER DATABASE PerformanceV3 -- prova PerformanceV3, tempdb SET DELAYED_DURABILITY =Disabilitato; -- try Disabled, Forced DROP TABLE SE ESISTE dbo.T1; SEQUENZA DI RILASCIO SE ESISTE dbo.Seq1; CREATE SEQUENCE dbo.Seq1 AS BIGINT MINVALUE 1 CACHE 50; -- prova NESSUNA CACHE, CACHE 50, CACHE 10000 DICHIARA @db AS sysname =N'PerformanceV3'; -- prova PerformanceV3, tempdb
E il codice seguente nella sezione del lavoro effettivo:
-- Actual workSELECT -- n -- per testare senza seq NEXT VALUE FOR dbo.Seq1 AS n -- per testare sequenceINTO dbo.T1FROM PerformanceV3.dbo.GetNums(1, 1000000) AS N;
Questo codice utilizza una transazione per scrivere 1.000.000 di righe in una tabella utilizzando l'istruzione SELECT INTO, generando tanti valori di sequenza quante sono le righe inserite.
Come indicato nei commenti, esegui il test con NO CACHE, CACHE 50 e CACHE 10000, sia in PerformanceV3 che in tempdb, e prova sia le transazioni completamente durevoli che quelle durevoli ritardate.
Ecco i numeri delle prestazioni che ho ottenuto sul mio sistema:
il registro della cache della durabilità del database cancella la durata in secondi -------------- ------------------- ------ --- ------------ --------------------PerformanceV3 completo NO CACHE 1000047 171PerformanceV3 completo 50 20008 4PerformanceV3 completo 10000 339 <1tempdb pieno NO CACHE 96 4tempdb pieno 50 74 1tempdb pieno 10000 8 <1PerformanceV3 ritardato NO CACHE 1000045 166PerformanceV3 ritardato 50 20011 4PerformanceV3 ritardato 10000 334 <1tempdb ritardato NO CACHE 91 4tempdb ritardato 50 74 1tempdb0 ritardato 1000>Ci sono alcune cose interessanti da notare.
Con NO CACHE ottieni un log flush per ogni singolo valore di sequenza generato. Pertanto, si consiglia vivamente di evitarlo.
Con una piccola dimensione della cache della sequenza, si ottengono comunque molti svuotamenti del registro. Forse la situazione non è così grave come con NO CACHE, ma osserva che il carico di lavoro ha impiegato 4 secondi per essere completato con la dimensione della cache predefinita di 50 rispetto a meno di un secondo con la dimensione 10.000. Personalmente utilizzo 10.000 come valore preferito.
In tempdb you don’t get log flushes when a sequence cache-related recovery value is written to disk, but the recovery value is still written to disk every cache-sized number of requests. That’s perhaps surprising since such a value would never need to be recovered. Therefore, even when using a sequence object in tempdb, I’d still recommend using a large cache size.
Also notice that delayed durability doesn’t prevent the need for log flushes every time the sequence cache-related recovery value is written to disk.
Conclusione
This article focused on log buffer flushes. Understanding this aspect of SQL Server’s logging architecture is important especially in order to be able to optimize OLTP-style workloads that require high frequency and low latency. Workloads using In-Memory OLTP included, of course. You have more options with newer features like delayed durability and persisted log buffer with storage class memory. Make sure you’re very careful with the former, though, since it does incur potential for data loss unlike the latter.
Be careful not to use the sequence object with a small cache size, not to speak of the NO CACHE mode. I find the default size 50 too small and prefer to use 10,000. I’ve heard people expressing concerns that with a cache size 10000, after multiple power failures they might lose all the values in the type. However, even with a four-byte INT type, using only the positive range, 10,000 fits 214,748 times. If your system experience that many power failures, you have a completely different problem to worry about. Therefore, I feel very comfortable with a cache size of 10,000.