[ Parte 1 | Parte 2 | Parte 3 | Parte 4]
Um problema que vi surgir algumas vezes recentemente é o cenário em que você criou uma coluna IDENTITY como um INT e agora está se aproximando do limite superior e precisa torná-lo maior (BIGINT). Se sua mesa for grande o suficiente para atingir o limite superior de um número inteiro (mais de 2 bilhões), essa não é uma operação que você pode realizar entre o almoço e o intervalo para o café em uma terça-feira. Esta série explorará a mecânica por trás dessa mudança e diferentes maneiras de fazer isso acontecer com impactos variados no tempo de atividade. Na primeira parte, eu queria dar uma olhada no impacto físico de mudar um INT para um BIGINT sem nenhuma das outras variáveis.
O que realmente acontece quando você amplia um INT?
INT e BIGINT são tipos de dados de tamanho fixo, portanto, uma conversão de um para o outro deve tocar a página, tornando esta uma operação de tamanho de dados. Isso é contra-intuitivo, porque parece que não seria possível uma mudança de tipo de dados de INT para BIGINT para exigir o espaço adicional na página imediatamente (e para uma coluna IDENTITY, nunca). Pensando logicamente, este é um espaço que não poderia ser necessário até mais tarde, quando um valor INT existente foi alterado para um valor> 4 bytes. Mas não é assim que funciona hoje. Vamos criar uma tabela simples e ver:
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
Uma consulta simples pode me dizer a página baixa e alta alocada para esse objeto, bem como a contagem total de páginas:
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 );
Agora, se eu executar essa consulta antes e depois de alterar o tipo de dados de INT para BIGINT:
ALTER TABLE dbo.FirstTest ALTER COLUMN RowID bigint;
Eu vejo esses resultados:
-- before: lo_page hi_page page_count ------- ------- ---------- 243 303 17 -- after: lo_page hi_page page_count ------- ------- ---------- 243 319 33
É claro que 16 novas páginas foram adicionadas para abrir espaço para o espaço adicional necessário (mesmo sabendo que nenhum dos valores na tabela realmente requer 8 bytes). Mas isso não foi feito da maneira que você imagina – em vez de alargar a coluna nas páginas existentes, as linhas foram movidas para novas páginas, com ponteiros deixados para trás em seu lugar. Olhando para a página 243 antes e depois (com a não documentada
DBCC PAGE
):-- ******** 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
Então, se olharmos para o alvo do ponteiro, página 296, slot 376, veremos:
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
Esta é uma mudança muito disruptiva na estrutura da tabela, obviamente. (E uma observação lateral interessante:a ordem física das colunas, RowID e preenchimento, foram invertidas na página.) O espaço reservado salta de 136 KB para 264 KB, e a fragmentação média aumenta modestamente de 33,3% para 40%. Esse espaço não é recuperado por uma reconstrução, online ou não, ou uma reorganização, e – como veremos em breve – isso não ocorre porque a tabela é pequena demais para ser beneficiada.
Observação:isso é verdade mesmo nas versões mais recentes do SQL Server 2016 – embora mais e mais operações como essa tenham sido aprimoradas para se tornarem operações somente de metadados nas versões modernas, esta ainda não foi corrigida, embora claramente pode ser – novamente, especialmente no caso em que a coluna é uma coluna IDENTITY, que não pode ser atualizada por definição.
Realizar a operação com a nova sintaxe ALTER COLUMN/ONLINE, da qual falei no ano passado, rende algumas diferenças:
-- drop / re-create here ALTER TABLE dbo.FirstTest ALTER COLUMN RowID bigint WITH (ONLINE = ON);
Agora o antes e depois fica:
-- before: lo_page hi_page page_count ------- ------- ---------- 243 303 17 -- after: lo_page hi_page page_count ------- ------- ---------- 307 351 17
Nesse caso, ainda era uma operação de tamanho de dados, mas as páginas existentes foram copiadas e recriadas devido à opção ONLINE. Você pode se perguntar por que, quando alteramos o tamanho da coluna como uma operação ONLINE, a tabela é capaz de empilhar mais dados no mesmo número de páginas? Cada página agora é mais densa (menos linhas, mas mais dados por página), ao custo de dispersão – a fragmentação dobra de 33,3% para 66,7%. O espaço usado mostra mais dados no mesmo espaço reservado (de 72 KB / 136 KB a 96 KB / 136 KB).
E em escala maior?
Vamos descartar a tabela, recriá-la e preenchê-la com muito mais dados:
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;
Desde o início, temos agora 8.657 páginas, um nível de fragmentação de 0,09% e o espaço utilizado é de 69.208 KB / 69.256 KB.
Se alterarmos o tipo de dados para bigint, saltamos para 25.630 páginas, a fragmentação é reduzida para 0,06% e o espaço utilizado é de 205.032 KB / 205.064 KB. Uma reconstrução online não muda nada, nem uma reorganização. Todo o processo, incluindo uma reconstrução, leva cerca de 97 segundos na minha máquina (o preenchimento de dados levou 2 segundos).
Se alterarmos o tipo de dados para bigint usando ONLINE, o aumento será de apenas 11.140 páginas, a fragmentação será de 85,5% e o espaço utilizado será de 89.088 KB / 89160 KB. Reconstruções e reorganizações online ainda não mudam nada. Desta vez, todo o processo leva apenas cerca de um minuto. Portanto, a nova sintaxe definitivamente leva a operações mais rápidas e menos espaço em disco adicional, mas alta fragmentação. Eu vou levar.
Próximo
Tenho certeza que você está olhando para meus testes acima e se perguntando sobre algumas coisas. Mais importante, por que a mesa é um monte? Eu queria investigar o que realmente acontece com a estrutura da página e a contagem de páginas sem índices, chaves ou restrições confundindo os detalhes. Você também pode se perguntar por que essa alteração foi tão fácil - em um cenário em que você precisa alterar uma coluna IDENTITY verdadeira, provavelmente também é a chave primária clusterizada e tem dependências de chave estrangeira em outras tabelas. Isso definitivamente introduz alguns soluços no processo. Vamos dar uma olhada nessas coisas no próximo post da série.
—
[ Parte 1 | Parte 2 | Parte 3 | Parte 4]