Por que ALTER COLUMN para NOT NULL causa um grande crescimento no arquivo de log?

56

Eu tenho uma tabela com linhas de 64m tomando 4,3 GB no disco para seus dados.

Cada linha tem cerca de 30 bytes de colunas inteiras, além de uma NVARCHAR(255)coluna variável para texto.

Adicionei uma coluna NULLABLE com tipo de dados Datetimeoffset(0).

Atualizei essa coluna para cada linha e verifiquei se todas as novas inserções colocam um valor nessa coluna.

Como não havia entradas NULL, executei este comando para tornar meu novo campo obrigatório:

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

O resultado foi um enorme crescimento no tamanho do log de transações - de 6 GB para mais de 36 GB até ficar sem espaço!

Alguém tem alguma idéia do que o SQL Server 2008 R2 está fazendo para que esse comando simples resulte em um crescimento tão grande?

PapillonUK
fonte
7
O SQL Server 2012 Enterprise adiciona a capacidade de adicionar uma NOT NULLcoluna com um padrão como uma operação de metadados. Consulte também "Adicionando colunas NOT NULL como uma operação online" na documentação .
Paul White

Respostas:

48

Quando você altera uma coluna para NOT NULL, o SQL Server precisa tocar em todas as páginas, mesmo se não houver valores NULL. Dependendo do fator de preenchimento, isso pode levar a muitas divisões de página. É claro que todas as páginas tocadas precisam ser registradas, e suspeito que, devido às divisões, duas alterações possam ter que ser registradas para muitas páginas. Porém, como tudo é feito em uma única passagem, o log deve contabilizar todas as alterações para que, se você clicar em cancelar, saiba exatamente o que desfazer.


Um exemplo. Tabela simples:

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

Agora, vejamos os detalhes da página. Primeiro, precisamos descobrir com que página e DB_ID estamos lidando. No meu caso, criei um banco de dados chamado fooe o DB_ID passou a ser 5.

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

A saída indicava que eu estava interessado na página 159 (a única linha na DBCC INDsaída com PageType = 1).

Agora, vejamos alguns detalhes da página selecionados à medida que avançamos no cenário do OP.

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

insira a descrição da imagem aqui

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

insira a descrição da imagem aqui

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

insira a descrição da imagem aqui

Agora, eu não tenho todas as respostas para isso, porque eu não sou um cara interno profundo. Mas é claro que - embora a operação de atualização e a adição da restrição NOT NULL sejam inegavelmente gravadas na página - a última o faz de uma maneira totalmente diferente. Parece realmente alterar a estrutura do registro, em vez de apenas mexer nos bits, trocando a coluna anulável por uma coluna não anulável. Por que isso precisa ser feito, não tenho muita certeza - uma boa pergunta para a equipe do mecanismo de armazenamento , eu acho. Acredito que o SQL Server 2012 lida com alguns desses cenários muito melhor, FWIW - mas ainda não fiz testes exaustivos.

Aaron Bertrand
fonte
4
Esse comportamento mudou consideravelmente nas versões posteriores do SQL Server. Eu verifiquei 2016 RC2 e descobri que, para esse cenário exato e 1 milhão de linhas na tabela, apenas 29 registros de log são gerados durante a alteração de NULL para NOT NULL se todos os valores já estiverem especificados para a coluna.
Endrju
32

Ao executar o comando

ALTER COLUMN ... NOT NULL

Isso parece ser implementado como uma operação Adicionar coluna, Atualizar, Soltar coluna.

  • Uma nova linha é inserida sys.sysrscolspara representar uma nova coluna. O statusbit para 128é definido, indicando que a coluna não permite NULLs
  • Uma atualização é realizada em todas as linhas da tabela, definindo o novo valor da coluna como o valor da coluna antiga. Se as versões "antes" e "depois" da linha forem exatamente iguais, isso não fará com que nada seja gravado no log de transações, caso contrário, a atualização será registrada.
  • A coluna original é marcada como descartada (trata-se apenas de uma mudança de metadados sys.sysrscols. rscolidAtualizada para um número inteiro grande e o statusbit 2 ativado como descartado indicado)
  • A entrada sys.sysrscolspara a nova coluna é alterada para fornecer a rscolidda coluna antiga.

A operação com potencial para causar muitos logs é a UPDATEde todas as linhas da tabela, no entanto, isso não significa que isso sempre ocorra. Se as imagens "antes" e "depois" da linha forem idênticas, isso será tratado como uma atualização sem atualização e não será registrado nos meus testes até o momento.

Portanto, a explicação do motivo pelo qual você está obtendo muitos logs dependerá do motivo pelo qual exatamente as versões "antes" e "depois" da linha não são as mesmas.

Para colunas de comprimento variável armazenadas no FixedVarformato, achei que a configuração NOT NULLsempre causa uma alteração na linha que precisa ser registrada. A contagem de colunas e a contagem de colunas de comprimento variável são incrementadas e a nova coluna é adicionada ao final da seção de comprimento variável duplicando os dados.

datetimeoffset(0)é de comprimento fixo, no entanto, e para colunas de comprimento fixo armazenadas no FixedVarformato, as colunas antigas e novas parecem receber o mesmo slot na parte de dados de comprimento fixo da linha e, pois ambas têm o mesmo comprimento e valorizam o "antes" e As versões "depois" da linha são iguais . Isso pode ser visto na resposta de @ Aaron. Ambas as versões da linha antes e depois da ALTER TABLE dbo.floob ALTER COLUMN bar INT NOT NULL;são

0x10000c00 01000000 00000000 020000

Isto não está registrado.

Logicamente, a partir da minha descrição de eventos, a linha deve de fato ser diferente aqui, pois a contagem de colunas 02deve ser aumentada para, 03mas nenhuma alteração realmente ocorre na prática.

Algumas razões possíveis para isso ocorrer em uma coluna de comprimento fixo são:

  • Se a coluna foi originalmente declarada como SPARSEa nova coluna seria armazenada em uma parte diferente da linha da original, fazendo com que as imagens da linha anterior e posterior fossem diferentes.
  • Se você estiver usando qualquer uma das opções de compactação, as versões anterior e posterior da linha serão diferentes, à medida que a seção de contagem de colunas na matriz do CD for incrementada.
  • Em bancos de dados com uma das opções de isolamento de captura instantânea ativada, as informações de versão em cada linha são atualizadas (o @SQL Kiwi indica que isso também pode ocorrer em bancos de dados sem o SI ativado, conforme descrito aqui ).
  • Pode haver alguma ALTER TABLEoperação anterior que foi implementada como uma alteração apenas de metadados e ainda não foi aplicada à linha. Por exemplo, se uma nova coluna de comprimento variável anulável foi adicionada, ela é aplicada originalmente como uma alteração apenas de metadados e, na verdade, é gravada apenas nas linhas na próxima atualização (a gravação que realmente ocorre nesta última instância é apenas atualização para seção contagem de colunas e NULL_BITMAPcomo uma NULL varcharcoluna no final da linha não ocupa qualquer espaço)
Martin Smith
fonte
5

Eu enfrentei o mesmo problema em uma tabela com 200.000.000 de linhas. Inicialmente, adicionei a coluna anulável, atualizei todas as linhas e, finalmente, alterei a coluna por NOT NULLmeio de uma ALTER TABLE ALTER COLUMNinstrução Isso resultou em duas grandes transações explodindo o arquivo de log incrivelmente (crescimento de 170 GB).

A maneira mais rápida que encontrei foi a seguinte:

  1. Adicione a coluna usando um valor padrão

    ALTER TABLE table1 ADD column1 INT NOT NULL DEFAULT (1)
  2. Elimine a restrição padrão usando o SQL dinâmico, pois a restrição não foi nomeada 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';
    

O tempo de execução caiu de> 30 minutos para 10 minutos, incluindo a replicação das alterações via Replicação Transacional. Estou executando uma instalação do SQL Server 2008 (SP2).

Fritz
fonte
2

Eu executei o seguinte teste:

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)

Acredito que isso tenha a ver com o espaço reservado que o log mantém para o caso de você reverter a transação. Procure na função fn_dblog na coluna 'Reserva de Log' a linha LOP_BEGIN_XACT e veja quanto espaço está tentando reservar.

Keith Tate
fonte
Se você tentar, select * FROM fn_dblog(null, null) where AllocUnitName='dbo.tblCheckResult' AND Operation = 'LOP_MODIFY_ROW'poderá ver as 10000 atualizações de linha.
Martin Smith
-2

O comportamento para isso é diferente no SQL Server 2012. Consulte http://rusanu.com/2011/07/13/online-non-null-with-values-column-add-in-sql-server-11/

O número de registros de log gerados para o SQL Server 2008 R2 e versões posteriores será significativamente maior que o número de registros de log do SQL Server 2012.

Solução de problemasSQL
fonte
2
A questão é por que alterar uma coluna existente para NOT NULLcausar log. A mudança em 2012 é sobre adicionar uma nova NOT NULLcoluna com um padrão.
Martin Smith