Guia: organizar migrations e rollbacks em PostgreSQL para equipes pequenas

Por · Publicado em · Atualizado em · 10 min de leitura

Guia: organizar migrations e rollbacks em PostgreSQL para equipes pequenas
Foto: Christina Morillo / Pexels

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 dados

Veja também