Na tentativa de desacoplar um aplicativo de nosso banco de dados monolítico, tentamos alterar as colunas INT IDENTITY de várias tabelas para serem uma coluna computada PERSISTED que usa COALESCE. Basicamente, precisamos que o aplicativo dissociado ainda tenha a capacidade de atualizar o banco de dados para dados comuns compartilhados em muitos aplicativos, enquanto ainda permite que aplicativos existentes criem dados nessas tabelas sem a necessidade de modificação de código ou procedimento.
Então, basicamente, passamos de uma definição de coluna de;
PkId INT IDENTITY(1,1) PRIMARY KEY
para;
PkId AS AS COALESCE(old_id, external_id, new_id) PERSISTED NOT NULL,
old_id INT NULL, -- Values here are from existing records of PkId before table change
external_id INT NULL,
new_id INT IDENTITY(2000000,1) NOT NULL
Em todos os casos, o PkId também é uma CHAVE PRIMÁRIA e, em todos, exceto um caso, é CLUSTERED. Todas as tabelas têm as mesmas chaves estrangeiras e índices de antes. Essencialmente, o novo formato permite que o PkId seja fornecido pelo aplicativo desacoplado (como external_id), mas também permite que o PkId seja o valor da coluna IDENTITY, permitindo, assim, o código existente que depende da coluna IDENTITY através do uso de SCOPE_IDENTITY e @@ IDENTITY para trabalhar como costumava.
O problema que tivemos é que encontramos algumas consultas que costumavam ser executadas em um tempo aceitável para explodir completamente agora. Os planos de consulta gerados usados por essas consultas não se parecem com o que eram antes.
Dada a nova coluna é uma PRIMARY KEY, o mesmo tipo de dados de antes, e PERSISTED, eu esperaria que as consultas e os planos de consulta se comportassem da mesma maneira que antes. O COMPUTED PERSISTED INT PkId se comporta essencialmente da mesma maneira que uma definição explícita de INT em termos de como o SQL Server produzirá o plano de execução? Existem outros problemas prováveis com essa abordagem que você pode ver?
O objetivo dessa mudança deveria nos permitir alterar a definição da tabela sem a necessidade de modificar procedimentos e códigos existentes. Dadas essas questões, não acho que possamos seguir essa abordagem.
fonte
Respostas:
PRIMEIRO
Você provavelmente não precisa de todas as três colunas:
old_id
,external_id
,new_id
. Anew_id
coluna, sendo umIDENTITY
, terá um novo valor gerado para cada linha, mesmo quando você inserirexternal_id
. Mas, entreold_id
eexternal_id
, esses são praticamente mutuamente exclusivos: ou já existe umold_id
valor ou essa coluna, na concepção atual, será apenas seNULL
estiver usandoexternal_id
ounew_id
. Como você não adicionará um novo ID "externo" a uma linha que já existe (ou seja, uma que tenha umold_id
valor), e não haverá novos valoresold_id
, então pode haver uma coluna usada para os dois propósitos.Portanto, livre-se da
external_id
coluna e renomeieold_id
para algo parecidoold_or_external_id
ou o que for. Isso não deve exigir nenhuma alteração real em nada, mas reduz algumas das complicações. No máximo, pode ser necessário chamar a colunaexternal_id
, mesmo que contenha valores "antigos", se o código do aplicativo já estiver gravado para inserçãoexternal_id
.Isso reduz a nova estrutura a ser justa:
Agora você adicionou apenas 8 bytes por linha em vez de 12 bytes (supondo que você não esteja usando a
SPARSE
opção ou Compactação de dados). E você não precisou alterar nenhum código, T-SQL ou código do aplicativo.SEGUNDO
Continuando nesse caminho de simplificação, vejamos o que nos resta:
old_or_external_id
coluna já possui valores ou receberá um novo valor do aplicativo ou será deixada comoNULL
.new_id
sempre terá um novo valor gerado, mas esse valor será usado apenas se aold_or_external_id
coluna forNULL
.Nunca há um momento em que você precisaria de valores em ambos
old_or_external_id
enew_id
. Sim, haverá momentos em que as duas colunas terão valores devido anew_id
serem umIDENTITY
, mas essesnew_id
valores serão ignorados. Novamente, esses dois campos são mutuamente exclusivos. E agora?Agora podemos analisar por que precisamos disso
external_id
em primeiro lugar. Considerando que é possível inserir em umaIDENTITY
coluna usandoSET IDENTITY_INSERT {table_name} ON;
, você pode evitar alterações no esquema e modificar apenas o código do aplicativo para agrupar asINSERT
instruções / operaçõesSET IDENTITY_INSERT {table_name} ON;
eSET IDENTITY_INSERT {table_name} OFF;
instruções. Você precisa determinar em qual intervalo inicial redefinir aIDENTITY
coluna (para valores recém-gerados), pois precisará estar bem acima dos valores que o código do aplicativo será inserido, pois a inserção de um valor mais alto fará com que o próximo valor gerado automaticamente seja ser maior que o valor MAX atual. Mas você sempre pode inserir um valor abaixo do valor IDENT_CURRENT .A combinação das colunas
old_or_external_id
enew_id
também não aumenta as chances de ocorrer uma sobreposição de valores entre valores gerados automaticamente e valores gerados por aplicativos, uma vez que a intenção de ter as colunas 2 ou 3 é combiná-las em um valor de Chave Primária, e esses são sempre valores únicos.Nesta abordagem, você só precisa:
Deixe as tabelas como estão:
Isso adiciona 0 bytes a cada linha, em vez de 8 ou até 12.
SET IDENTITY_INSERT {table_name} ON;
e asSET IDENTITY_INSERT {table_name} OFF;
instruções.SEGUNDA, parte B
Uma variação na abordagem observada diretamente acima seria fazer com que o código do aplicativo insira valores começando com -1 e diminuindo a partir daí. Isso deixa os
IDENTITY
valores como os únicos subindo . O benefício aqui é que você não apenas não complica o esquema, mas também não precisa se preocupar em encontrar IDs sobrepostos (se os valores gerados pelo aplicativo forem executados no novo intervalo gerado automaticamente). Essa é apenas uma opção se você ainda não estiver usando valores negativos de ID (e parece muito raro as pessoas usarem valores negativos em colunas geradas automaticamente, portanto, essa deve ser uma possibilidade provável na maioria das situações).Nesta abordagem, você só precisa:
Deixe as tabelas como estão:
Isso adiciona 0 bytes a cada linha, em vez de 8 ou até 12.
-1
.SET IDENTITY_INSERT {table_name} ON;
e asSET IDENTITY_INSERT {table_name} OFF;
instruções.Aqui você ainda precisa fazer o
IDENTITY_INSERT
, mas: você não adiciona nenhuma nova coluna, não precisa "reimplementar" nenhumaIDENTITY
coluna e não tem risco futuro de sobreposições.SEGUNDA, Parte 3
Uma última variação dessa abordagem seria possivelmente trocar as
IDENTITY
colunas e, em vez disso, usar Sequências . O motivo para adotar essa abordagem é poder fazer com que o código do aplicativo insira valores que sejam: positivos, acima do intervalo gerado automaticamente (não abaixo) e sem necessidadeSET IDENTITY_INSERT ON / OFF
.Nesta abordagem, você só precisa:
Copie a
IDENTITY
coluna para uma nova coluna que não possui aIDENTITY
propriedade, mas possui umaDEFAULT
restrição usando a função NEXT VALUE FOR :Isso adiciona 0 bytes a cada linha, em vez de 8 ou até 12.
SET IDENTITY_INSERT {table_name} ON;
e asSET IDENTITY_INSERT {table_name} OFF;
instruções.NO ENTANTO , devido ao requisito de que o código com
SCOPE_IDENTITY()
ou@@IDENTITY
ainda funcione corretamente, alternar para Sequências atualmente não é uma opção, pois parece que não há equivalente dessas funções para Sequências :-(. Triste!fonte
IDENTITY_INSERT
, mas ainda não o testei. Não tenho certeza se a opção 1 resolverá seu problema geral; foi apenas uma observação para reduzir a complexidade desnecessária. Ainda assim, se você tiver vários threads inserindo novos IDs "externos", como garantir que eles sejam exclusivos?IDENTITY_INSERT ON
para a mesma tabela em duas sessões e foi inserida em ambas sem problemas.