2

Zero-Downtime Migrations: Como Alterar Colunas e Tabelas Sem Derrubar Sua Aplicação

O problema real: um ALTER TABLE que derruba tudo

Uma migration que adiciona uma coluna NOT NULL sem default a uma tabela com 5 milhões de linhas adquire um ACCESS EXCLUSIVE lock no PostgreSQL. Enquanto o banco reescreve cada linha para validar a constraint, toda query que toca aquela tabela entra em fila. Se a operação leva 30 segundos, são 30 segundos de requests empilhando, health checks falhando e load balancers removendo instâncias.

O ponto cego é que a migration funciona perfeitamente no ambiente de staging com 200 linhas. O problema só aparece em produção, com volume real.

Este post cobre o padrão expand-contract e as técnicas operacionais para alterar schema em produção sem interromper o serviço. O foco é PostgreSQL com Prisma, mas os princípios se aplicam a qualquer banco relacional.

Expand-Contract: o modelo mental

A ideia é dividir qualquer migration destrutiva em fases seguras:

  1. Expand: adicione a estrutura nova sem remover a antiga. A aplicação continua funcionando com o schema antigo.
  2. Migrate data: preencha a estrutura nova com dados. Pode ser um backfill assíncrono.
  3. Transition: atualize o código da aplicação para ler e escrever na estrutura nova.
  4. Contract: remova a estrutura antiga quando nenhum deploy antigo estiver rodando.

Cada fase é um deploy separado. Cada fase é reversível. Se algo quebra na fase 2, você volta o código da fase 1 e a estrutura antiga ainda está lá.

Se você usa rolling updates ou blue-green deploy, esse modelo é obrigatório: durante a janela de transição, instâncias antigas e novas coexistem. O schema precisa ser compatível com ambas.

Operações perigosas e suas alternativas seguras

OperaçãoRiscoAlternativa zero-downtime
ADD COLUMN ... NOT NULL (sem default)ACCESS EXCLUSIVE lock + rewrite da tabela inteiraAdicionar com default, depois remover o default
ALTER COLUMN TYPE (ex: varchar → integer)Rewrite completo da tabelaCriar coluna nova, backfill, trocar no código, remover antiga
RENAME COLUMNQuebra queries em andamentoCriar coluna nova, manter ambas, migrar código, remover antiga
DROP COLUMNQuebra código antigo ainda em execuçãoRemover do código primeiro, depois dropar a coluna
ADD INDEXSHARE lock bloqueia writesCREATE INDEX CONCURRENTLY
ADD CONSTRAINT ... NOT NULL (retroativa)Valida todas as linhas com lockNOT VALID + VALIDATE CONSTRAINT separado

Adicionando coluna NOT NULL com segurança

A operação mais comum e mais perigosa. O caminho seguro tem três migrations separadas.

Fase 1: adicionar coluna nullable com default

-- migration_001_add_status_column.sql
-- Adicionar com DEFAULT não reescreve a tabela no PostgreSQL 11+.
-- O banco armazena o default no catálogo e aplica on-read.
ALTER TABLE orders ADD COLUMN status TEXT DEFAULT 'pending';

Fase 2: backfill de dados existentes

-- migration_002_backfill_status.sql
-- Atualizar em batches para não segurar lock por tempo demais.
-- O WHERE evita reescrever linhas que já têm valor correto.
UPDATE orders
SET status = 'pending'
WHERE status IS NULL
  AND id BETWEEN 1 AND 100000;

UPDATE orders
SET status = 'pending'
WHERE status IS NULL
  AND id BETWEEN 100001 AND 200000;

Fase 3: adicionar constraint NOT NULL sem rewrite

-- migration_003_add_not_null_constraint.sql
-- NOT VALID diz ao PostgreSQL para não validar linhas existentes agora.
-- Isso adquire um lock brevíssimo (milissegundos).
ALTER TABLE orders ADD CONSTRAINT orders_status_not_null
  CHECK (status IS NOT NULL) NOT VALID;

-- VALIDATE CONSTRAINT verifica linhas existentes sem ACCESS EXCLUSIVE lock.
-- Usa apenas SHARE UPDATE EXCLUSIVE, que não bloqueia reads nem writes.
ALTER TABLE orders VALIDATE CONSTRAINT orders_status_not_null;

Esse padrão de três fases é o que o [post sobre migrations seguras com Prisma](/b


Leia o artigo completo em https://www.vivodecodigo.com.br/backend/zero-downtime-migrations-alterar-colunas-tabelas-sem-derrubar-aplicacao

Carregando publicação patrocinada...