Migrações de banco de dados sem downtime

Uma migração que toma um lock exclusivo no momento do deploy é uma queda programada com distintivo de deploy. Migrações Postgres sem downtime usam expand/contract: adicionar schema antes de removê-lo, fazer backfill antes de aplicar restrições e nunca bloquear leituras no hot path.

Engenharia7 min de leitura
PostgresMigrações de banco de dadosZero downtimeExpand contractDevOps
Compartilhar

Deploy na sexta. A migração roda. ALTER TABLE orders ADD COLUMN status_v2 VARCHAR(20) NOT NULL DEFAULT 'pending' — exceto que a tabela tem quarenta milhões de linhas e o Postgres reescreve o heap sob um lock ACCESS EXCLUSIVE. O checkout para por onze minutos. O time chama de deploy, não de queda. Os clientes chamam de outra coisa. Migração Postgres sem downtime não é feature de ferramenta — é disciplina: expandir o schema junto ao antigo, migrar dados de forma assíncrona, contrair só após a prova e tratar a duração do lock como bloqueador de release.

A produção não pausa por mudanças de schema. Aplicações são implantadas de forma contínua. Versões antigas e novas do código rodam em simultâneo durante deploys. Migrações devem funcionar enquanto ambas as versões estão ativas — compatíveis para trás na subida, compatíveis para frente até a fase de contract terminar.

Modos de lock decidem se o deploy é migração ou queda

DDL do Postgres adquire níveis de lock que impedem acesso concorrente:

Lock levelBlocks reads?Blocks writes?Typical DDL
ACCESS SHARENoNoinício de CREATE INDEX CONCURRENTLY
ROW EXCLUSIVENoNoINSERT, UPDATE, DELETE
SHARE UPDATE EXCLUSIVENoBloqueio breve de escritaVACUUM, CREATE INDEX CONCURRENTLY
ACCESS EXCLUSIVESimSima maioria das formas de ALTER TABLE, DROP

Qualquer migração que exija ACCESS EXCLUSIVE em uma tabela quente em horário de pico é um plano de queda. Medir espera de lock em staging com contagens de linhas no formato produção — não tabelas vazias.

Uma coluna NOT NULL adicionada em uma transação em tabela grande é um lock, não uma estratégia de migração.

Expand/contract é o padrão de migração sem downtime

Três fases, muitas vezes em vários deploys:

Expand. Adicionar schema novo sem quebrar o código antigo.

  • Adicionar coluna nullable — o código antigo ignora.
  • Adicionar tabela nova junto à antiga — dual-write ou sync assíncrono.
  • Adicionar índice novo CONCURRENTLY — sem lock de tabela para leituras/escritas.
  • Adicionar trigger ou coluna gerada para o período de transição.

Migrate. Mover dados e tráfego.

  • Fazer backfill da coluna nova em lotes — UPDATE ... WHERE id BETWEEN ? AND ? LIMIT 1000 com pausa entre lotes.
  • Dual-write: a aplicação escreve em colunas/tabelas antigas e novas.
  • Trocar leituras para o caminho novo via feature flag quando o backfill terminar.
  • Validar com queries de reconciliação — contagens, checksums, diffs por amostra.

Contract. Remover schema antigo só após a prova.

  • Dropar coluna antiga — só quando nenhum código lê ou escreve nela.
  • Dropar tabela antiga — só após tráfego em zero por período sustentado.
  • Adicionar restrição NOT NULL — só após o backfill confirmar que não há nulls.
Deploy 1 (expand):  ADD COLUMN email_normalized TEXT NULL
Deploy 2 (migrate): backfill job + app dual-write
Deploy 3 (migrate): app reads email_normalized; flag on
Deploy 4 (contract): DROP COLUMN email_legacy — after N days zero traffic

O artigo sobre estratégia de rollback para deploys cobre o que acontece quando deploys na fase migrate falham — rollback na fase expand costuma ser reverter a app enquanto o schema permanece; rollback na fase contract pode ser impossível sem forward-fix.

Operações de alto risco e alternativas seguras

Risky migrationWhy it locks or blocksSafe pattern
ADD COLUMN ... NOT NULL em tabela grandeReescreve a tabela, lock exclusivoAdicionar nullable → backfill → SET NOT NULL com check constraint em etapas
CREATE INDEX (não concurrent)Lock SHARE bloqueia escritasCREATE INDEX CONCURRENTLY
Mudança de tipo de colunaReescrita, lock exclusivoColuna nova + backfill + swap de rename em contract
ADD FOREIGN KEYValida todas as linhas, lock forteRestrição NOT VALID primeiro → VALIDATE CONSTRAINT separadamente
Reescrita de tabela (ALTER ... TYPE)Lock totalTabela nova + cópia + swap de views ou routing
Renomear colunaQuebra o código antigo na horaAdicionar nova, dual-write, deprecar nome antigo no código primeiro

CREATE INDEX CONCURRENTLY não pode rodar dentro de um bloco de transação. Ferramentas de migração que envolvem tudo em transações precisam de tratamento especial para criação concorrente de índices.

Backfill sem derreter o banco de dados

Backfills em lotes protegem a produção:

-- Repeat until zero rows affected
UPDATE orders
SET status_v2 = map_status(status_legacy)
WHERE id IN (
  SELECT id FROM orders
  WHERE status_v2 IS NULL
  LIMIT 5000
  FOR UPDATE SKIP LOCKED
);

Padrões:

  • Tamanho de lote ajustado ao I/O — monitorar replication lag em réplicas durante o backfill.
  • SKIP LOCKED — evitar disputar escritas ao vivo pelas mesmas linhas.
  • Agendamento fora de pico — backfill é carga de produção.
  • Acompanhamento de progresso — linhas restantes, ETA, interruptor de pausa para incidentes.
  • Backfill idempotente — seguro de reiniciar; guarda WHERE new_col IS NULL.

Jobs de backfill competem com tráfego ao vivo por I/O e slots do connection pool. Coordenar com connection pooling em serverless — backfills de longa duração pelo mesmo pool que request handlers esgotam queries da aplicação.

Matriz de compatibilidade da aplicação por deploy

Cada deploy de migração precisa de uma matriz escrita:

Deploy phaseOld app (N)New app (N+1)Schema state
ExpandWorksWorksColunas novas nullable existem
Dual-writeEscreve só col antigaEscreve ambasAmbas as colunas são preenchidas ao longo do tempo
Read switchLê antigaLê novaBackfill completo
ContractDeve estar foraWorksColuna antiga removida

Se N e N+1 devem rodar em simultâneo — sempre verdade em rolling deploys — mudanças de schema não devem quebrar N. A fase contract exige prova de que N não está mais em execução.

Como equipes devem executar migrações Postgres sem downtime?

Essas práticas separam a disciplina expand/contract de deploys de travar-e-rezar.

Todas as migrações podem ser sem downtime?

Não. Algumas mudanças — dividir uma tabela muito contenciosa, reescritas fundamentais do modelo — exigem janelas de manutenção ou modo somente leitura. O objetivo é sem downtime por padrão, janela de manutenção por decisão explícita com aviso a stakeholders — não por acidente quando um lock atinge produção.

Quantos deploys expand/contract deve levar?

Quantos forem necessários por segurança. Sequências de quatro deploys são normais. Apressar contract antes do backfill terminar cria corrupção de dados, não velocidade.

Quando uma janela de manutenção é aceitável?

Períodos de baixo tráfego para operações que genuinamente não podem ser concorrentes — raro com o tooling do Postgres. Documentar RTO, avisar clientes, ensaiar. Não usar janelas de manutenção para evitar aprender CREATE INDEX CONCURRENTLY.

Uma visão oposta

Um argumento frequente sustenta que migrações sem downtime acrescentam complexidade desproporcional ao risco de queda — que times pequenos deveriam aceitar locks breves e enviar mais rápido.

Locks breves em tabelas pequenas são aceitáveis. Locks breves em tabelas quentes com milhões de linhas não são breves. A complexidade de expand/contract é carregada no início e documentada; a complexidade de quedas não planejadas de onze minutos é reativa e visível ao cliente. Times que medem duração de lock em staging raramente escolhem o lock.

Key takeaways

  • Migrações sem downtime usam expand/contract em vários deploys — nunca DDL big-bang em tabelas quentes.
  • Locks ACCESS EXCLUSIVE em tabelas de produção são planos de queda — medir em staging com contagens reais de linhas.
  • Adicionar colunas nullable primeiro; backfill em lotes; forçar NOT NULL só após o backfill terminar.
  • CREATE INDEX CONCURRENTLY e foreign keys NOT VALID evitam locks de validação.
  • Escrever matriz de compatibilidade da app por deploy — código antigo e novo rodam em simultâneo em rolling deploys.
  • Backfill é carga de produção — lotear, monitorar replication lag em réplicas, coordenar com connection pools.

Conclusion

Migrações de banco de dados sem downtime são um contrato com a produção em execução: mudanças de schema chegam compatíveis com o código que serve tráfego agora, dados se movem em cronogramas que respeitam limites de I/O e passos destrutivos de contract esperam a prova.

A próxima migração deve ser classificada antes de alguém escrever SQL: expand, migrate ou contract? Se a resposta for «os três em uma transação», devolver para redesenho. A produção não pausa — migrações não deveriam pedir isso.

Artigos relacionados

Paleta de comandos

Pesquise um comando para executar...