Alterando a chave primária de IDENTITY para persistente Coluna computada usando COALESCE

10

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.

Mr Moose
fonte
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Paul White 9

Respostas:

4

PRIMEIRO

Você provavelmente não precisa de todas as três colunas: old_id, external_id, new_id. A new_idcoluna, sendo um IDENTITY, terá um novo valor gerado para cada linha, mesmo quando você inserir external_id. Mas, entre old_ide external_id, esses são praticamente mutuamente exclusivos: ou já existe um old_idvalor ou essa coluna, na concepção atual, será apenas se NULLestiver usando external_idou new_id. Como você não adicionará um novo ID "externo" a uma linha que já existe (ou seja, uma que tenha um old_idvalor), e não haverá novos valores old_id, então pode haver uma coluna usada para os dois propósitos.

Portanto, livre-se da external_idcoluna e renomeie old_idpara algo parecido old_or_external_idou 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 coluna external_id, mesmo que contenha valores "antigos", se o código do aplicativo já estiver gravado para inserção external_id.

Isso reduz a nova estrutura a ser justa:

PkId AS AS COALESCE(old_or_external_id, new_id, -1) PERSISTED NOT NULL,
old_or_external_id INT NULL, -- values from existing record OR passed in from app
new_id INT IDENTITY(2000000, 1) NOT NULL

Agora você adicionou apenas 8 bytes por linha em vez de 12 bytes (supondo que você não esteja usando a SPARSEopçã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:

  • A old_or_external_idcoluna já possui valores ou receberá um novo valor do aplicativo ou será deixada como NULL.
  • O new_idsempre terá um novo valor gerado, mas esse valor será usado apenas se a old_or_external_idcoluna for NULL.

Nunca há um momento em que você precisaria de valores em ambos old_or_external_ide new_id. Sim, haverá momentos em que as duas colunas terão valores devido a new_idserem um IDENTITY, mas esses new_idvalores serão ignorados. Novamente, esses dois campos são mutuamente exclusivos. E agora?

Agora podemos analisar por que precisamos disso external_idem primeiro lugar. Considerando que é possível inserir em uma IDENTITYcoluna usando SET IDENTITY_INSERT {table_name} ON;, você pode evitar alterações no esquema e modificar apenas o código do aplicativo para agrupar as INSERTinstruções / operações SET IDENTITY_INSERT {table_name} ON;e SET IDENTITY_INSERT {table_name} OFF;instruções. Você precisa determinar em qual intervalo inicial redefinir a IDENTITYcoluna (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_ide new_idtambé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:

    PkId INT IDENTITY(1,1) PRIMARY KEY

    Isso adiciona 0 bytes a cada linha, em vez de 8 ou até 12.

  • Determine o intervalo inicial para valores gerados por aplicativos. Eles serão maiores que o valor MAX atual em cada tabela, mas menores que o que se tornará o valor mínimo para os valores gerados automaticamente.
  • Determine em qual valor o intervalo gerado automaticamente deve começar. Deve haver muito espaço entre o atual valor MAX e muito espaço para crescer, sabendo que no limite superior é pouco mais de 2,14 bilhões. Você pode definir esse novo valor mínimo de propagação via DBCC CHECKIDENT .
  • Coloque o código do aplicativo INSERTs SET IDENTITY_INSERT {table_name} ON;e as SET 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 IDENTITYvalores 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:

    PkId INT IDENTITY(1,1) PRIMARY KEY

    Isso adiciona 0 bytes a cada linha, em vez de 8 ou até 12.

  • O intervalo inicial para os valores gerados pelo aplicativo será -1.
  • Coloque o código do aplicativo INSERTs SET IDENTITY_INSERT {table_name} ON;e as SET 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" nenhuma IDENTITYcoluna e não tem risco futuro de sobreposições.

SEGUNDA, Parte 3

Uma última variação dessa abordagem seria possivelmente trocar as IDENTITYcolunas 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 necessidade SET IDENTITY_INSERT ON / OFF.

Nesta abordagem, você só precisa:

  • Crie sequências usando CREATE SEQUENCE
  • Copie a IDENTITYcoluna para uma nova coluna que não possui a IDENTITYpropriedade, mas possui uma DEFAULTrestrição usando a função NEXT VALUE FOR :

    PkId INT PRIMARY KEY CONSTRAINT [DF_TableName_NextID] DEFAULT (NEXT VALUE FOR...)

    Isso adiciona 0 bytes a cada linha, em vez de 8 ou até 12.

  • O intervalo inicial para valores gerados por aplicativos estará bem acima do que você acha que os valores gerados automaticamente se aproximarão.
  • Coloque o código do aplicativo INSERTs SET IDENTITY_INSERT {table_name} ON;e as SET IDENTITY_INSERT {table_name} OFF;instruções.

NO ENTANTO , devido ao requisito de que o código com SCOPE_IDENTITY()ou @@IDENTITYainda 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!

Solomon Rutzky
fonte
Muito obrigado pela sua resposta. Você levanta alguns pontos que foram discutidos aqui internamente. Infelizmente, alguns deles não funcionam para nós por alguns motivos. Nosso banco de dados é bastante antigo e um tanto quebradiço e é executado no modo de compatibilidade de 2005, portanto, SEQUENCES estão fora. Nosso envio de dados do aplicativo ocorre por meio de uma ferramenta de carregamento de dados que obtém novos registros das filas do service broker e os envia por vários encadeamentos. IDENTITY_INSERT pode ser usado apenas para uma tabela por sessão, e o pensamento atual é que nossa arquitetura não pode atender a isso sem alterações significativas. Estou testando sua sugestão de punho agora.
Moose
@MrMoose Sim, atualizei minha resposta para incluir mais informações sobre Sequências no final. De qualquer maneira, não funcionaria na sua situação. E eu estava pensando sobre possíveis problemas de simultaneidade com 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?
Solomon Rutzky
@MrMoose Na verdade, em relação a " IDENTITY_INSERT pode ser usado apenas para uma tabela por sessão ", qual é exatamente o problema aqui? 1) você pode inserir apenas uma tabela por vez, desativando-a na Tabela A antes de inseri-la na Tabela B e 2) Acabei de testar e, ao contrário do que eu pensava, não há problemas de simultaneidade - consegui tem IDENTITY_INSERT ONpara a mesma tabela em duas sessões e foi inserida em ambas sem problemas.
Solomon Rutzky
11
Como você sugeriu, a alteração 1 fez pouca diferença. O ID que usaremos será alocado fora do banco de dados atual e usado para relacionar registros. Pode ser que meu entendimento das sessões não esteja certo, portanto IDENTITY_INSERT pode funcionar. Vai demorar um pouco para eu investigar isso, então não poderei responder por um tempo. Mais uma vez obrigado pela contribuição. Isso é muito apreciado.
Moose
11
Acho que sua sugestão de usar IDENTITY_INSERT (com um alto valor inicial para aplicativos existentes) funcionará bem. Aaron Bertrand forneceu uma resposta aqui com um bom exemplo de como testá-lo com simultaneidade. Modificamos nossa ferramenta de carregamento de dados para lidar com tabelas que precisam especificar valores de identidade e faremos outros testes nas próximas semanas.
Moose