ALTER TABLE ... DROP COLUMN é realmente uma operação apenas de metadados?

11

Encontrei várias fontes que afirmam ALTER TABLE ... DROP COLUMN é uma operação apenas de metadados.

Fonte

Como isso pode ser? Os dados durante uma DROP COLUMN não precisam ser limpos dos índices não clusterizados subjacentes e do índice / heap clusterizado?

Além disso, por que o Microsoft Docs implica que é uma operação totalmente registrada?

As modificações feitas na tabela são registradas e totalmente recuperáveis. Alterações que afetam todas as linhas de tabelas grandes, como descartar uma coluna ou, em algumas edições do SQL Server, adicionar uma coluna NOT NULL com um valor padrão, podem levar muito tempo para concluir e gerar muitos registros de log . Execute essas instruções ALTER TABLE com o mesmo cuidado que qualquer instrução INSERT, UPDATE ou DELETE que afeta muitas linhas.

Como pergunta secundária: como o mecanismo controla as colunas descartadas se os dados não são removidos das páginas subjacentes?

George.Palacios
fonte
2
Bem, acho que o idioma sobreviveu a muitas versões do produto e a muitas outras iterações da documentação. Com o tempo, mais e mais operações envolvendo colunas se tornaram on-line / apenas metadados. Talvez seja um exemplo específico ruim agora, mas o objetivo da frase é simplesmente avisá-lo de que, em geral, algumas operações alter podem ser operações de tamanho de dados em determinados cenários, em vez de listar todos os cenários específicos.
Aaron Bertrand

Respostas:

14

Há certas circunstâncias em que largar uma coluna pode ser uma operação apenas de metadados. As definições de coluna para qualquer tabela não são incluídas em todas as páginas em que as linhas são armazenadas, as definições de coluna são armazenadas apenas nos metadados do banco de dados, incluindo sys.sysrowsets, sys.sysrscols, etc.

Ao eliminar uma coluna que não é referenciada por nenhum outro objeto, o mecanismo de armazenamento simplesmente marca a definição da coluna como não mais presente, excluindo os detalhes pertinentes de várias tabelas do sistema. A ação de excluir os metadados invalida o cache do procedimento, exigindo uma recompilação sempre que uma consulta fizer referência posteriormente a essa tabela. Como a recompilação retorna apenas colunas que existem atualmente na tabela, os detalhes da coluna eliminada nunca são solicitados; o mecanismo de armazenamento ignora os bytes armazenados em cada página dessa coluna, como se a coluna não existisse mais.

Quando uma operação DML subsequente ocorre na tabela, as páginas afetadas são reescritas sem os dados da coluna descartada. Se você recriar um índice em cluster ou um heap, todos os bytes da coluna descartada naturalmente não serão gravados de volta na página no disco. Isso efetivamente espalha a carga de soltar a coluna ao longo do tempo, tornando-a menos perceptível.

Há circunstâncias em que você não pode soltar uma coluna, como quando a coluna é incluída em um índice ou quando você criou manualmente um objeto de estatística para a coluna. Eu escrevi uma postagem de blog mostrando o erro que é apresentado ao tentar alterar uma coluna com um objeto de estatísticas criado manualmente. A mesma semântica se aplica ao descartar uma coluna - se a coluna for referenciada por qualquer outro objeto, ela não poderá ser simplesmente descartada. O objeto de referência deve ser alterado primeiro e, em seguida, a coluna pode ser descartada.

Isso é bastante fácil de mostrar, observando o conteúdo do log de transações depois de soltar uma coluna. O código abaixo cria uma tabela com uma única coluna de caracteres de 8.000 caracteres. Ele adiciona uma linha, a elimina e exibe o conteúdo do log de transações aplicável à operação de descarte. Os registros de log mostram modificações em várias tabelas do sistema em que as definições de tabela e coluna são armazenadas. Se os dados da coluna estivessem realmente sendo excluídos das páginas alocadas à tabela, você veria registros de log registrando os dados reais da página; não existem tais registros.

DROP TABLE IF EXISTS dbo.DropColumnTest;
GO
CREATE TABLE dbo.DropColumnTest
(
    rid int NOT NULL
        CONSTRAINT DropColumnTest_pkc
        PRIMARY KEY CLUSTERED
    , someCol varchar(8000) NOT NULL
);

INSERT INTO dbo.DropColumnTest (rid, someCol)
SELECT 1, REPLICATE('Z', 8000);
GO

DECLARE @startLSN nvarchar(25);

SELECT TOP(1) @startLSN = dl.[Current LSN]
FROM sys.fn_dblog(NULL, NULL) dl
ORDER BY dl.[Current LSN] DESC;

DECLARE @a int = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10),      LEFT(@startLSN, 8), 0), 1)
      , @b int = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10), SUBSTRING(@startLSN, 10, 8), 0), 1)
      , @c int = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10),     RIGHT(@startLSN, 4), 0), 1);

SELECT @startLSN = CONVERT(varchar(8), @a, 1) 
    + ':' + CONVERT(varchar(8), @b, 1) 
    + ':' + CONVERT(varchar(8), @c, 1)

ALTER TABLE dbo.DropColumnTest DROP COLUMN someCol;

SELECT *
FROM sys.fn_dblog(@startLSN, NULL)


--modify an existing data row 
SELECT TOP(1) @startLSN = dl.[Current LSN]
FROM sys.fn_dblog(NULL, NULL) dl
ORDER BY dl.[Current LSN] DESC;

SET @a = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10),      LEFT(@startLSN, 8), 0), 1);
SET @b = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10), SUBSTRING(@startLSN, 10, 8), 0), 1);
SET @c = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10),     RIGHT(@startLSN, 4), 0), 1);

SELECT @startLSN = CONVERT(varchar(8), @a, 1) 
    + ':' + CONVERT(varchar(8), @b, 1) 
    + ':' + CONVERT(varchar(8), @c, 1)

UPDATE dbo.DropColumnTest SET rid = 2;

SELECT *
FROM sys.fn_dblog(@startLSN, NULL)

(A saída é grande demais para ser mostrada aqui e o dbfiddle.uk não permitirá que eu acesse fn_dblog)

O primeiro conjunto de saída mostra o log como resultado da instrução DDL descartando a coluna. O segundo conjunto de saída mostra o log após a execução da instrução DML onde atualizamos a ridcoluna. No segundo conjunto de resultados, vemos registros de log indicando uma exclusão no dbo.DropColumnTest, seguido de uma inserção no dbo.DropColumnTest. Cada comprimento do registro de log é 8116, indicando que a página real foi atualizada.

Como você pode ver na saída do fn_dblogcomando no teste acima, toda a operação é totalmente registrada. Isso vale para a recuperação simples, bem como a recuperação completa. A terminologia "totalmente registrada" pode ser mal interpretada, pois a modificação dos dados não é registrada. Não é isso que acontece - a modificação é registrada e pode ser totalmente revertida. O log está simplesmente registrando apenas as páginas que foram tocadas e, como nenhuma das páginas de dados da tabela foi registrada pela operação DDL, a DROP COLUMNreversão e qualquer reversão que possa ocorrer ocorrerão extremamente rapidamente, independentemente do tamanho da tabela.

Para fins científicos , o código a seguir irá despejar as páginas de dados da tabela incluída no código acima, usando o DBCC PAGEestilo "3". O estilo "3" indica que queremos o cabeçalho da página mais uma interpretação detalhada por linha . O código usa um cursor para exibir os detalhes de todas as páginas da tabela; portanto, convém não executar isso em uma tabela grande.

DBCC TRACEON(3604); --directs out from DBCC commands to the console, instead of the error log
DECLARE @dbid int = DB_ID();
DECLARE @fileid int;
DECLARE @pageid int;
DECLARE cur CURSOR LOCAL FORWARD_ONLY STATIC READ_ONLY
FOR
SELECT dpa.allocated_page_file_id
    , dpa.allocated_page_page_id
FROM sys.schemas s  
    INNER JOIN sys.objects o ON o.schema_id = s.schema_id
CROSS APPLY sys.dm_db_database_page_allocations(DB_ID(), o.object_id, NULL, NULL, 'DETAILED') dpa
WHERE o.name = N'DropColumnTest'
    AND s.name = N'dbo'
    AND dpa.page_type_desc = N'DATA_PAGE';
OPEN cur;
FETCH NEXT FROM cur INTO @fileid, @pageid;
WHILE @@FETCH_STATUS = 0
BEGIN
    DBCC PAGE (@dbid, @fileid, @pageid, 3);
    FETCH NEXT FROM cur INTO @fileid, @pageid;
END
CLOSE cur;
DEALLOCATE cur;
DBCC TRACEOFF(3604);

Observando a saída da primeira página da minha demonstração (após a queda da coluna, mas antes da atualização da coluna), vejo o seguinte:

PÁGINA: (1: 100104)


AMORTECEDOR:


BUF @ 0x0000021793E42040

bpage = 0x000002175A7A0000 bhash = 0x0000000000000000 bpageno = (1: 100104)
bdbid = 10 breferences = 1 bcputicks = 0
bsampleCount = 0 bUse1 = 13760 bstat = 0x10b
blog = 0x212121cc bnext = 0x0000000000000000 bDirtyContext = 0x000002175004B640
bstat2 = 0x0                        

CABEÇALHO DA PÁGINA:


Page @ 0x000002175A7A0000

m_pageId = (1: 100104) m_headerVersion = 1 m_type = 1
m_typeFlagBits = 0x0 m_level = 0 m_flagBits = 0xc000
m_objId (AllocUnitId.idObj) = 300 m_indexId (AllocUnitId.idInd) = 256 
Metadados: AllocUnitId = 72057594057588736                                
Metadados: PartitionId = 72057594051756032 Metadados: IndexId = 1
Metadados: ObjectId = 174623665 m_prevPage = (0: 0) m_nextPage = (0: 0)
pminlen = 8 m_slotCnt = 1 m_freeCnt = 79
m_freeData = 8111 m_reservedCnt = 0 m_lsn = (616: 14191: 25)
m_xactReserved = 0 m_xdesId = (0: 0) m_ghostRecCnt = 0
m_tornBits = 0 ID de frag do banco de dados = 1                      

Status de alocação

GAM (1: 2) = SGAM ATRIBUÍDO (1: 3) = NÃO ATRIBUÍDO          
PFS (1: 97056) = 0x40 ALOCADO 0_PCT_FULL DIFF (1: 6) = ALTERADO
ML (1: 7) = NÃO MIN_LOGGED           

Comprimento do deslocamento 0x60 do slot 0x60 8015

Tipo de registro = PRIMARY_RECORD Atributos do registro = NULL_BITMAP VARIABLE_COLUMNS
Tamanho do registro = 8015                  
Despejo de memória @ 0x000000B75227A060

0000000000000000: 30000800 01000000 02000001 004f1f5a 5a5a5a5a 0 ............ O.ZZZZZ
0000000000000014: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
.
.
.
0000000000001F2C: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
0000000000001F40: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a ZZZZZZZZZZZZZZZZ

Slot 0 Coluna 1 Deslocamento 0x4 Comprimento 4 Comprimento (físico) 4

rid = 1                             

Slot 0 Coluna 67108865 Deslocamento 0xf Comprimento 0 Comprimento (físico) 8000

DROPPED = NULL                      

Slot 0 Deslocamento 0x0 Comprimento 0 Comprimento (físico) 0

KeyHashValue = (8194443284a0)       

Eu removi a maior parte do despejo de página bruta da saída mostrada acima por questões de concisão No final da saída, você verá isso na ridcoluna:

Slot 0 Coluna 1 Deslocamento 0x4 Comprimento 4 Comprimento (físico) 4

rid = 1                             

A última linha acima rid = 1,, retorna o nome da coluna e o valor atual armazenado na coluna na página.

Em seguida, você verá o seguinte:

Slot 0 Coluna 67108865 Deslocamento 0xf Comprimento 0 Comprimento (físico) 8000

DROPPED = NULL                      

A saída mostra que o Slot 0 contém uma coluna excluída, em virtude do DELETEDtexto onde o nome da coluna normalmente estaria. O valor da coluna é retornado como NULLdesde que a coluna foi excluída. No entanto, como você pode ver nos dados brutos, o valor de 8.000 caracteres REPLICATE('Z', 8000), para essa coluna ainda existe na página. Esta é uma amostra dessa parte da saída DBCC PAGE:

0000000000001EDC: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
0000000000001EF0: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
0000000000001F04: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
0000000000001F18: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
Max Vernon
fonte