Guia: organizar migrations e rollbacks em PostgreSQL para equipes pequenas
Por Kevin Marcondes · Publicado em · Atualizado em · 10 min de leitura
Este guia mostra como organizar migrations e rollbacks em PostgreSQL para equipes pequenas, com processos leves e previsíveis. Você verá princípios, estrutura mínima de diretórios, fluxo de trabalho, estratégias de reversão e um checklist de CI/CD. O foco é ser prático para quem não tem um DBA dedicado, mas precisa de mudanças seguras.
Por que times pequenos sofrem com migrations (e como resolver)
Em equipes enxutas, a mesma pessoa codifica, revisa e faz deploy. Migrations improvisadas viram risco: conflitos entre branches, mudanças que travam a produção e falta de rollback claro.
Os sinais mais comuns são fáceis de reconhecer e apontam para a necessidade de um processo simples, porém disciplinado. A boa notícia: dá para organizar com convenções e poucos scripts bem pensados.
Se quiser ampliar fundamentos além do PostgreSQL, veja o nosso guia de versionamento de banco de dados ágil e conecte as ideias a este processo prático.
- Migrations que funcionam em dev e falham em staging/produção.
- Deploys que exigem janela longa por causa de locks e reindex.
- Rollback inexistente ou arriscado, exigindo correções manuais.
- Branches que geram arquivos de migration em ordem conflitante.
- Scripts sem idempotência e sem rastreio do que foi aplicado.
Princípios essenciais: atomicidade, idempotência e reversibilidade
Três princípios guiam o processo: (1) atomicidade com transações; (2) idempotência sempre que possível; (3) reversibilidade planejada. Some a isso visibilidade (log do que rodou) e parâmetros de segurança (timeouts e locks), e você elimina o risco mais comum em produção.
Use transações para DDL sempre que for seguro no PostgreSQL. Ajuste timeouts locais para não travar o banco. E aplique um lock lógico para evitar rodar duas migrations ao mesmo tempo na mesma base.
- Atomicidade: rode DDL/pequenos DML em uma transação.
- Idempotência: proteções como IF NOT EXISTS e checagens prévias.
- Reversibilidade: mantenha um down.sql simples e testável.
- Segurança: SET LOCAL statement_timeout/lock_timeout.
- Visibilidade: tabela schema_migrations para auditoria.
-- 20241003_add_status_to_orders_up.sql
BEGIN;
SET LOCAL lock_timeout = '5s';
SET LOCAL statement_timeout = '30s';
-- Evita concorrência entre execuções de migrations
SELECT pg_advisory_xact_lock(987654321);
-- Exemplo de mudança segura
ALTER TABLE public.orders
ADD COLUMN IF NOT EXISTS status text NOT NULL DEFAULT 'pending';
COMMENT ON COLUMN public.orders.status IS 'Estado do pedido';
COMMIT;
-- 20241003_add_status_to_orders_down.sql
BEGIN;
SET LOCAL lock_timeout = '5s';
SET LOCAL statement_timeout = '30s';
SELECT pg_advisory_xact_lock(987654321);
-- Reversão simples (cuidado com dados que dependam da coluna)
ALTER TABLE public.orders DROP COLUMN IF EXISTS status;
COMMIT;
Estrutura mínima de diretórios e convenções de nome
Padronize nomes e local dos arquivos. Isso ajuda no diff, revisão e automação. Prefira timestamp + slug curto descritivo. Separe up.sql e down.sql. Mantenha scripts utilitários em uma pasta à parte.
Crie também uma tabela schema_migrations para registrar versões aplicadas. Ela evita reaplicar scripts e dá rastreabilidade.
- Prefixo: AAAAMMDD-HHMMSS_slug.
- Separação: um par up/down por mudança atômica.
- Sem arquivos gigantes: divida por etapa se necessário.
- Coloque comentários claros no topo dos arquivos.
# Estrutura sugerida
.
├── db/
│ ├── migrations/
│ │ ├── 20241003-120000_add_status_up.sql
│ │ ├── 20241003-120000_add_status_down.sql
│ │ ├── 20241004-093000_add_idx_orders_created_at_up.sql
│ │ └── 20241004-093000_add_idx_orders_created_at_down.sql
│ └── scripts/
│ ├── migrate.sh
│ └── rollback.sh
└── .env
Fluxo de trabalho leve: branches, PR e staging antes do merge
Organize o fluxo para reduzir conflitos e validar rollbacks cedo. O objetivo é que cada PR contenha suas migrations, rodadas em staging automaticamente, com validação rápida e clara.
Para evitar conflitos de ordem, gere o timestamp no momento de criar a migration. Se dois PRs colidirem, rebaseie o que foi aberto primeiro e regenere o timestamp.
- Branch cria migration com timestamp único.
- PR roda migrations em banco de staging automaticamente.
- Revisor checa up/down e impactos de lock e tempo.
- Merge para main só após sucesso em staging.
- Tag de release dispara migrations em produção.
# Makefile (trecho) para padronizar comandos
migrate:
bash db/scripts/migrate.sh $(DATABASE_URL)
rollback:
bash db/scripts/rollback.sh $(DATABASE_URL) $(VERSION)
# Uso:
# make migrate DATABASE_URL=postgres://user:pass@host:5432/dbname
# make rollback DATABASE_URL=... VERSION=20241003-120000
Rollbacks na prática: estratégias por tipo de mudança
Nem toda mudança permite rollback trivial. Planeje reversão já no PR. Para DDL potencialmente custosa, faça rollout em duas fases: primeiro tornar a mudança compatível; depois, quando seguro, consolidar.
Para dados, evite grandes atualizações em lote dentro da migration. Prefira jobs idempotentes e observáveis fora da transação da migration, registrando progresso.
Se a reversão falhar ou o impacto for amplo, considere estratégias de recuperação como point‑in‑time recovery. Veja quando isso é útil em como recuperar uma tabela excluída com PITR.
- Adicionar coluna: permita NULL ou DEFAULT barato; backfill fora da migration.
- Índice: use CREATE INDEX CONCURRENTLY; rollback com DROP INDEX CONCURRENTLY.
- Renomear/alterar tipo: crie coluna nova, sincronize e faça cutover.
- Remover coluna: desative uso no app, monitore e só então drope.
- Data fix: registre linhas tocadas para reverter se necessário.
-- Exemplo de data fix com trilha para reversão
BEGIN;
CREATE TABLE IF NOT EXISTS ops_datafix_log (
key text PRIMARY KEY,
details jsonb,
applied_at timestamptz DEFAULT now()
);
-- Marca registros afetados antes de atualizar
INSERT INTO ops_datafix_log(key, details)
SELECT '20241005_fix_status_orders', jsonb_build_object('count', count(*))
FROM orders WHERE status = 'pending';
-- Atualiza de forma idempotente
UPDATE orders SET status = 'new'
WHERE status = 'pending';
COMMIT;
-- Para reverter, leia ops_datafix_log e volte ao estado anterior se fizer sentido.
Ferramentas leves que funcionam bem em times pequenos
Times pequenos podem começar com SQL puro + scripts. Isso reduz dependências e força clareza. Quando o volume cresce, ferramentas de migrations ajudam com rastreio, validações e dependências explícitas.
Critérios de escolha: facilidade de revisão de PR, suporte a up/down, execução transacional, locks, dry‑run, e integração simples com seu CI/CD. Evite ferramentas que escondem o SQL e dificultam auditoria.
- SQL + scripts: controle total, pouca mágica.
- Ferramentas dedicadas: trilha e dependências declarativas.
- Evite gerar migrations a partir de diff sem revisão humana.
- Padronize variáveis de ambiente e logs de execução.
Integração com CI/CD e checklist de release seguro
Automatize a execução em staging a cada PR. Em produção, dispare junto à release, com parâmetros de segurança e logs centralizados. Monitore locks, tempo de execução e rows modificadas.
Inclua checagens de saúde do banco e políticas de timeout. Trabalhos de dados grandes devem rodar fora da janela de pico e de forma incremental.
Se falhas de deploy são frequentes, mapeie gargalos e estabeleça um plano. Este processo se integra com uma auditoria de processos de deploy para ganhar previsibilidade.
- Preflight: backup testado e espaço livre suficiente.
- SET LOCAL statement_timeout/lock_timeout nas migrations.
- Lock lógico (pg_advisory_xact_lock) para evitar concorrência.
- Relatório: versões aplicadas, duração, erros, tamanho de tabelas.
- Jobs de dados: batches pequenos e reentrantes.
#!/usr/bin/env bash
# db/scripts/migrate.sh
set -euo pipefail
DATABASE_URL="$1"
export PGCONNECT_TIMEOUT=5
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 <<'SQL'
CREATE TABLE IF NOT EXISTS schema_migrations (
version text PRIMARY KEY,
applied_at timestamptz DEFAULT now()
);
SQL
for f in $(ls -1 db/migrations/*_up.sql | sort); do
ver=$(basename "$f" | cut -d'_' -f1)
if ! psql -Atqc "select 1 from schema_migrations where version='${ver}'" "$DATABASE_URL" | grep -q 1; then
echo "Applying $f";
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f "$f"
psql "$DATABASE_URL" -c "insert into schema_migrations(version) values ('${ver}') on conflict do nothing;"
fi
done
Quando usar migration, script ad‑hoc ou job de dados
Nem toda mudança cabe em uma migration transacional. Use esta tabela rápida para decidir a abordagem adequada. Para migrações complexas sem downtime, avalie padrões como CDC. Há um guia sobre isso em migração zero‑downtime com CDC.
| Cenário | Abordagem principal | Rollback recomendado | Observações |
|---|---|---|---|
| Adicionar coluna com default barato | Migration transacional (ALTER TABLE ... ADD COLUMN DEFAULT) | DROP COLUMN se seguro | Backfill fora da migration se a tabela for grande. |
| Criar índice em tabela grande | CREATE INDEX CONCURRENTLY | DROP INDEX CONCURRENTLY | Evita longos locks de escrita. |
| Alterar tipo de coluna populosa | Coluna nova + backfill incremental + cutover | Reapontar para a coluna antiga | Planeje janelas e monitore impacto. |
| Renomear tabela/coluna usada no app | Compatibilidade por etapa (views/colunas duplicadas) | Restaurar nomes antigos | Liberar flag no app antes do drop final. |
| Correção massiva de dados | Job batch idempotente com log de progresso | Reverter usando log/estado anterior | Não misture com deploy crítico. |
Perguntas frequentes
Qual ferramenta usar para migrations em times pequenos?
Comece com SQL puro e scripts versionados. Isso força clareza, facilita revisão e reduz dependências. Se o volume crescer, adote uma ferramenta de migrations que ofereça trilha de execução, up/down e integração simples com seu CI/CD. O importante é manter padrões e auditoria.
Como garantir rollback seguro no PostgreSQL?
Planeje a reversão no PR, mantenha down.sql simples e testável e evite data fixes irreversíveis. Para mudanças arriscadas, faça rollout em etapas e monitore. Se o impacto for amplo, use estratégias como point‑in‑time recovery ou cutover reversível, conforme seu RTO/RPO.
Como evitar conflitos de migrations entre branches?
Use timestamp no nome do arquivo e gere a migration ao abrir o branch. Se dois PRs colidirem, rebaseie e regenere o timestamp. Automatize a execução em staging por PR, garantindo que a ordem final no merge reflita a sequência correta de aplicação em produção.
Posso rodar grandes atualizações de dados dentro da migration?
Prefira não. Grandes atualizações prendem locks e ampliam janelas de falha. Rode data fixes como jobs idempotentes e em batches, com log de progresso e possibilidade de pausa. A migration deve criar estruturas e gatilhos necessários; o processamento ocorre fora dela.
Quais timeouts configurar nas migrations?
Defina SET LOCAL lock_timeout para evitar esperar por locks indefinidamente e SET LOCAL statement_timeout para impedir comandos longos demais. Valores exatos dependem do seu tráfego e tamanho das tabelas. O objetivo é falhar rápido e de forma previsível, sem travar a produção.
Como lidar com deadlocks ao aplicar migrations?
Reduza escopo da transação, ordene operações de forma consistente e aplique ALTERs compatíveis. Evite data fixes grandes dentro da migration. Em casos persistentes, audite jobs concorrentes e a ordem de acesso às tabelas. Consulte o guia sobre bloqueios e deadlocks em cron.
Quer fortalecer seu processo de migrations e rollbacks?
Converse sobre seu contexto, riscos atuais e próximos passos práticos para estruturar um fluxo previsível de mudanças no PostgreSQL.
Falar sobre seu banco de dadosVeja também
- Guia: checklist de pré‑deploy e plano de rollback rápido para aplicações PHP em produção
- Checklist de segurança para pipelines CI/CD com runners self‑hosted e secrets management
- Guia: centralização de logs com Fluentd e Elasticsearch para aplicações PHP em VPS
- Guia prático: como auto‑escalar runners self‑hosted no GitHub Actions para pipelines CI de projetos PHP