[ Parte 1 | Parte 2 | Parte 3 | Parte 4]
Até agora nesta série, demonstrei o impacto físico direto na página ao fazer o upsizing de
int
para bigint
e, em seguida, iterou por vários dos bloqueadores comuns para essa operação. Neste post, eu queria examinar duas possíveis soluções alternativas:uma simples e uma incrivelmente complicada. A maneira mais fácil
Eu fui roubado um pouco do meu trovão em um comentário no meu post anterior – Keith Monroe sugeriu que você poderia apenas reenviar a tabela para o negativo mais baixo limite do tipo de dados inteiro, dobrando sua capacidade para novos valores. Você pode fazer isso com
DBCC CHECKIDENT
:DBCC CHECKIDENT(N'dbo.TableName', RESEED, -2147483648);
Isso pode funcionar, supondo que os valores substitutos não tenham significado para os usuários finais (ou, se tiverem, que os usuários não se assustem ao obter números negativos de repente). Suponho que você poderia enganá-los com uma visão:
CREATE VIEW dbo.ViewNameAS SELECT ID =CONVERT(bigint, CASE WHEN ID <0 THEN (2147483648*2) - 1 + CONVERT(bigint, ID) ELSE ID END) FROM dbo.TableName;
Isso significa que o usuário que adicionou
ID = -2147483648
realmente veria +2147483648
, o usuário que adicionou ID = -2147483647
veria +2147483649
, e assim por diante. Você teria que ajustar outro código para ter certeza de fazer o cálculo reverso quando o usuário passar esse ID
, por exemplo. ALTER PROCEDURE dbo.GetRowByID @ID bigintASBEGIN SET NOCOUNT ON; DECLARE @RealID bigint; SET @RealID =CASE WHEN @ID> 2147483647 THEN @ID - (2147483648*2) + 1 ELSE @ID END; SELECT ID, @ID /*, outras colunas */ FROM dbo.TableName WHERE ID =@RealID;ENDGO
Eu não sou louco por essa ofuscação. De forma alguma. É confuso, enganoso e propenso a erros. E incentiva a visibilidade das chaves substitutas – geralmente,
IDENTITY
os valores não devem ser expostos aos usuários finais, portanto, eles realmente não devem se importar se forem clientes 24, 642, -376 ou números muito maiores em ambos os lados de zero. Esta "solução" também pressupõe que você não tenha código em nenhum lugar que ordene pela
IDENTITY
coluna para apresentar primeiro as linhas inseridas mais recentemente ou inferir que a IDENTITY
mais alta valor deve ser a linha mais recente. Código que faz confiar na ordem de classificação de IDENTITY
coluna, explícita ou implicitamente (o que pode ser mais do que você pensa se for o índice clusterizado), não apresentará mais as linhas na ordem esperada - mostrará todas as linhas criadas após o RESEED
, começando com o primeiro e, em seguida, mostrará todas as linhas criadas antes do RESEED
, começando pelo primeiro. O principal benefício dessa abordagem é que ela não exige que você altere o tipo de dados e, como resultado, o
RESEED
change não requer nenhuma alteração em índices, restrições ou chaves estrangeiras de entrada. A desvantagem – além das mudanças de código mencionadas acima, é claro – é que isso só lhe dá tempo no curto prazo. Eventualmente, você também esgotará todos os inteiros negativos disponíveis. E não pense que isso dobra a vida útil da versão atual da tabela em termos de tempo – em muitos casos, o crescimento de dados está acelerando, não permanecendo constante, então você usará os próximos 2 bilhões de linhas muito mais rápido do que os primeiros 2 bilhões.
Uma maneira mais difícil
Outra abordagem que você pode adotar é parar de usar uma
IDENTITY
coluna completamente; em vez disso, você pode converter usando uma SEQUENCE
. Você pode criar um novo bigint
coluna, defina o padrão para o próximo valor de uma SEQUENCE
, atualize todos esses valores com os valores da coluna original (em lotes, se necessário), elimine a coluna original e renomeie a nova coluna. Vamos criar esta tabela fictícia e inserir uma única linha:CREATE TABLE dbo.SequenceDemo( ID int IDENTITY(1,1), x char(1), CONSTRAINT PK_SD_Identity PRIMARY KEY CLUSTERED (ID));GO INSERT dbo.SequenceDemo(x) VALUES('x');
Em seguida, criaremos umaSEQUENCE
que começa logo além do limite superior de um int:
CREATE SEQUENCE dbo.BeyondIntAS bigintSTART WITH 2147483648 INCREMENT BY 1;
Em seguida, as alterações na tabela necessárias para passar a usar aSEQUENCE
para a nova coluna:
INICIAR TRANSAÇÃO; -- adiciona uma nova coluna de "identidade":ALTER TABLE dbo.SequenceDemo ADD ID2 bigint;GO -- define a nova coluna igual aos valores de identidade existentes -- para tabelas grandes, pode ser necessário fazer isso em lotes:UPDATE dbo.SequenceDemo SET ID2 =ID; -- agora torná-lo não anulável e adicionar o padrão de nossa SEQUENCE:ALTER TABLE dbo.SequenceDemo ALTER COLUMN ID2 bigint NOT NULL;ALTER TABLE dbo.SequenceDemo ADD CONSTRAINT DF_SD_Identity DEFAULT NEXT VALUE FOR dbo.BeyondInt FOR ID2; -- precisa eliminar o PK existente (e quaisquer índices):ALTER TABLE dbo.SequenceDemo DROP CONSTRAINT PK_SD_Identity; -- elimine a coluna antiga e renomeie a nova:ALTER TABLE dbo.SequenceDemo DROP COLUMN ID;EXEC sys.sp_rename N'dbo.SequenceDemo.ID2', N'ID', 'COLUMN'; -- agora coloque o PK de volta:ALTER TABLE dbo.SequenceDemo ADD CONSTRAINT PK_SD_Identity PRIMARY KEY CLUSTERED (ID); COMEÇAR TRANSAÇÃO;
Nesse caso, a próxima inserção produziria os seguintes resultados (observe queSCOPE_IDENTITY()
não retorna mais um valor válido):
INSERIR dbo.SequenceDemo(x) VALUES('y');SELECT Si =SCOPE_IDENTITY();SELECT ID, x FROM dbo.SequenceDemo; /* resultados Si----ID NULL x---------- -1 x2147483648 y */
Se a tabela for grande e você precisar atualizar a nova coluna em lotes em vez da transação única acima, como descrevi aqui – permitindo que os usuários interajam com a tabela enquanto isso – você precisará ter um gatilho em vigor para substituir aSEQUENCE
valor para quaisquer novas linhas inseridas, para que continuem a corresponder ao que é gerado para qualquer código de chamada. (Isso também pressupõe que você ainda tem algum espaço no intervalo inteiro para continuar aceitando algumas atualizações; caso contrário, se você já esgotou o intervalo, terá que fazer algum tempo de inatividade - ou usar a solução fácil acima no curto prazo .)
Vamos largar tudo e começar de novo, depois é só adicionar a nova coluna:
DROP TABLE dbo.SequenceDemo;DROP SEQUENCE dbo.BeyondInt;GO CREATE TABLE dbo.SequenceDemo( ID int IDENTITY(1,1), x char(1), CONSTRAINT PK_SD_Identity PRIMARY KEY CLUSTERED (ID));GO INSERT dbo .SequenceDemo(x) VALUES('x');GO CREATE SEQUENCE dbo.BeyondIntAS bigintSTART WITH 2147483648 INCREMENT BY 1;GO ALTER TABLE dbo.SequenceDemo ADD ID2 bigint;GO
E aqui está o gatilho que adicionaremos:
CREATE TRIGGER dbo.After_SequenceDemoON dbo.SequenceDemoAFTER INSERTASBEGIN UPDATE sd SET sd.ID2 =sd.ID FROM dbo.SequenceDemo AS sd INNER JOIN inserido AS i ON sd.ID =i.ID;END
Desta vez, a próxima inserção continuará a gerar linhas no intervalo inferior de inteiros para ambas as colunas, até que todos os valores pré-existentes tenham sido atualizados e o restante das alterações tenha sido confirmada:
INSERIR dbo.SequenceDemo(x) VALUES('y');SELECT Si =SCOPE_IDENTITY();SELECT ID, ID2, x FROM dbo.SequenceDemo; /* resultados Si----2 ID ID2 x---- ---- --1 NULL x2 2 y */
Agora, podemos continuar atualizando oID2
existente valores enquanto novas linhas continuam sendo inseridas no intervalo inferior:
SET NOCOUNT ON; DECLARE @r INT =1; WHILE @r> 0BEGIN BEGIN TRANSACTION; UPDATE TOP (10000) dbo.SequenceDemo SET ID2 =ID WHERE ID2 É NULL; SET @r =@@ROWCOUNT; COMPROMETA A TRANSAÇÃO; -- PONTO DE VERIFICAÇÃO; -- se simples -- BACKUP LOG ... -- se completoEND
Depois de atualizar todas as linhas existentes, podemos continuar com o restante das alterações e, em seguida, descartar o gatilho:
BEGIN TRANSACTION;ALTER TABLE dbo.SequenceDemo ALTER COLUMN ID2 BIGINT NOT NULL;ALTER TABLE dbo.SequenceDemo ADD CONSTRAINT DF_SD_Identity DEFAULT NEXT VALUE FOR dbo.BeyondInt FOR ID2;ALTER TABLE dbo.SequenceDemo DROP CONSTRAINT PK_SD_Identity;ALTER TABLE dbo.SequenceDemo dbo.SequenceDemo DROP COLUMN ID;EXEC sys.sp_rename N'dbo.SequenceDemo.ID2', N'ID', 'COLUMN';ALTER TABLE dbo.SequenceDemo ADD CONSTRAINT PK_SD_Identity PRIMARY KEY CLUSTERED (ID);DROP TRIGGER dbo.InsteadOf_SequenceDemoCOMMIT TRANSACTION;
Agora, a próxima inserção irá gerar estes valores:
INSERIR dbo.SequenceDemo(x) VALUES('z');SELECT Si =SCOPE_IDENTITY();SELECT ID, x FROM dbo.SequenceDemo; /* resultados Si----ID NULO x---------- -1 x2 y2147483648 z */
Se você tem um código que depende deSCOPE_IDENTITY()
,@@IDENTITY
, ouIDENT_CURRENT()
, também teria que mudar, pois esses valores não são mais preenchidos após uma inserção - embora oOUTPUT
cláusula deve continuar a funcionar corretamente na maioria dos cenários. Se você precisar que seu código continue acreditando, a tabela gera umaIDENTITY
valor, então você poderia usar um gatilho para falsificar isso - no entanto, só seria capaz de preencher@@IDENTITY
na inserção, nãoSCOPE_IDENTITY()
. Isso ainda pode exigir alterações porque, na maioria dos casos, você não deseja confiar em@@IDENTITY
para qualquer coisa (portanto, se você for fazer alterações, remova todas as suposições sobre umaIDENTITY
coluna em tudo).
CREATE TRIGGER dbo.FakeIdentityON dbo.SequenceDemoINSTEAD OF INSERTASBEGIN SET NOCOUNT ON; DECLARE @lowestID bigint =(SELECT MIN(id) FROM inserido); DECLARE @sql nvarchar(max) =N'DECLARE @foo TABLE(ID bigint IDENTITY(' + CONVERT(varchar(32), @lowestID) + N',1));'; SELECT @sql +=N'INSERT @foo VALORES PADRÃO;' DE inserido; EXEC sys.sp_executesql @sql; INSERT dbo.SequenceDemo(ID, x) SELECT ID, x FROM inserido;END
Agora, a próxima inserção irá gerar estes valores:
INSERIR dbo.SequenceDemo(x) VALUES('a');SELECT Si =SCOPE_IDENTITY(), Ident =@@IDENTITY;SELECT ID, x FROM dbo.SequenceDemo; /* resultados Si Ident ---- -----NULL 2147483649 ID x---------- -1 x2 y2147483648 z2147483649 a */
Com essa solução alternativa, você ainda precisaria lidar com outras restrições, índices e tabelas com chaves estrangeiras de entrada. Restrições e índices locais são bem diretos, mas lidarei com a situação mais complexa com chaves estrangeiras na próxima parte desta série.
Um que não funcione, mas gostaria que funcionasse
ALTER TABLE SWITCH
pode ser uma maneira muito poderosa de fazer algumas alterações de metadados que são difíceis de realizar de outra forma. E, ao contrário da crença popular, isso não envolve apenas o particionamento e não se restringe à Enterprise Edition. O código a seguir funcionará no Express e é um método que as pessoas usaram para adicionar ou remover aIDENTITY
propriedade em uma tabela (novamente, sem considerar chaves estrangeiras e todos aqueles outros bloqueadores traquinas).
CREATE TABLE dbo.WithIdentity( ID int IDENTITY(1,1) NOT NULL); CREATE TABLE dbo.WithoutIdentity( ID int NOT NULL); ALTER TABLE dbo.WithIdentity SWITCH TO dbo.WithoutIdentity;GO DROP TABLE dbo.WithIdentity;EXEC sys.sp_rename N'dbo.WithoutIdentity', N'dbo.WithIdentity', 'OBJECT';
Isso funciona porque os tipos de dados e a nulidade correspondem exatamente, e nenhuma atenção é dada àIDENTITY
atributo. Tente misturar tipos de dados, porém, e as coisas não funcionam tão bem:
CREATE TABLE dbo.SourceTable( ID int IDENTITY(1,1) NOT NULL); CREATE TABLE dbo.TrySwitch( ID bigint IDENTITY(1,1) NOT NULL); ALTER TABLE dbo.SourceTable SWITCH TO dbo.TrySwitch;
Isto resulta em:
Msg 4944, Nível 16, Estado 1 A instrução ALTER TABLE SWITCH falhou porque a coluna 'ID' tem o tipo de dados int na tabela de origem 'dbo.SourceTable' que é diferente de seu tipo bigint na tabela de destino 'dbo.TrySwitch'.
Seria fantástico se umSWITCH
operação poderia ser usada em um cenário como este, onde a única diferença no esquema na verdade não *exigisse* nenhuma alteração física para acomodar (novamente, como mostrei na parte 1, os dados são reescritos em novas páginas, embora não há necessidade de fazê-lo).
Conclusão
Esta postagem investigou duas possíveis soluções alternativas para ganhar tempo antes de alterar suaIDENTITY
existente coluna ou abandonandoIDENTITY
completamente agora em favor de umaSEQUENCE
. Se nenhuma dessas soluções alternativas for aceitável para você, observe a parte 4, na qual abordaremos esse problema de frente.
—
[ Parte 1 | Parte 2 | Parte 3 | Parte 4]