Perché ALTER COLUMN su NOT NULL provoca una massiccia crescita dei file di registro?


56

Ho una tabella con 64m di righe che prendono 4.3 GB su disco per i suoi dati.

Ogni riga contiene circa 30 byte di colonne intere, più una NVARCHAR(255)colonna variabile per il testo.

Ho aggiunto una colonna NULLABLE con tipo di dati Datetimeoffset(0).

Ho quindi AGGIORNATO questa colonna per ogni riga e mi sono assicurato che tutti i nuovi inserti inserissero un valore in questa colonna.

Una volta che non c'erano voci NULL, ho eseguito questo comando per rendere obbligatorio il mio nuovo campo:

ALTER TABLE tblCheckResult 
ALTER COLUMN [dtoDateTime] [datetimeoffset](0) NOT NULL

Il risultato è stato una crescita ENORME delle dimensioni del registro delle transazioni, da 6 GB a oltre 36 GB fino a quando non ha esaurito lo spazio!

Qualcuno ha idea di cosa diavolo sta facendo SQL Server 2008 R2 per questo semplice comando che si traduca in una crescita così grande?


7
SQL Server 2012 Enterprise aggiunge la possibilità di aggiungere una NOT NULLcolonna con un valore predefinito come operazione di metadati. Vedere anche "Aggiunta di colonne NOT NULL come operazione online" nella documentazione .
Paul White

Risposte:


48

Quando si modifica una colonna in NOT NULL, SQL Server deve toccare ogni singola pagina, anche se non sono presenti valori NULL. A seconda del fattore di riempimento, ciò potrebbe effettivamente comportare molte divisioni di pagina. Ogni pagina toccata, ovviamente, deve essere registrata e sospetto che, a causa delle divisioni, potrebbe essere necessario registrare due modifiche per molte pagine. Dato che è tutto fatto in un unico passaggio, tuttavia, il registro deve tenere conto di tutte le modifiche in modo che, se si preme Annulla, sappia esattamente cosa annullare.


Un esempio. Tavolo semplice:

DROP TABLE dbo.floob;
GO

CREATE TABLE dbo.floob
(
  id INT IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED, 
  bar INT NULL
);

INSERT dbo.floob(bar) SELECT NULL UNION ALL SELECT 4 UNION ALL SELECT NULL;

ALTER TABLE dbo.floob ADD CONSTRAINT df DEFAULT(0) FOR bar

Ora diamo un'occhiata ai dettagli della pagina. Per prima cosa dobbiamo scoprire con quale pagina e DB_ID abbiamo a che fare. Nel mio caso ho creato un database chiamato fooe il DB_ID è risultato essere 5.

DBCC TRACEON(3604, -1);
DBCC IND('foo', 'dbo.floob', 1);
SELECT DB_ID();

L'output ha indicato che ero interessato a pagina 159 (l'unica riga DBCC INDdell'output con PageType = 1).

Ora diamo un'occhiata ad alcuni dettagli della pagina selezionata mentre esaminiamo lo scenario del PO.

DBCC PAGE(5, 1, 159, 3);

inserisci qui la descrizione dell'immagine

UPDATE dbo.floob SET bar = 0 WHERE bar IS NULL;    
DBCC PAGE(5, 1, 159, 3);

inserisci qui la descrizione dell'immagine

ALTER TABLE dbo.floob ALTER COLUMN bar INT NOT NULL;
DBCC PAGE(5, 1, 159, 3);

inserisci qui la descrizione dell'immagine

Ora, non ho tutte le risposte a questo, dato che non sono un tipo da interno profondo. Ma è chiaro che - mentre sia l'operazione di aggiornamento sia l'aggiunta del vincolo NOT NULL scrivono innegabilmente alla pagina - quest'ultima lo fa in un modo completamente diverso. Sembra effettivamente cambiare la struttura del record, piuttosto che giocherellare con i bit, scambiando la colonna nullable con una colonna non nullable. Perché debba farlo, non ne sono del tutto sicuro - una buona domanda per il team del motore di archiviazione , immagino. Credo che SQL Server 2012 gestisca alcuni di questi scenari molto meglio, FWIW - ma devo ancora fare test approfonditi.


4
Questo comportamento è cambiato notevolmente nelle versioni successive di SQL Server. Ho controllato RC2 2016 e ho scoperto che per questo esatto scenario e 1 milione di righe nella tabella vengono generati solo 29 record di registro durante il passaggio da NULL a NOT NULL se tutti i valori erano già stati specificati per la colonna.
Endrju,

32

Quando si esegue il comando

ALTER COLUMN ... NOT NULL

Questo sembra essere implementato come un'operazione Aggiungi colonna, Aggiorna, Elimina colonna.

  • Viene inserita una nuova riga sys.sysrscolsper rappresentare una nuova colonna. Il statusbit per 128viene impostato per indicare che la colonna non consente NULLs
  • Viene eseguito un aggiornamento su ogni riga della tabella impostando il nuovo valore columnn su quello del vecchio valore colum. Se le versioni "precedenti" e "successive" della riga sono esattamente le stesse, ciò non comporta la scrittura di alcun elemento nel registro delle transazioni, altrimenti viene registrato l'aggiornamento.
  • La colonna originale è contrassegnata come eliminata (si tratta di una modifica solo dei metadati sys.sysrscols. rscolidAggiornata a un intero di grandi dimensioni e il statusbit 2 impostato su indicato è eliminato)
  • La voce sys.sysrscolsper la nuova colonna viene modificata per darle quella rscoliddella vecchia colonna.

L'operazione che ha il potenziale per causare molte registrazioni è la UPDATEdi tutte le righe nella tabella, tuttavia ciò non significa che ciò accadrà sempre . Se le immagini "prima" e "dopo" della riga sono identiche, questo verrà trattato come un aggiornamento non aggiornante e non verrà registrato dai miei test finora.

Quindi la spiegazione del motivo per cui stai ricevendo molte registrazioni dipenderà dal motivo per cui esattamente le versioni "prima" e "dopo" della riga non sono le stesse.

Per le colonne a lunghezza variabile memorizzate nel FixedVarformato ho scoperto che l'impostazione per NOT NULLcausare sempre una modifica nella riga che deve essere registrata. Il conteggio delle colonne e il conteggio delle colonne di lunghezza variabile vengono entrambi incrementati e la nuova colonna viene aggiunta alla fine della sezione di lunghezza variabile duplicando i dati.

datetimeoffset(0)è comunque a lunghezza fissa e per le colonne a lunghezza fissa memorizzate nel FixedVarformato, sia la vecchia che la nuova colonna sembrano avere lo stesso slot nella porzione di dati a lunghezza fissa della riga e poiché entrambi hanno la stessa lunghezza e valore il "prima" e Le versioni "after" della riga sono uguali . Questo può essere visto nella risposta di @ Aaron. Entrambe le versioni della riga prima e dopo ALTER TABLE dbo.floob ALTER COLUMN bar INT NOT NULL;sono

0x10000c00 01000000 00000000 020000

Questo non è registrato.

Logicamente dalla mia descrizione degli eventi, la riga dovrebbe in effetti essere diversa qui poiché il conteggio delle colonne 02dovrebbe essere aumentato a, 03ma in realtà tale cambiamento non si verifica nella pratica.

Alcuni possibili motivi per cui questo può accadere in una colonna di lunghezza fissa sono

  • Se la colonna fosse stata originariamente dichiarata come SPARSEallora, la nuova colonna verrebbe memorizzata in una parte diversa della riga rispetto all'originale, causando differenze tra le immagini della riga prima e dopo.
  • Se si utilizza una delle opzioni di compressione, le versioni precedente e successiva della riga saranno diverse in quanto la sezione di conteggio delle colonne nell'array CD viene incrementata.
  • Sui database con una delle opzioni di isolamento dell'istantanea abilitata, vengono aggiornate le informazioni sul controllo delle versioni in ogni riga (@SQL Kiwi sottolinea che ciò può verificarsi anche nei database senza SI abilitato, come descritto qui ).
  • Potrebbero esserci delle ALTER TABLEoperazioni precedenti implementate come modifica solo dei metadati e non ancora applicate alla riga. Ad esempio, se è stata aggiunta una nuova colonna di lunghezza variabile nullable, questa viene inizialmente applicata come modifica solo dei metadati e viene effettivamente scritta nelle righe solo al successivo aggiornamento (la scrittura che si verifica effettivamente in quest'ultima istanza è solo l'aggiornamento a la sezione di conteggio delle colonne e la colonna NULL_BITMAPas NULL varcharalla fine della riga non occupano spazio)

5

Ho riscontrato lo stesso problema per quanto riguarda una tabella con 200.000.000 di righe. Inizialmente ho aggiunto la colonna nullable, quindi ho aggiornato tutte le righe e infine ho modificato la colonna NOT NULLtramite ALTER TABLE ALTER COLUMNun'istruzione. Ciò ha comportato due enormi transazioni che hanno fatto esplodere il file di registro incredibilmente (crescita di 170 GB).

Il modo più veloce che ho trovato è stato il seguente:

  1. Aggiungi la colonna usando un valore predefinito

    ALTER TABLE table1 ADD column1 INT NOT NULL DEFAULT (1)
  2. Eliminare il vincolo predefinito utilizzando SQL dinamico poiché il vincolo non è stato nominato prima:

    DECLARE 
        @constraint_name SYSNAME,
        @stmt NVARCHAR(510);
    
    SELECT @CONSTRAINT_NAME = DC.NAME
    FROM SYS.DEFAULT_CONSTRAINTS DC
    INNER JOIN SYS.COLUMNS C
        ON DC.PARENT_OBJECT_ID = C.OBJECT_ID
        AND DC.PARENT_COLUMN_ID = C.COLUMN_ID
    WHERE
        PARENT_OBJECT_ID = OBJECT_ID('table1')
        AND C.NAME = 'column1';

Il tempo di esecuzione è passato da> 30 minuti a 10 minuti, inclusa la replica delle modifiche tramite la replica transazionale. Sto eseguendo un'installazione di SQL Server 2008 (SP2).


2

Ho eseguito il seguente test:

create table tblCheckResult(
        ColID   int identity
    ,   dtoDateTime Datetimeoffset(0) null
    )

 go

insert into tblCheckResult (dtoDateTime)
select getdate()
go 10000

checkpoint 

ALTER TABLE tblCheckResult 
ALTER COLUMN [dtoDateTime] [datetimeoffset](0) NOT NULL

select * from fn_dblog(null,null)

Ritengo che ciò abbia a che fare con lo spazio riservato contenuto nel registro nel caso in cui venga eseguito il rollback della transazione. Cerca nella funzione fn_dblog la colonna "Log Reserve" per la riga LOP_BEGIN_XACT e vedi quanto spazio sta cercando di prenotare.


Se ci provi select * FROM fn_dblog(null, null) where AllocUnitName='dbo.tblCheckResult' AND Operation = 'LOP_MODIFY_ROW'puoi vedere gli aggiornamenti di 10000 righe.
Martin Smith,

-2

Il comportamento per questo è diverso in SQL Server 2012. Vedere http://rusanu.com/2011/07/13/online-non-null-with-values-column-add-in-sql-server-11/

Il numero di record di registro generati per SQL Server 2008 R2 e versioni successive sarà significativamente superiore al numero di record di registro per SQL Server 2012.


2
La domanda è perché la modifica di una colonna esistente NOT NULLprovoca la registrazione. La modifica nel 2012 riguarda l'aggiunta di una nuova NOT NULLcolonna con un valore predefinito.
Martin Smith,
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.