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.
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 level | Blocks reads? | Blocks writes? | Typical DDL |
|---|---|---|---|
ACCESS SHARE | No | No | início de CREATE INDEX CONCURRENTLY |
ROW EXCLUSIVE | No | No | INSERT, UPDATE, DELETE |
SHARE UPDATE EXCLUSIVE | No | Bloqueio breve de escrita | VACUUM, CREATE INDEX CONCURRENTLY |
ACCESS EXCLUSIVE | Sim | Sim | a 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 1000com 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 migration | Why it locks or blocks | Safe pattern |
|---|---|---|
ADD COLUMN ... NOT NULL em tabela grande | Reescreve a tabela, lock exclusivo | Adicionar nullable → backfill → SET NOT NULL com check constraint em etapas |
CREATE INDEX (não concurrent) | Lock SHARE bloqueia escritas | CREATE INDEX CONCURRENTLY |
| Mudança de tipo de coluna | Reescrita, lock exclusivo | Coluna nova + backfill + swap de rename em contract |
ADD FOREIGN KEY | Valida todas as linhas, lock forte | Restrição NOT VALID primeiro → VALIDATE CONSTRAINT separadamente |
Reescrita de tabela (ALTER ... TYPE) | Lock total | Tabela nova + cópia + swap de views ou routing |
| Renomear coluna | Quebra o código antigo na hora | Adicionar 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 phase | Old app (N) | New app (N+1) | Schema state |
|---|---|---|---|
| Expand | Works | Works | Colunas novas nullable existem |
| Dual-write | Escreve só col antiga | Escreve ambas | Ambas as colunas são preenchidas ao longo do tempo |
| Read switch | Lê antiga | Lê nova | Backfill completo |
| Contract | Deve estar fora | Works | Coluna 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 EXCLUSIVEem 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 CONCURRENTLYe foreign keysNOT VALIDevitam 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.