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