¿Por qué ALTER COLUMN to NOT NULL causa un crecimiento masivo de archivos de registro?

56

Tengo una tabla con 64 millones de filas que toman 4,3 GB en disco para sus datos.

Cada fila tiene aproximadamente 30 bytes de columnas enteras, más una NVARCHAR(255)columna variable para texto.

Agregué una columna NULLABLE con tipo de datos Datetimeoffset(0).

Luego ACTUALIZÉ esta columna para cada fila y me aseguré de que todas las nuevas inserciones coloquen un valor en esta columna.

Una vez que no hubo entradas NULL, ejecuté este comando para hacer que mi nuevo campo sea obligatorio:

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

El resultado fue un enorme crecimiento en el tamaño del registro de transacciones, de 6GB a más de 36GB hasta que se quedó sin espacio.

¿Alguien tiene alguna idea de lo que está haciendo SQL Server 2008 R2 para que este simple comando resulte en un crecimiento tan grande?

PapillonUK
fuente
77
SQL Server 2012 Enterprise agrega la capacidad de agregar una NOT NULLcolumna con una operación predeterminada como metadatos. Consulte también "Agregar columnas NO NULL como una operación en línea" en la documentación .
Paul White

Respuestas:

48

Cuando cambia una columna a NOT NULL, SQL Server tiene que tocar cada página, incluso si no hay valores NULL. Dependiendo de su factor de relleno, esto podría conducir a muchas divisiones de página. Cada página que se toca, por supuesto, debe registrarse, y sospecho que debido a las divisiones, es posible que se deban registrar dos cambios para muchas páginas. Sin embargo, dado que todo se hace en una sola pasada, el registro debe tener en cuenta todos los cambios para que, si presiona cancelar, sepa exactamente qué deshacer.


Un ejemplo. Tabla simple:

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

Ahora, veamos los detalles de la página. Primero tenemos que averiguar qué página y DB_ID estamos tratando. En mi caso, creé una base de datos llamada fooy el DB_ID resultó ser 5.

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

La salida indicaba que estaba interesado en la página 159 (la única fila en la DBCC INDsalida con PageType = 1).

Ahora, veamos algunos detalles de la página seleccionada a medida que avanzamos por el escenario del OP.

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

ingrese la descripción de la imagen aquí

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

ingrese la descripción de la imagen aquí

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

ingrese la descripción de la imagen aquí

Ahora, no tengo todas las respuestas a esto, ya que no soy un tipo interno profundo. Pero está claro que, si bien tanto la operación de actualización como la adición de la restricción NOT NULL, sin lugar a dudas, escriben en la página, este último lo hace de una manera completamente diferente. Parece que en realidad cambia la estructura del registro, en lugar de solo jugar con bits, cambiando la columna anulable por una columna no anulable. Por qué tiene que hacer eso, no estoy muy seguro: una buena pregunta para el equipo del motor de almacenamiento , supongo. Creo que SQL Server 2012 maneja algunos de estos escenarios mucho mejor, FWIW, pero todavía tengo que hacer pruebas exhaustivas.

Aaron Bertrand
fuente
44
Este comportamiento cambió considerablemente en versiones posteriores de SQL Server. Revisé 2016 RC2 y descubrí que para este escenario exacto y 1 millón de filas en la tabla, solo se generan 29 registros durante el cambio de NULL a NOT NULL si todos los valores ya se especificaron para la columna.
Endrju
32

Al ejecutar el comando

ALTER COLUMN ... NOT NULL

Esto parece implementarse como una operación Agregar columna, Actualizar, Eliminar columna.

  • Se inserta una nueva fila sys.sysrscolspara representar una nueva columna. El statusbit para 128se establece indicando que la columna no permite NULLs
  • Se lleva a cabo una actualización en cada fila de la tabla configurando el nuevo valor de la columna al valor de la columna anterior. Si las versiones "antes" y "después" de la fila son exactamente las mismas, esto no hace que se escriba nada en el registro de transacciones; de lo contrario, se registra la actualización.
  • La columna original está marcada como descartada (este es un cambio de metadatos solo sys.sysrscols. rscolidActualizado a un entero grande y el statusbit 2 configurado como indicado)
  • La entrada sys.sysrscolspara la nueva columna se modifica para darle el rscolidde la columna anterior.

La operación que tiene el potencial de causar muchos registros es la UPDATEde todas las filas de la tabla, sin embargo, eso no significa que esto siempre ocurra. Si las imágenes "antes" y "después" de la fila son idénticas, esto se tratará como una actualización que no se actualiza y no se registrará desde mi prueba hasta el momento.

Por lo tanto, la explicación de por qué está obteniendo muchos registros dependerá de por qué exactamente las versiones "antes" y "después" de la fila no son las mismas.

Para las columnas de longitud variable almacenadas en el FixedVarformato, encontré que la configuración NOT NULLsiempre provoca un cambio en la fila que debe registrarse. El recuento de columnas y el recuento de columnas de longitud variable se incrementan y la nueva columna se agrega al final de la sección de longitud variable duplicando los datos.

datetimeoffset(0)es, sin embargo, de longitud fija y para las columnas de longitud fija almacenadas en el FixedVarformato, las columnas antiguas y nuevas parecen tener el mismo espacio en la porción de datos de longitud fija de la fila y, como ambas tienen la misma longitud y valor, el "antes" y Las versiones "después" de la fila son las mismas . Esto se puede ver en la respuesta de @ Aaron. Ambas versiones de la fila antes y después del ALTER TABLE dbo.floob ALTER COLUMN bar INT NOT NULL;son

0x10000c00 01000000 00000000 020000

Esto no está registrado.

Lógicamente, a partir de mi descripción de los eventos, la fila debería ser diferente aquí, ya que el recuento de columnas 02debería aumentarse, 03pero en la práctica no ocurre tal cambio.

Algunas razones posibles de por qué esto puede ocurrir en una columna de longitud fija son

  • Si la columna se declaró originalmente como SPARSEentonces, la nueva columna se almacenaría en una parte diferente de la fila del original, lo que provocaría que las imágenes de la fila anterior y posterior fueran diferentes.
  • Si está utilizando alguna de las opciones de compresión, las versiones anterior y posterior de la fila serán diferentes a medida que se incremente la sección de recuento de columnas en la matriz de CD.
  • En las bases de datos con una de las opciones de aislamiento de instantáneas habilitadas, se actualiza la información de versiones en cada fila (@SQL Kiwi señala que esto también puede ocurrir en bases de datos sin SI habilitado como se describe aquí ).
  • Puede haber alguna ALTER TABLEoperación previa que se implementó como un cambio de metadatos solamente y que aún no se ha aplicado a la fila. Por ejemplo, si se agregó una nueva columna de longitud variable anulable, esto se aplica originalmente como un cambio de metadatos y solo se escribe en las filas la próxima vez que se actualizan (la escritura que realmente ocurre en esta última instancia es solo actualizaciones para la sección de recuento de columnas y NULL_BITMAPcomo NULL varcharcolumna al final de la fila no ocupan espacio)
Martin Smith
fuente
5

Me enfrenté al mismo problema con respecto a una tabla que tenía 200,000,000 de filas. Inicialmente agregué la columna anulable, luego actualicé todas las filas y finalmente modifiqué la columna a NOT NULLtravés de una ALTER TABLE ALTER COLUMNdeclaración. Esto resultó en dos grandes transacciones que explotaron el archivo de registro increíblemente (crecimiento de 170 GB).

La forma más rápida que encontré fue la siguiente:

  1. Agregue la columna usando un valor predeterminado

    ALTER TABLE table1 ADD column1 INT NOT NULL DEFAULT (1)
  2. Descarte la restricción predeterminada utilizando SQL dinámico ya que la restricción no se ha nombrado antes:

    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';
    

El tiempo de ejecución bajó de> 30 minutos a 10 minutos, incluida la replicación de los cambios a través de la replicación transaccional. Estoy ejecutando una instalación de SQL Server 2008 (SP2).

Fritz
fuente
2

Ejecuté la siguiente prueba:

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)

Creo que esto tiene que ver con el espacio reservado que contiene el registro en caso de que revierta la transacción. Mire en la función fn_dblog en la columna 'Log Reserve' para la fila LOP_BEGIN_XACT y vea cuánto espacio está tratando de reservar.

Keith Tate
fuente
Si lo intentas select * FROM fn_dblog(null, null) where AllocUnitName='dbo.tblCheckResult' AND Operation = 'LOP_MODIFY_ROW', puedes ver las actualizaciones de 10000 filas.
Martin Smith
-2

El comportamiento para esto es diferente en SQL Server 2012. Ver http://rusanu.com/2011/07/13/online-non-null-with-values-column-add-in-sql-server-11/

El número de registros generados para SQL Server 2008 R2 y versiones posteriores será significativamente mayor que el número de registros para SQL Server 2012.

Solucionar problemas de SQL
fuente
2
La pregunta es por qué alterar una columna existente para NOT NULLprovocar el registro. El cambio en 2012 se trata de agregar una nueva NOT NULLcolumna con un valor predeterminado.
Martin Smith