Truques para escrever migrations de banco de dados seguras
1. Planejamento e Versionamento de Migrations
A base de uma migration segura começa antes mesmo de escrever uma linha de SQL. A nomenclatura consistente é o primeiro pilar: utilize timestamps no formato YYYYMMDDHHMMSS_descricao.sql ou numeração sequencial (001_criar_usuarios.sql, 002_adicionar_email.sql). Isso garante ordem de execução previsível e evita conflitos em equipes.
Checklist de pré-requisitos antes de escrever:
- A migration foi revisada por pelo menos um colega?
- O impacto em tabelas com milhões de linhas foi avaliado?
- Existe um plano de rollback testado?
- A migration foi executada em ambiente staging com dados similares aos de produção?
Estrutura de branches recomendada:
main/
└── migrations/
├── 20240101000000_criar_tabela_clientes.sql
├── 20240102000000_adicionar_coluna_telefone.sql
└── 20240103000000_remover_coluna_antiga.sql
Para ambientes, mantenha um arquivo de controle (migration_version.txt) que registre qual migration foi aplicada em cada ambiente, evitando execuções duplicadas.
2. Uso de Transações para Atomicidade
Toda migration deve ser envolvida em uma transação explícita. Isso garante que, se algo falhar no meio do caminho, o banco retorne ao estado anterior sem corromper dados.
Exemplo de migration segura com transação:
BEGIN;
-- Passo 1: Criar nova coluna
ALTER TABLE usuarios ADD COLUMN email_alternativo VARCHAR(255);
-- Passo 2: Copiar dados existentes
UPDATE usuarios SET email_alternativo = email WHERE email_alternativo IS NULL;
-- Passo 3: Remover coluna antiga (após validação)
ALTER TABLE usuarios DROP COLUMN email;
COMMIT;
Limitação importante: Em bancos como MySQL com engine MyISAM ou PostgreSQL em operações DDL específicas, transações podem não funcionar. Nesses casos, utilize estratégias de rollback manual:
-- Tente aplicar a migration
ALTER TABLE pedidos MODIFY COLUMN status VARCHAR(20) NOT NULL;
-- Se falhar, execute manualmente:
ALTER TABLE pedidos MODIFY COLUMN status VARCHAR(10) NOT NULL;
Para bancos que suportam DDL transacional (PostgreSQL, SQL Server), sempre prefira o uso de transações.
3. Migrations Idempotentes e Reversíveis
Uma migration idempotente pode ser executada múltiplas vezes sem causar erros. Isso é crucial para ambientes onde a ordem de execução pode variar.
Exemplo de migration com verificações de existência:
-- Migration UP
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'produtos' AND column_name = 'preco_promocional'
) THEN
ALTER TABLE produtos ADD COLUMN preco_promocional DECIMAL(10,2);
END IF;
END $$;
-- Migration DOWN
ALTER TABLE produtos DROP COLUMN IF EXISTS preco_promocional;
Sempre escreva down completo e testado:
-- UP: Cria índice
CREATE INDEX IF NOT EXISTS idx_pedidos_data ON pedidos(data_criacao);
-- DOWN: Remove índice
DROP INDEX IF EXISTS idx_pedidos_data;
Verificação de estado atual antes de alterações:
-- Antes de renomear coluna, verifique se a nova já existe
SELECT COUNT(*) FROM information_schema.columns
WHERE table_name = 'clientes' AND column_name = 'nome_completo';
-- Se COUNT > 0, pule a migration ou trate o conflito
4. Validação de Dados Antes de Alterações Estruturais
Nunca confie cegamente que os dados estão no formato esperado. Sempre valide antes de fazer alterações destrutivas.
Script de validação antes de remover coluna:
-- Verificar se todos os dados foram migrados corretamente
SELECT COUNT(*) FROM usuarios
WHERE email_alternativo IS NULL AND email IS NOT NULL;
-- Se COUNT > 0, pare a migration e corrija os dados
-- Se COUNT = 0, prossiga com a remoção
Backup dos dados críticos antes da migration:
-- Criar tabela de backup
CREATE TABLE usuarios_backup_20240101 AS
SELECT * FROM usuarios;
-- Ou exportar para CSV (via ferramenta externa)
COPY usuarios TO '/tmp/backup_usuarios.csv' DELIMITER ',' CSV HEADER;
Uso de condições para evitar perda acidental:
-- Remover apenas registros que atendem a critérios específicos
DELETE FROM logs_antigos WHERE data_criacao < '2023-01-01';
-- Nunca use DELETE sem WHERE em migrations
-- DELETE FROM logs_antigos; -- Isso removeria TUDO!
5. Estratégias de Deploy Sem Downtime
Para sistemas críticos, migrations precisam ser aplicadas sem interromper o serviço. A técnica de "expandir, migrar, contrair" é a mais utilizada.
Passo 1: Expandir (adicionar nova estrutura sem remover a antiga)
BEGIN;
ALTER TABLE usuarios ADD COLUMN email_novo VARCHAR(255);
ALTER TABLE usuarios ADD COLUMN email_antigo VARCHAR(255);
COMMIT;
Passo 2: Migrar (copiar dados gradualmente)
-- Em lote, para evitar locks longos
UPDATE usuarios SET email_novo = email
WHERE email_novo IS NULL
LIMIT 1000;
-- Repita até que todos os registros sejam migrados
Passo 3: Contrair (remover estrutura antiga após validação)
-- Somente após confirmar que nenhum sistema usa mais a coluna antiga
ALTER TABLE usuarios DROP COLUMN email;
ALTER TABLE usuarios DROP COLUMN email_antigo;
Uso de colunas nullable temporárias:
-- Adicione a nova coluna como nullable primeiro
ALTER TABLE pedidos ADD COLUMN novo_status VARCHAR(20) NULL;
-- Preencha os dados
UPDATE pedidos SET novo_status = status;
-- Depois torne NOT NULL (em outra migration)
ALTER TABLE pedidos ALTER COLUMN novo_status SET NOT NULL;
6. Testes Automatizados de Migrations
Testes automatizados são a única garantia de que migrations funcionarão em produção.
Estrutura de teste com dados reais anonimizados:
-- 1. Copiar estrutura de produção para staging
CREATE DATABASE staging_test TEMPLATE producao;
-- 2. Anonimizar dados sensíveis
UPDATE usuarios SET email = CONCAT('user_', id, '@test.com');
UPDATE usuarios SET cpf = LPAD(CAST(id AS TEXT), 11, '0');
-- 3. Executar migrations
-- (via script automatizado)
Teste de performance:
-- Medir tempo de execução
EXPLAIN ANALYZE
ALTER TABLE pedidos ADD COLUMN desconto DECIMAL(5,2);
-- Verificar locks gerados
SELECT * FROM pg_locks WHERE relation = 'pedidos'::regclass;
Teste de reversão:
-- Aplicar UP
-- Verificar estado
-- Aplicar DOWN
-- Verificar se estado retornou ao original
-- Repetir ciclo 3x para garantir idempotência
7. Monitoramento e Logging Durante a Execução
Logs detalhados são essenciais para diagnosticar problemas em tempo real.
Exemplo de logging estruturado:
-- Início da migration
INSERT INTO migration_log (nome, status, inicio) VALUES ('20240101_adicionar_email', 'INICIADO', NOW());
-- Passos intermediários
RAISE NOTICE 'Criando coluna email_novo...';
ALTER TABLE usuarios ADD COLUMN email_novo VARCHAR(255);
RAISE NOTICE 'Coluna email_novo criada com sucesso';
-- Finalização
UPDATE migration_log SET status = 'CONCLUIDO', fim = NOW() WHERE nome = '20240101_adicionar_email';
Alertas para migrations lentas:
-- Configurar timeout (exemplo PostgreSQL)
SET statement_timeout = '300s';
-- Se a migration exceder 5 minutos, disparar alerta
-- (via ferramenta externa monitorando logs)
Ferramentas de observabilidade recomendadas:
- pg_stat_activity (PostgreSQL) para monitorar locks
- Performance Insights (AWS RDS) para métricas de banco
- Datadog ou New Relic para APM integrado
Referências
- Documentação oficial de migrations do Rails — Guia completo sobre como escrever migrations seguras e reversíveis no ecossistema Rails
- PostgreSQL: Migrations Without Downtime — Documentação oficial sobre constraints e alterações de esquema sem bloquear tabelas
- Laravel Migrations: Best Practices — Tutorial oficial sobre migrations idempotentes e versionamento no Laravel
- Zero-Downtime Database Migrations (Stripe Blog) — Artigo técnico da Stripe sobre estratégias de deploy sem downtime para bancos de dados
- Flyway: Database Migrations Made Easy — Documentação da ferramenta Flyway com exemplos de migrations versionadas e testadas
- Testing Database Migrations (ThoughtWorks) — Artigo sobre como testar migrations automaticamente com pipelines CI/CD