Migrations sem medo: estratégias para alterar schema em produção
1. Por que migrations em produção assustam (e como evitar o pânico)
Alterar o schema de um banco de dados em produção é uma das operações mais temidas por engenheiros de software. Os riscos são reais: downtime inesperado, dados inconsistentes, rollbacks que falham e a temida corrupção de registros. Em ambiente de desenvolvimento, uma migration falha significa apenas reverter um commit local. Em produção, o mesmo erro pode derrubar um sistema inteiro por horas.
A diferença fundamental está no contexto: em produção há concorrência real de usuários, volume massivo de dados e dependências complexas entre serviços. Uma simples adição de coluna pode travar uma tabela de milhões de registros por minutos. A mentalidade correta é tratar cada migration como uma cirurgia: testar exaustivamente, versionar cada passo e documentar cada decisão.
O primeiro passo para perder o medo é entender que migrations não precisam ser eventos de risco. Com planejamento e estratégias adequadas, é possível alterar schemas sem jamais interromper o serviço.
2. Estratégias de deployment: expandir vs. contrair (expand-contract pattern)
O padrão expand-contract (ou parallel change) é a abordagem mais segura para alterações de schema. Ele divide a migration em três fases, garantindo que nunca haja um ponto onde código antigo e novo sejam incompatíveis.
Fase 1: Expandir — Adicionar sem quebrar
Nesta fase, você adiciona novos campos, tabelas ou índices sem modificar ou remover os existentes. O código antigo continua funcionando perfeitamente.
-- Exemplo: Adicionar nova coluna sem remover a antiga
ALTER TABLE usuarios ADD COLUMN email_novo VARCHAR(255);
-- A coluna 'email' antiga continua existindo e sendo usada pelo código legado
Fase 2: Migrar — Transição gradual
A aplicação passa a escrever nos dois campos simultaneamente, enquanto um job de backfill preenche os dados históricos. Após validar a consistência, o código passa a ler apenas do novo campo.
-- Job de backfill em lotes
UPDATE usuarios
SET email_novo = email
WHERE email_novo IS NULL
LIMIT 1000;
-- Repetir até que todos os registros estejam migrados
Fase 3: Contrair — Remover o legado
Após confirmar que tudo está estável (monitore por dias ou semanas), remova a coluna antiga.
ALTER TABLE usuarios DROP COLUMN email;
3. Técnicas para alterações sem downtime
Colunas com valor default
Adicionar uma coluna com NOT NULL e valor default em tabelas grandes pode travar o banco por horas. A solução é adicionar primeiro sem constraints, depois preencher os valores e só então aplicar a constraint.
-- Abordagem incorreta (pode travar)
ALTER TABLE pedidos ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'pendente';
-- Abordagem correta (sem downtime)
ALTER TABLE pedidos ADD COLUMN status VARCHAR(20);
UPDATE pedidos SET status = 'pendente' WHERE status IS NULL; -- em lotes
ALTER TABLE pedidos ALTER COLUMN status SET NOT NULL;
ALTER TABLE pedidos ALTER COLUMN status SET DEFAULT 'pendente';
Índices concorrentes
No PostgreSQL, criar um índice tradicional bloqueia escritas na tabela. Use CONCURRENTLY para criar sem interromper operações.
CREATE INDEX CONCURRENTLY idx_pedidos_data ON pedidos (data_criacao);
-- A criação leva mais tempo, mas não bloqueia leituras e escritas
Renomeação com views
Para renomear tabelas ou colunas sem quebrar queries existentes, crie uma view como alias temporário.
-- Em vez de renomear diretamente
CREATE VIEW clientes AS SELECT * FROM usuarios;
-- Gradualmente, migre as queries para usar a nova tabela
-- Depois, remova a view antiga
DROP VIEW clientes;
4. Migrações de dados em larga escala (backfill)
Quando é necessário migrar milhões de registros, fazer tudo em uma única transação é inviável. O segredo é o batch processing com limites controlados.
-- Backfill seguro com batches
DO $$
DECLARE
batch_size INTEGER := 5000;
affected_rows INTEGER;
BEGIN
LOOP
UPDATE produtos
SET preco_com_taxa = preco * 1.1
WHERE preco_com_taxa IS NULL
LIMIT batch_size;
GET DIAGNOSTICS affected_rows = ROW_COUNT;
EXIT WHEN affected_rows = 0;
COMMIT; -- Libera locks entre batches
PERFORM pg_sleep(1); -- Pausa para reduzir impacto
END LOOP;
END $$;
Para cenários ainda maiores, considere estratégias assíncronas com filas de mensagens (RabbitMQ, SQS) ou workers processando jobs em paralelo. A validação pós-migration pode usar checksums:
-- Validar consistência antes/depois
SELECT COUNT(*), SUM(CAST(MD5(CAST(preco AS TEXT)) AS NUMERIC))
FROM produtos WHERE preco_com_taxa IS NOT NULL;
5. Ferramentas e boas práticas de versionamento
Ferramentas como Flyway, Liquibase e Alembic oferecem controle de versão e ordem de execução. A nomenclatura deve ser semântica:
V20240315_001_adicionar_coluna_status.sql
V20240315_002_backfill_status.sql
V20240316_001_remover_coluna_antiga.sql
Testes automatizados são essenciais. Em ambiente staging, execute a migration e o rollback várias vezes:
# Script de teste de migration
alembic upgrade head
python -m pytest tests/test_schema.py
alembic downgrade -1
python -m pytest tests/test_schema_rollback.py
6. Rollback e recuperação: o plano B
Toda migration deve ter um down consistente. Nunca confie em backups como única estratégia de rollback.
-- Migration UP
ALTER TABLE usuarios ADD COLUMN telefone VARCHAR(20);
-- Migration DOWN
ALTER TABLE usuarios DROP COLUMN telefone;
Para mudanças destrutivas, tire um snapshot do schema antes:
-- Snapshot de schema
pg_dump --schema-only -t usuarios > schema_backup_antes_migration.sql
Tenha réplicas de leitura configuradas para fallback imediato. Se algo der errado, redirecione o tráfego para a réplica enquanto restaura o primário.
7. Monitoramento e validação pós-migration
Após aplicar a migration, monitore métricas críticas por pelo menos 24 horas:
- Latência de queries: aumentos acima de 20% indicam problemas de índices
- Erros de query: deadlocks ou violações de constraint
- CPU/IO do banco: picos anormais durante operações normais
Configure alertas automáticos:
-- Alerta de deadlock no PostgreSQL
SELECT COUNT(*) FROM pg_stat_activity
WHERE wait_event_type = 'Lock'
AND state = 'active';
Checklist de validação obrigatório:
1. Verificar integridade referencial:
SELECT * FROM tabela_filho WHERE fk_id NOT IN (SELECT id FROM tabela_pai);
2. Verificar índices:
SELECT schemaname, tablename, indexname, idx_scan
FROM pg_stat_user_indexes WHERE idx_scan = 0;
3. Comparar performance antes/depois:
EXPLAIN ANALYZE SELECT * FROM tabela WHERE condicao;
Com essas estratégias, migrations em produção deixam de ser um evento de risco para se tornarem operações rotineiras e controladas. O medo dá lugar à confiança baseada em processos testados e ferramentas adequadas.
Referências
- PostgreSQL Documentation: ALTER TABLE — Documentação oficial sobre alterações de schema no PostgreSQL, incluindo opções para evitar locks.
- Flyway Documentation: Best Practices — Guia oficial de boas práticas para versionamento de migrations com Flyway.
- Liquibase Documentation: Rollback Strategies — Estratégias de rollback e gerenciamento de mudanças no schema com Liquibase.
- Alembic Documentation: Operations Reference — Referência completa de operações de migration com Alembic para SQLAlchemy.
- PostgreSQL Wiki: Indexes and Concurrency — Artigo técnico sobre criação de índices concorrentes e boas práticas de performance.
- Use The Index, Luke: Schema Changes Without Downtime — Guia prático sobre alterações de schema sem downtime em bancos relacionais.
- Martin Fowler: ParallelChange (Expand-Contract) — Explicação detalhada do padrão expand-contract para mudanças evolutivas em sistemas.