[ Parte 1 | Parte 2 | Parte 3 | Parte 4]
Un problema che ho visto sorgere alcune volte di recente è lo scenario in cui hai creato una colonna IDENTITY come INT e ora ti stai avvicinando al limite superiore e devi ingrandirlo (BIGINT). Se il tuo tavolo è abbastanza grande da raggiungere il limite superiore di un numero intero (oltre 2 miliardi), questa non è un'operazione che puoi eseguire tra il pranzo e la pausa caffè di martedì. Questa serie esplorerà i meccanismi alla base di tale cambiamento e diversi modi per realizzarlo con impatti variabili sui tempi di attività. Nella prima parte, volevo dare un'occhiata da vicino all'impatto fisico della modifica di un INT in un BIGINT senza nessuna delle altre variabili.
Cosa succede effettivamente quando si allarga un INT?
INT e BIGINT sono tipi di dati a dimensione fissa, quindi una conversione dall'uno all'altro deve toccare la pagina, rendendo questa un'operazione di dimensione dei dati. Questo è contro-intuitivo, perché sembra che non sarebbe possibile per una modifica del tipo di dati da INT a BIGINT richiedere lo spazio aggiuntivo sulla pagina immediatamente (e per una colonna IDENTITY, mai). Pensando in modo logico, questo è spazio che non potrebbe essere necessario fino a più tardi, quando un valore INT esistente è stato modificato in un valore> 4 byte. Ma non è così che funziona oggi. Creiamo una semplice tabella e vediamo:
CREATE TABLE dbo.FirstTest ( RowID int IDENTITY(1,1), Filler char(2500) NOT NULL DEFAULT 'x' ); GO INSERT dbo.FirstTest WITH (TABLOCKX) (Filler) SELECT TOP (20) 'x' FROM sys.all_columns AS c; GO
Una semplice query può dirmi la pagina bassa e alta allocata a questo oggetto, nonché il conteggio totale delle pagine:
SELECT lo_page = MIN(allocated_page_page_id), hi_page = MAX(allocated_page_page_id), page_count = COUNT(*) FROM sys.dm_db_database_page_allocations ( DB_ID(), OBJECT_ID(N'dbo.FirstTest'), NULL, NULL, NULL );
Ora, se eseguo quella query prima e dopo aver modificato il tipo di dati da INT a BIGINT:
ALTER TABLE dbo.FirstTest ALTER COLUMN RowID bigint;
Vedo questi risultati:
-- before: lo_page hi_page page_count ------- ------- ---------- 243 303 17 -- after: lo_page hi_page page_count ------- ------- ---------- 243 319 33
È chiaro che sono state aggiunte 16 nuove pagine per fare spazio allo spazio aggiuntivo richiesto (anche se sappiamo che nessuno dei valori nella tabella richiede effettivamente 8 byte). Ma questo non è stato effettivamente realizzato nel modo in cui potresti pensare:anziché ampliare la colonna sulle pagine esistenti, le righe sono state spostate su nuove pagine, con i puntatori lasciati al loro posto. Guardando la pagina 243 prima e dopo (con la DBCC PAGE
non documentata ):
-- ******** Page 243, before: ******** Slot 0 Offset 0x60 Length 12 Record Type = PRIMARY_RECORD Record Attributes = NULL_BITMAP Record Size = 12 Memory Dump @0x000000E34B9FA060 0000000000000000: 10000900 01000000 78020000 .. .....x... Slot 0 Column 1 Offset 0x4 Length 4 Length (physical) 4 RowID = 1 Slot 0 Column 2 Offset 0x8 Length 1 Length (physical) 1 filler = x -- ******** Page 243, after: ******** Slot 0 Offset 0x60 Length 9 Record Type = FORWARDING_STUB Record Attributes = Record Size = 9 Memory Dump @0x000000E34B9FA060 0000000000000000: 04280100 00010078 01 .(.....x. Forwarding to = file 1 page 296 slot 376
Quindi se osserviamo la destinazione del puntatore, pagina 296, slot 376, vediamo:
Slot 376 Offset 0x8ca Length 34 Record Type = FORWARDED_RECORD Record Attributes = NULL_BITMAP VARIABLE_COLUMNS Record Size = 34 Memory Dump @0x000000E33BBFA8CA 0000000000000000: 32001100 01000000 78010000 00000000 00030000 2.......x........... 0000000000000014: 01002280 0004f300 00000100 0000 .."...ó....... Forwarded from = file 1 page 243 slot 0 Slot 376 Column 67108865 Offset 0x4 Length 0 Length (physical) 4 DROPPED = NULL Slot 376 Column 2 Offset 0x8 Length 1 Length (physical) 1 filler = x Slot 376 Column 1 Offset 0x9 Length 8 Length (physical) 8 RowID = 1
Questo è un cambiamento molto dirompente nella struttura del tavolo, ovviamente. (E un'interessante osservazione laterale:l'ordine fisico delle colonne, RowID e riempimento, è stato capovolto sulla pagina.) Lo spazio riservato passa da 136 KB a 264 KB e la frammentazione media aumenta leggermente dal 33,3% al 40%. Questo spazio non viene recuperato da una ricostruzione, online o meno, o da una riorganizzazione e, come vedremo tra poco, non è perché il tavolo è troppo piccolo per trarne vantaggio.
Nota:questo è vero anche nelle build più recenti di SQL Server 2016 – mentre sempre più operazioni come questa sono state migliorate per diventare operazioni di soli metadati nelle versioni moderne, questa non è stata ancora risolta, anche se chiaramente potrebbe essere, ancora una volta, soprattutto nel caso in cui la colonna sia una colonna IDENTITY, che non può essere aggiornata per definizione.
Eseguire l'operazione con la nuova sintassi ALTER COLUMN / ONLINE, di cui ho parlato l'anno scorso, produce alcune differenze:
-- drop / re-create here ALTER TABLE dbo.FirstTest ALTER COLUMN RowID bigint WITH (ONLINE = ON);
Ora il prima e il dopo diventano:
-- before: lo_page hi_page page_count ------- ------- ---------- 243 303 17 -- after: lo_page hi_page page_count ------- ------- ---------- 307 351 17
In questo caso, si trattava ancora di un'operazione di dimensione dei dati, ma le pagine esistenti sono state copiate e ricreate grazie all'opzione ONLINE. Potresti chiederti perché, quando abbiamo cambiato la dimensione della colonna come operazione ONLINE, la tabella è in grado di stipare più dati nello stesso numero di pagine? Ogni pagina è ora più densa (meno righe ma più dati per pagina), a scapito della dispersione:la frammentazione raddoppia dal 33,3% al 66,7%. Lo spazio utilizzato mostra più dati nello stesso spazio riservato (da 72 KB/136 KB a 96 KB/136 KB).
E su larga scala?
Rilasciamo la tabella, la ricreiamo e la popolamo con molti più dati:
CREATE TABLE dbo.FirstTest ( RowID INT IDENTITY(1,1), filler CHAR(1) NOT NULL DEFAULT 'x' ); GO INSERT dbo.FirstTest WITH (TABLOCKX) (filler) SELECT TOP (5000000) 'x' FROM sys.all_columns AS c1 CROSS JOIN sys.all_columns AS c2;
Dall'inizio abbiamo ora 8.657 pagine, un livello di frammentazione dello 0,09% e lo spazio utilizzato è 69.208 KB / 69.256 KB.
Se cambiamo il tipo di dati in bigint, si passa a 25.630 pagine, la frammentazione viene ridotta allo 0,06% e lo spazio utilizzato è 205.032 KB / 205.064 KB. Una ricostruzione online non cambia nulla, né una riorganizzazione. L'intero processo, inclusa la ricostruzione, richiede circa 97 secondi sulla mia macchina (il popolamento dei dati ha richiesto tutti i 2 secondi).
Se modifichiamo il tipo di dati in bigint utilizzando ONLINE, il bump è solo di 11.140 pagine, la frammentazione arriva all'85,5% e lo spazio utilizzato è 89.088 KB / 89160 KB. Le ricostruzioni e le riorganizzazioni online continuano a non cambiare nulla. Questa volta, l'intero processo richiede solo circa un minuto. Quindi la nuova sintassi porta sicuramente a operazioni più veloci e meno spazio su disco aggiuntivo, ma a un'elevata frammentazione. Lo prendo io.
Prossimo
Sono sicuro che stai guardando i miei test sopra e ti stai chiedendo alcune cose. Soprattutto, perché il tavolo è un mucchio? Volevo indagare su cosa succede effettivamente alla struttura della pagina e al conteggio delle pagine senza indici, chiavi o vincoli che confondono i dettagli. Potresti anche chiederti perché questa modifica è stata così semplice:in uno scenario in cui devi modificare una vera colonna IDENTITY, probabilmente è anche la chiave primaria del cluster e ha dipendenze di chiave esterna in altre tabelle. Questo introduce sicuramente alcuni intoppi nel processo. Daremo un'occhiata più da vicino a queste cose nel prossimo post della serie.
—
[ Parte 1 | Parte 2 | Parte 3 | Parte 4]