Mudanças de esquema sem indisponibilidade com o padrão expandir e contrair
Planeje e publique mudanças de esquema sem indisponibilidade com o padrão expandir e contrair, preenchimentos seguros, versões compatíveis, verificação e reversão.

Por que mudanças de esquema causam indisponibilidade
Mudanças de esquema causam indisponibilidade quando versões do aplicativo, workers em segundo plano e o banco de dados deixam de concordar sobre quais estruturas e valores são válidos. A falha pode ser evidente, como todas as requisições retornando erro, ou gradual, como aumento da latência das consultas, falhas de gravação, atraso de réplicas e uma fila de jobs que precisam ser reprocessados.
Uma implantação em produção raramente altera todos os processos de uma vez. Lançamentos graduais mantêm instâncias antigas e novas do aplicativo em execução ao mesmo tempo. Workers de longa duração podem continuar com uma versão antiga por horas, clientes móveis podem permanecer ativos por meses, e jobs de relatórios ou integração podem usar tabelas sem passar pelo aplicativo principal. Todos compartilham um único banco de dados.
Falhas comuns incluem:
- O novo código grava em uma coluna antes de a migração que a cria terminar.
- O código antigo lê uma tabela ou coluna que uma versão posterior renomeou ou eliminou.
- Uma reescrita de tabela, um preenchimento de dados ou a criação de um índice consome I/O e CPU suficientes para deixar o tráfego normal mais lento.
- Um comando de esquema espera por um bloqueio enquanto as requisições se acumulam atrás dele.
- Uma nova restrição rejeita gravações de um processo que ainda não foi atualizado.
O risco muitas vezes está na obtenção do bloqueio, não no tempo nominal de execução. Um ALTER TABLE rápido pode esperar por uma transação longa. Enquanto espera, consultas posteriores podem ficar enfileiradas atrás do bloqueio de esquema pendente, transformando uma migração pequena em uma paralisação de todo o aplicativo.
Não ter indisponibilidade exige que cada estado intermediário do banco permaneça utilizável por toda versão do aplicativo que ainda possa estar em execução. Adicione primeiro estruturas compatíveis, mova tráfego e dados em etapas controladas e remova o caminho antigo apenas quando seu último consumidor tiver desaparecido.
Esse trabalho se justifica em sistemas com tráfego ativo, implantações graduais, metas rígidas de disponibilidade ou procedimentos de recuperação caros. Uma pequena ferramenta interna com um banco pouco usado pode se beneficiar mais de uma janela de manutenção testada. A decisão deve refletir o custo de uma indisponibilidade e a complexidade operacional da migração.
Expandir e contrair em palavras simples
O padrão expandir e contrair transforma uma mudança incompatível em uma sequência de lançamentos compatíveis. O banco passa a suportar temporariamente duas representações enquanto código e dados migram da antiga para a nova.
A sequência tem três partes:
- Expanda adicionando colunas, tabelas, índices ou restrições sem remover nada de que o código atual precise.
- Faça a transição publicando código compatível, movendo dados históricos e direcionando leituras e gravações para a nova representação.
- Contraia eliminando o código antigo e os objetos do banco depois que a verificação provar que eles não são mais usados.
Suponha que uma tabela PostgreSQL armazene o nome de uma pessoa em full_name, e o aplicativo passe a precisar dos campos separados first_name e last_name. A expansão adiciona colunas anuláveis e preserva full_name. Uma versão compatível grava as representações necessárias durante a transição. Um preenchimento separa os valores existentes, com uma política explícita para nomes que não podem ser divididos com segurança. As leituras só mudam depois que os novos campos estiverem suficientemente preenchidos. A contração remove full_name mais tarde.
Essa ordem funciona com implantações graduais porque a versão antiga ainda encontra full_name, e a nova encontra as três colunas. Ela também preserva um caminho para reverter o aplicativo. Se a nova versão se comportar mal, a versão anterior pode executar porque suas dependências de esquema não foram removidas.
Reverter o banco não é o mesmo que reverter o aplicativo. Desfazer uma migração depois que os dados foram transformados pode descartar informações ou restaurar valores desatualizados. Durante a transição, prefira devolver o tráfego do aplicativo à representação conhecida e manter os objetos aditivos do banco. Corrija a migração adiante depois que o incidente estiver estável.
O padrão não significa que toda mudança exige código de gravação dupla. Adicionar uma coluna opcional usada apenas por código novo pode exigir uma migração aditiva e uma implantação. Renomeações, mudanças de representação, divisão de tabelas e alterações em campos obrigatórios normalmente exigem mais fases porque duas versões do aplicativo não conseguem compartilhar o esquema com segurança de outra forma.
Classifique a mudança antes de escolher as etapas
O plano de migração deve refletir os riscos reais de bloqueio, reescrita, compatibilidade e conversão de dados da operação. Tratar todo ALTER TABLE como equivalente produz burocracia desnecessária ou um lançamento inseguro.
Mudanças aditivas costumam ser as mais simples. Uma coluna anulável, uma tabela separada ou um índice criado com um método online muitas vezes podem ser introduzidos antes de o código do aplicativo usá-los. O comando ainda precisa de um bloqueio, então teste seu comportamento em uma tabela e uma carga de transações semelhantes às de produção.
Mudanças destrutivas incluem eliminar ou renomear colunas, restringir tipos, substituir tabelas e adicionar restrições mais rígidas. Elas invalidam uma suposição feita pelo código existente. Coloque-as na fase de contração, depois que referências no código e consumidores externos forem removidos.
Operações que alteram dados merecem avaliação própria. Converter carimbos de data e hora, normalizar números de telefone, combinar registros ou dividir texto livre pode perder informações. Defina como valores inválidos e ambíguos serão tratados antes de iniciar o preenchimento. Se uma transformação não puder ser revertida, preserve a origem até que o resultado passe por verificações de negócio.
Uma revisão útil antes da execução responde a cinco perguntas:
- Qual bloqueio cada instrução solicita e por quanto tempo ela pode esperar ou manter esse bloqueio?
- A operação vai reescrever a tabela, gerar muito WAL ou aumentar o atraso das réplicas?
- Quais aplicativos, jobs, relatórios e consumidores de captura de alterações de dados usam os objetos afetados?
- As versões atual e proposta podem executar em cada estado de transição?
- Qual sinal pausa a operação e qual estado exato permanece depois que ela para?
Execute a migração exata em dados com volume e distribuição realistas. Uma tabela de teste com mil linhas organizadas diz pouco sobre uma tabela de produção com centenas de milhões de linhas, tuplas largas, linhas mortas, valores desbalanceados e transações de longa duração.
Expanda com segurança no PostgreSQL
Uma expansão segura no PostgreSQL usa mudanças curtas de metadados, esperas de bloqueio limitadas e operações online separadas quando o banco exige isso. Adicione a nova estrutura antes de publicar código que dependa dela.
Adicionar uma coluna anulável sem valor padrão costuma ser uma operação curta de metadados:
BEGIN;
SET LOCAL lock_timeout = '2s';
SET LOCAL statement_timeout = '30s';
ALTER TABLE customers
ADD COLUMN phone_e164 text;
COMMIT;
O tempo limite impede que a versão espere indefinidamente atrás de uma transação aberta. Se o bloqueio não puder ser obtido rapidamente, deixe a migração falhar, investigue o bloqueador e tente novamente em um momento mais seguro. Não tente de novo automaticamente em um ciclo apertado, pois pedidos repetidos de bloqueio podem continuar atrapalhando o tráfego de produção.
Versões modernas do PostgreSQL podem adicionar uma coluna com valor padrão constante sem gravar esse valor imediatamente em todas as linhas existentes. Essa otimização não torna todo valor padrão inofensivo. Uma expressão volátil pode exigir reescrita, e ALTER TABLE ainda precisa de um bloqueio breve ACCESS EXCLUSIVE. Confirme o comportamento da versão PostgreSQL implantada e da expressão exata, em vez de se apoiar em uma regra geral.
Um CREATE INDEX normal pode bloquear gravações. Use a criação concorrente quando a tabela precisar permanecer gravável:
CREATE INDEX CONCURRENTLY idx_customers_phone_e164
ON customers (phone_e164);
CREATE INDEX CONCURRENTLY não pode ser executado dentro de um bloco de transação. Demora mais, faz trabalho adicional e pode esperar por transações antigas, mas inserções, atualizações e exclusões normais podem continuar. Ainda consome CPU, I/O e WAL, então monitore a latência do banco e as réplicas durante a execução.
Uma criação concorrente que falha pode deixar um índice inválido. Inspecione o estado do índice antes de tentar novamente e então remova ou recrie deliberadamente o objeto inválido. Ferramentas de migração que envolvem todo arquivo em uma transação precisam de um modo não transacional compatível para operações de índice concorrentes.
Novas tabelas costumam ser mais fáceis de introduzir que transformações no local. Para um relacionamento de um para muitos ou de muitos para muitos, adicione a tabela de destino e seus índices enquanto mantém a coluna de origem. Adie a exclusão da origem até que novas gravações, dados históricos, leituras e consumidores posteriores tenham migrado.
Mudanças de tipo exigem cuidado extra. Algumas alteram apenas metadados, enquanto outras reescrevem cada linha ou obtêm um bloqueio restritivo por tempo demais. Para uma conversão arriscada, adicione uma coluna com o tipo de destino, preencha-a em lotes, altere o acesso do aplicativo e elimine a original mais tarde. Isso também dá à equipe um lugar para registrar falhas de conversão em vez de fazer um grande ALTER COLUMN TYPE ter sucesso ou falhar como uma unidade.
Publique código que continua compatível
Código de aplicativo compatível aceita valores transitórios ausentes e nunca exige uma migração destrutiva na mesma publicação. A expansão do banco deve terminar antes que a primeira instância do aplicativo comece a usar o novo objeto.
A gravação dupla é útil quando as duas representações precisam permanecer atualizadas. Sempre que possível, faça as duas gravações na mesma transação do banco. Uma segunda gravação assíncrona pode falhar depois que a primeira tiver sucesso, criando divergências que leituras futuras podem expor.
A lógica de gravação dupla também precisa de uma única fonte de verdade. Se phone_e164 deriva de phone, defina qual entrada prevalece quando ambas são fornecidas e aplique a mesma normalização em manipuladores de API, workers, importações e ferramentas administrativas. Caso contrário, dois caminhos de código aparentemente corretos podem armazenar resultados diferentes.
As leituras devem migrar depois das gravações. Mantenha as leituras no campo já estabelecido enquanto as novas gravações preenchem ambas as formas e o preenchimento trata as linhas históricas. Depois da verificação, publique um caminho de leitura que prefira o novo campo e use o valor antigo apenas segundo uma regra de fallback definida. Meça o uso do fallback. Um fallback silencioso pode ocultar dados incompletos para sempre.
Uma sequência típica de versões é:
- A versão 1 adiciona os novos objetos do banco sem alterar o comportamento do aplicativo.
- A versão 2 grava as representações transitórias e mantém as leituras estabelecidas.
- A versão 3 altera as leituras depois que o preenchimento e as verificações de consistência passam.
- A versão 4 deixa de manter a representação antiga depois que expirarem os critérios de reversão.
- A versão 5 remove referências ao código antigo, seguida mais tarde pela limpeza do banco.
Mantenha contratos de API pública separados das mudanças físicas de esquema. Uma coluna de banco renomeada não exige uma renomeação imediata de campo nas respostas web, móveis ou de integração. Altere esses contratos por uma política de compatibilidade própria, principalmente quando os clientes não podem ser atualizados junto com o servidor.
Faça um inventário de todos os gravadores. Manipuladores HTTP são apenas uma origem de mudanças. Consumidores de filas, jobs agendados, scripts de importação, ferramentas de reparo de dados, gatilhos de banco e operações administrativas diretas podem continuar produzindo linhas no formato antigo. Quando for viável, identifique as conexões de banco com o nome do aplicativo e registre o uso de caminhos transitórios para que processos esquecidos se tornem visíveis.
Processos de longa duração podem preservar suposições desatualizadas por meio de instruções preparadas, metadados em cache ou uma camada de mapeamento objeto-relacional. Teste reinicializações graduais e o comportamento do pool de conexões antes da contração. Um processo que não recebeu tráfego recentemente ainda pode falhar na primeira vez que um job raro for executado.
Preencha dados sem sobrecarregar o banco
Um preenchimento seguro atualiza lotes pequenos e retomáveis, diminuindo o ritmo quando a saúde da produção piora. Ele começa apenas depois que os gravadores ativos conseguem manter a nova representação.
Escolha os lotes pelo tempo decorrido e pelo impacto no banco, não por uma quantidade universal de linhas. Mil linhas estreitas podem terminar em milissegundos, enquanto mil linhas com valores grandes ou transformações caras podem gerar I/O significativo. Comece com cautela e busque transações que terminem em segundos. Confirme entre os lotes para que bloqueios e versões antigas de linhas não se acumulem em uma única transação.
O PostgreSQL não aceita ORDER BY e LIMIT diretamente em um UPDATE simples. Selecione um lote em uma expressão de tabela comum e atualize essas linhas:
WITH batch AS (
SELECT id
FROM my_table
WHERE id > $1
AND new_col IS NULL
ORDER BY id
LIMIT 1000
)
UPDATE my_table AS target
SET new_col = transform_expression(target.old_col)
FROM batch
WHERE target.id = batch.id
AND target.new_col IS NULL
RETURNING target.id;
O aplicativo registra o maior id concluído como cursor. A atualização condicional torna novas execuções idempotentes, portanto uma falha após a confirmação não corrompe linhas já processadas. Armazene o progresso com cuidado suficiente para que o cursor não avance além de um lote não confirmado.
Um cursor crescente de id evita varrer repetidamente o início da tabela, mas não encontra correções tardias nem linhas inseridas abaixo do cursor. Termine com uma passada de atualização sobre todos os valores NULL restantes. Se os identificadores não estiverem ordenados ou as linhas puderem mudar de estado de elegibilidade, use uma tabela de trabalho ou outro ponto de controle explícito em vez de presumir que uma única varredura adiante está completa.
Vários workers podem reservar linhas com FOR UPDATE SKIP LOCKED, mas o paralelismo aumenta a pressão de gravação e complica o acompanhamento do progresso. Não combine linhas ignoradas com um cursor que avança permanentemente além delas. Uma fila de identificadores reservados ou uma varredura repetida de elegibilidade é mais segura para workers paralelos.
Reduza o ritmo com base em medições de produção, como latência de consultas, conexões ativas, esperas por bloqueio, geração de WAL, atraso de reprodução das réplicas e crescimento de linhas mortas. Pause ao ultrapassar um limite e retome a partir do ponto de controle. Esperas fixas são simples, mas o retorno do banco responde melhor às mudanças de tráfego.
Evite alterar todas as linhas quando apenas algumas precisam de trabalho. Filtre pelo novo campo, pelo estado de origem ou por um marcador de migração. Se a transformação for cara, calcule-a fora da transação de atualização quando a consistência permitir, depois faça uma gravação condicional curta. Mantenha uma contagem e uma amostra dos valores rejeitados em vez de inventar dados silenciosamente.
O autovacuum e as réplicas precisam absorver o trabalho depois de cada atualização. Um preenchimento pode terminar com sucesso no primário enquanto as réplicas ficam muito atrasadas ou a expansão da tabela piora consultas futuras. Os limites de ritmo devem considerar esse custo tardio, não apenas o tempo imediato de execução do lote.
Verifique os dados e o tráfego de produção
Uma migração só está pronta para a contração quando verificações de dados, telemetria do aplicativo e evidências de dependência concordam que o novo caminho é a fonte de verdade. Apenas um contador de jobs concluídos não prova que o resultado está correto.
Comece pela completude e consistência. O IS DISTINCT FROM do PostgreSQL compara valores tratando NULL explicitamente, diferente de <>, que produz um resultado desconhecido quando um dos lados é NULL:
SELECT count(*)
FROM customers
WHERE normalize_phone(phone) IS DISTINCT FROM phone_e164;
Não execute repetidamente uma contagem de tabela inteira sem índice em uma tabela muito grande e ocupada. Use uma validação controlada única, intervalos limitados de identificadores, amostras ou um processo temporário de verificação que avance pela tabela. O método certo depende do custo de errar e da capacidade disponível no banco.
A verificação deve cobrir:
- Nenhum valor ausente inesperado permanece nas linhas que exigem o novo campo.
- O novo valor corresponde à transformação acordada, incluindo entradas malformadas e vazias.
- Linhas e atualizações recentes permanecem consistentes depois que a passada histórica termina.
- O uso de fallback de leitura atingiu o limite planejado, geralmente zero para tráfego controlado pelo servidor.
- Taxas de erro, latência de consulta, bloqueios e atraso de réplica permanecem dentro dos limites da versão.
Compare resultados de negócio, além das colunas. Se uma migração altera preços, permissões, estado da conta ou identificadores, valide totais e invariantes dos quais os usuários dependem. Duas colunas podem corresponder mecanicamente e ainda codificar a regra de negócio errada.
Observe um ciclo operacional completo antes da limpeza. O intervalo correto se baseia no comportamento real do sistema, não em uma regra fixa de uma semana. Talvez precise incluir o processamento de fim de mês, um job de cobrança pouco frequente, novas tentativas atrasadas de fila ou a vida útil máxima de um cliente móvel antigo. Registre a evidência de que cada consumidor migrou.
Faça um canário da mudança de leitura quando a arquitetura do aplicativo permitir. Envie uma pequena parte do tráfego para o novo caminho de leitura, compare resultados e aumente gradualmente. Mantenha a ação de reversão simples: redirecione leituras para a representação estabelecida sem desfazer o preenchimento.
Adicione restrições depois que os dados estiverem prontos
As restrições só devem ficar rígidas depois que todos os gravadores obedecerem às regras e os dados existentes forem validados. Aplicar NOT NULL, uma verificação ou uma chave estrangeira durante a expansão pode bloquear tráfego ou rejeitar gravações de um processo antigo.
O PostgreSQL pode adicionar uma restrição CHECK como NOT VALID, que aplica a regra a linhas novas ou alteradas sem varrer imediatamente todas as linhas históricas. Valide-a separadamente depois do preenchimento:
ALTER TABLE customers
ADD CONSTRAINT customers_phone_e164_present
CHECK (phone_e164 IS NOT NULL) NOT VALID;
ALTER TABLE customers
VALIDATE CONSTRAINT customers_phone_e164_present;
Depois que a validação tiver sucesso, versões compatíveis do PostgreSQL podem usar essa prova ao definir a coluna como NOT NULL, evitando outra varredura completa da tabela. A alteração final ainda precisa de um bloqueio forte na tabela, portanto use um tempo limite de bloqueio e um plano de nova tentativa:
ALTER TABLE customers
ALTER COLUMN phone_e164 SET NOT NULL;
ALTER TABLE customers
DROP CONSTRAINT customers_phone_e164_present;
A verificação temporária pode permanecer se tiver valor, mas manter restrições equivalentes acrescenta poluição ao catálogo sem mudar a regra.
Chaves estrangeiras podem seguir uma sequência parecida com NOT VALID e VALIDATE CONSTRAINT. Novas gravações são verificadas depois que a restrição é criada, enquanto a validação histórica ocorre mais tarde. Adicione intencionalmente o índice de suporte quando o comportamento de exclusão ou atualização no relacionamento referenciado puder causar varreduras caras.
A validação no aplicativo deve vir antes da imposição no banco, mas não a substitui. O código produz mensagens de erro mais claras para o usuário, enquanto o banco protege dados gravados por todos os caminhos. Durante a publicação, acompanhe violações de restrição para identificar um gravador que a auditoria de dependências não encontrou.
Contraia o caminho antigo com segurança
A fase de contração deve remover dependências do aplicativo antes de remover objetos do banco. Quando a telemetria e a verificação estabelecerem que o novo caminho é a fonte de verdade, a limpeza pode seguir em versões separadas.
Primeiro, pare de ler o campo antigo e remova a lógica de fallback. Em seguida, desative suas gravações e observe a produção pelo tempo suficiente para encontrar caminhos raros. Remova flags de funcionalidade, gatilhos, views de compatibilidade, scripts de reparo e jobs agendados que mencionem a representação antiga. Pesquise o código-fonte exportado e o código de migração, mas também examine relatórios, consultas de integração e configurações de captura de alterações de dados fora do repositório principal.
Uma ordem segura de limpeza é:
- Remova leituras de fallback e confirme que elas não aparecem mais na telemetria.
- Interrompa as gravações antigas e elimine o código de sincronização.
- Remova referências do aplicativo de todas as versões que podem ser publicadas.
- Elimine índices e restrições obsoletos com o método online adequado.
- Elimine a coluna ou tabela antiga em uma versão posterior do banco.
Eliminar uma coluna PostgreSQL é principalmente uma mudança no catálogo, mas ainda exige um bloqueio ACCESS EXCLUSIVE. Por isso, uma instrução curta pode esperar uma transação longa e bloquear o trabalho posterior. Aplique um tempo limite de bloqueio, inspecione antes as transações de longa duração e agende a tentativa para um período de menor risco.
Use DROP INDEX CONCURRENTLY para um índice obsoleto quando bloquear gravações for inaceitável. Assim como a criação concorrente, ele não pode ser executado dentro de um bloco de transação e tem restrições que a ferramenta de migração precisa tratar.
Não combine a limpeza do código e a exclusão física em uma única versão. A separação permite que o aplicativo limpo execute em um banco que ainda contém o objeto não usado. Se surgir um problema no aplicativo, a reversão continua possível sem recriar o esquema ou reconstruir dados.
Antes de eliminar uma tabela, verifique a propriedade de sequências, views, funções, permissões, gatilhos, publicações de replicação e consultas externas. Evite usar CASCADE como atalho em uma migração de produção, pois ele pode remover dependências que não faziam parte da mudança pretendida.
Lide com reversões e etapas com falha
O planejamento de reversão deve definir uma ação segura para cada fase, em vez de depender de uma única migração genérica de retorno. Objetos aditivos, movimentação de dados, mudanças de leitura e exclusão têm propriedades de recuperação diferentes.
Se a expansão não conseguir obter o bloqueio, deixe o aplicativo inalterado e tente novamente depois de resolver a transação bloqueadora. Se a criação de índice concorrente falhar, verifique se deixou um índice inválido e limpe esse objeto específico antes de outra tentativa.
Se um preenchimento gerar carga, pause-o. Lotes já confirmados e idempotentes podem permanecer. Reduza o tamanho ou a taxa dos lotes, trate a transformação cara e retome a partir do ponto de controle. Reverter milhões de atualizações corretas normalmente acrescenta risco sem ajudar a produção a se recuperar.
Se um novo caminho de leitura retornar resultados incorretos, redirecione as leituras à representação antiga e mantenha os novos dados para diagnóstico. Continue com gravações duplas apenas se elas forem comprovadamente corretas. Quando o próprio gravador tiver falhas, desative-o ou reverta o aplicativo antes de reparar as linhas afetadas.
Depois da contração, a recuperação pode exigir restauração de dados, e não apenas publicar uma versão mais antiga. Defina explicitamente o ponto sem retorno. Faça o backup ou snapshot exigido pela política de recuperação do sistema, teste a restauração antes da versão e mantenha o objeto antigo pelo período de retenção acordado quando o custo de armazenamento permitir.
Comandos de esquema podem ser transacionais, mas efeitos externos nem sempre estão cobertos. Operações de índice concorrentes, mensagens de fila, mudanças de cache e implantações de aplicativo não compartilham uma transação atômica. O runbook deve descrever o estado observável após cada falha parcial e o comando que continua com segurança a partir dele.
Evite armadilhas comuns de migração
A maioria das migrações sem indisponibilidade que falham aplica o novo estado cedo demais ou esquece um consumidor do estado antigo. As armadilhas abaixo merecem revisão explícita antes da aprovação.
- Adicionar
NOT NULLenquanto uma instância antiga do aplicativo ainda pode omitir o campo. - Executar um grande preenchimento em uma transação, mantendo bloqueios e versões de linhas por tempo demais.
- Renomear uma coluna como se fosse uma mudança aditiva, embora o código antigo ainda use seu nome original.
- Alterar leituras antes que todos os caminhos de gravação e linhas históricas preencham a nova representação.
- Tratar uma implantação bem-sucedida como prova de que relatórios, workers, réplicas e integrações são compatíveis.
Outra falha sutil vem da sincronização bidirecional. Um gatilho copia old_col para new_col, enquanto o código do aplicativo copia new_col de volta para old_col. Diferenças de normalização ou ordem de gatilhos podem criar ciclos, sobrescrever valores intencionais ou tornar a responsabilidade pouco clara. Prefira uma única direção e documente qual representação é a fonte de verdade em cada versão.
Valores padrão podem ocultar atualizações ausentes nos gravadores. Se uma nova coluna obrigatória receber um valor padrão vazio ou genérico, o código antigo parecerá compatível enquanto armazena dados semanticamente inválidos. Use uma transição anulável quando a ausência trouxer informação útil para diagnóstico e aplique a regra real depois que cada gravador fornecer um valor significativo.
Uma flag de funcionalidade não torna um comando de esquema incompatível seguro por si só. Um caminho de código desativado ainda pode ser carregado, preparado ou executado por um processo antigo. O objeto do banco deve permanecer até que nenhuma versão ativa ou que possa ser publicada faça referência a ele.
A responsabilidade pela migração também importa. Atribua uma pessoa ou equipe à transição até a contração, incluindo as datas de verificação e remoção. Caso contrário, colunas temporárias, flags e jobs de sincronização podem permanecer por meses, aumentando o custo de cada mudança posterior.
Substitua uma coluna de telefone sem indisponibilidade
Substituir customers.phone por customers.phone_e164 normalizado exige uma coluna aditiva, uma política de conversão definida, código compatível, preenchimento limitado, mudança de leitura e limpeza adiada. A política de conversão deve vir antes do SQL porque nem todo valor armazenado pode ser normalizado automaticamente.
Comece classificando os valores existentes. Números válidos podem ser convertidos quando o contexto de país necessário é conhecido. Valores em branco podem se tornar NULL. Números ambíguos ou malformados devem entrar em um relatório de exceções, em vez de serem adivinhados. Decida se o produto exige que todo cliente tenha número de telefone, pois isso determina se NOT NULL será apropriado mais tarde.
Adicione a coluna com um tempo limite de bloqueio curto:
BEGIN;
SET LOCAL lock_timeout = '2s';
ALTER TABLE customers
ADD COLUMN phone_e164 text;
COMMIT;
Publique código que normalize a nova entrada e grave phone e phone_e164 em uma transação. Inicialmente, mantenha as leituras em phone. Atualize todos os gravadores, incluindo importações de contas, ferramentas de suporte, jobs de worker e testes que criam dados de clientes.
Preencha linhas elegíveis em transações curtas. Registre o último identificador processado, a quantidade convertida, a quantidade ignorada e o motivo de cada categoria de falha. Limite a taxa do job com base na latência de produção e no atraso das réplicas. Quando a passada adiante terminar, examine novamente os valores NULL elegíveis para encontrar inserções simultâneas ou linhas perdidas após uma reinicialização.
Execute verificações de consistência usando as mesmas regras de normalização do aplicativo e então faça amostras manuais de prefixos internacionais, ramais, valores em branco, registros de contato duplicados e dados antigos importados. Uma contagem de linhas prova cobertura, não um número de telefone correto.
Publique um caminho de leitura que retorne phone_e164 quando estiver presente e use phone apenas em uma exceção registrada. Monitore o uso de fallback e erros de normalização. Resolva as exceções restantes em vez de deixar o fallback se tornar comportamento permanente.
Quando o novo campo for a fonte de verdade, remova o fallback e pare de gravar phone. Observe jobs raros e tráfego de integração durante um ciclo operacional adequado. Adicione a restrição validada apenas se a regra do produto exigir.
Por fim, remova referências de código a phone. Elimine separadamente seus índices ou restrições e depois elimine a coluna em uma migração posterior, com espera de bloqueio limitada. Se a mudança de leitura falhar em qualquer momento antes dessa eliminação, reverta o comportamento do aplicativo enquanto ambas as colunas continuarem disponíveis.
Este exemplo também expõe uma questão de domínio que a mecânica do esquema não resolve: dividir ou normalizar dados inseridos por pessoas nem sempre preserva toda a informação. O plano de migração precisa preservar exceções e dar a alguém uma forma de resolvê-las.
Verifique cada versão antes de publicá-la
Uma lista de verificação da versão deve provar compatibilidade, limitar o impacto na produção e nomear a ação de recuperação da fase atual. Mantenha as evidências junto com a mudança para que um operador não tenha de reconstruir a intenção durante um incidente.
Antes da implantação, confirme:
- A versão do aplicativo funciona com o estado do banco antes e depois desta publicação.
- Tempos limites de bloqueio e de instrução estão definidos para comandos de esquema que possam esperar pelo tráfego.
- O job de preenchimento ou validação tem controles de progresso, pausa, retomada e limite de taxa.
- Os painéis cobrem erros, latência, bloqueios, carga do banco, WAL e atraso das réplicas.
- A ação de reversão foi testada sem depender de um objeto já removido.
Registre condições explícitas de conclusão. Exemplos incluem zero novas falhas de consistência por um ciclo completo de job, zero leituras de fallback do tráfego controlado pelo servidor, todos os consumidores conhecidos atualizados e uma consulta de validação controlada bem-sucedida. A porcentagem concluída ajuda durante um preenchimento, mas 100% processado não é o mesmo que 100% correto.
Revise a ordem da migração separadamente da revisão de código. Uma coleção correta de SQL e mudanças no aplicativo ainda pode falhar se a implantação as executar na sequência errada. Indique qual etapa só pode começar depois que outra terminar.
As condições de parada devem ser numéricas quando possível. Defina latência de consulta, espera por bloqueio, atraso de réplica, taxa de erro e duração de lote aceitáveis. Quando um limite for ultrapassado, o operador deve saber se precisa pausar um job, cancelar uma instrução em espera ou redirecionar leituras sem buscar nova aprovação durante o incidente.
A migração só termina depois que a nova representação atende leituras e gravações, os dados históricos passam pela verificação, o objeto antigo é removido e a infraestrutura operacional temporária desaparece.
Torne o processo repetível
Um runbook de migração reutilizável transforma expandir e contrair em trabalho comum de publicação, com responsáveis definidos e etapas mensuráveis. Ele deve ser curto o bastante para ser seguido em uma implantação ativa e específico o suficiente para descrever estados de falha parcial.
Use cinco seções no runbook:
- Expansão: operações exatas de esquema, bloqueios esperados, tempos limites e requisitos de transação.
- Compatibilidade: código afetado, gravadores, leitores, flags, clientes e ordem de implantação.
- Preenchimento: política de transformação, lotes, pontos de controle, limitação de ritmo e tratamento de exceções.
- Verificação: checagens SQL, invariantes de negócio, telemetria e limites de conclusão.
- Contração: remoção de dependências, período de observação, limpeza física e limites de recuperação.
Atribua um responsável e uma data prevista de conclusão a cada objeto transitório. Acompanhe colunas, índices, flags, gatilhos e jobs no mesmo lugar. A limpeza faz parte da migração, não é manutenção opcional.
Para equipes que desenvolvem com Koder.ai, o Modo de Planejamento pode detalhar essas fases e pontos de controle antes do início de mudanças em produção. A exportação do código-fonte também permite que o SQL de migração e a lógica de compatibilidade recebam a mesma revisão que o restante do código do aplicativo. O Koder.ai oferece suporte a implantação, hospedagem, snapshots e reversão, mas não se deve presumir que uma reversão do aplicativo desfaça uma transformação de dados já confirmada. Preserve a compatibilidade de esquema até que o plano de recuperação do banco não dependa mais da representação antiga.
Agende trabalho com muitas gravações para períodos de menor tráfego quando possível, mas não use o horário como único controle de segurança. Transações limitadas, limitação baseada em feedback, progresso observável e uma ação de pausa testada são o que mantêm uma migração online administrável quando o tráfego ou os dados se comportam de forma diferente do esperado.
Perguntas frequentes
Por que uma mudança de esquema pode causar indisponibilidade?
Mudanças de esquema interrompem a produção quando versões antigas e novas do aplicativo esperam estruturas de banco de dados diferentes. Durante uma implantação gradual, as duas versões podem funcionar ao mesmo tempo, portanto remover ou renomear uma coluna cedo demais pode causar falhas de leitura ou gravação.
O que é o padrão de migração expandir e contrair?
Expandir e contrair divide uma mudança incompatível em etapas seguras. Primeiro, adicione a nova estrutura; depois, mova o código e os dados para ela; por fim, remova a estrutura antiga quando nenhum consumidor mais a utilizar.
Como renomear ou substituir uma coluna de banco de dados sem indisponibilidade?
Adicione primeiro a nova coluna e mantenha a antiga. Publique um código que funcione com os dois campos, preencha as linhas existentes em lotes pequenos, altere as leituras depois da validação e elimine a coluna antiga em uma versão posterior.
Posso adicionar uma coluna no PostgreSQL sem bloquear o tráfego?
Em geral, sim. Uma coluna anulável sem valor padrão costuma ser uma alteração curta de metadados no PostgreSQL, mas ainda exige bloqueio da tabela. Defina um tempo limite curto para o bloqueio, para que a migração falhe em vez de esperar uma transação longa.
Como criar um índice sem bloquear gravações?
Use CREATE INDEX CONCURRENTLY quando a tabela precisar continuar gravável. A operação demora mais, aumenta a carga do banco e não pode ser executada dentro de um bloco de transação, portanto monitore latência, WAL e atraso das réplicas enquanto ela é executada.
Quando o aplicativo deve gravar os campos antigo e novo?
Grave ambos os valores na mesma transação do banco sempre que as duas representações precisarem permanecer atualizadas. Defina qual campo prevalece em caso de divergência e aplique as mesmas regras de normalização em APIs, workers, importações e ferramentas de suporte.
Como preencher uma tabela grande do PostgreSQL com segurança?
Processe lotes curtos e retomáveis, confirmando cada lote. Armazene um ponto de controle, atualize somente linhas que ainda precisam de trabalho e reduza ou pause o job quando aumentarem a latência das consultas, as esperas por bloqueio, o volume de WAL ou o atraso das réplicas.
Como saber se o preenchimento está completo e correto?
Não altere as leituras apenas porque o preenchimento terminou. Verifique se os valores obrigatórios existem, compare as representações antiga e nova, monitore leituras de fallback e confirme que novas gravações continuam consistentes depois do processamento dos dados históricos.
Quando devo adicionar NOT NULL, restrições CHECK ou chaves estrangeiras?
Adicione restrições rígidas depois que os dados existentes passarem pela validação e todos os gravadores ativos fornecerem valores válidos. O PostgreSQL permite adicionar algumas restrições como NOT VALID, aplicá-las a novas linhas e validar as linhas históricas separadamente.
Quando é seguro remover o caminho antigo do esquema?
Primeiro, remova as leituras de fallback; depois, interrompa as gravações antigas e observe o sistema durante um ciclo operacional completo. Quando aplicativos, jobs, relatórios, integrações e clientes não fizerem mais referência ao objeto antigo, remova o código relacionado e elimine a coluna ou tabela em uma versão posterior.