Migrações de schema: como atualizar o banco sem downtime em produção
1. Fundamentos: Por que migrações sem downtime são críticas
Em sistemas modernos de alta disponibilidade, o tempo de inatividade não é apenas um inconveniente — é uma ameaça direta ao negócio. SLAs de 99,99% permitem apenas 52 minutos de downtime por ano, e cada minuto extra pode custar milhares de dólares em receita perdida e danos à reputação.
A diferença fundamental entre migração de schema e migração de dados reside na natureza da operação. Migrações de schema alteram a estrutura do banco (colunas, tabelas, índices), enquanto migrações de dados transformam valores existentes. O grande desafio é que ambas precisam ocorrer sem interromper o fluxo de operações.
O princípio central de qualquer migração sem downtime é a compatibilidade retroativa: o schema deve funcionar corretamente tanto com a versão antiga quanto com a nova do código da aplicação. Isso significa que você nunca pode remover algo que o código antigo ainda está usando.
2. Estratégias de deploy paralelo (Expand-Migrate-Contract)
O padrão Expand-Migrate-Contract (também conhecido como parallel change) é a estratégia mais robusta para migrações sem downtime. Ele divide a operação em três fases distintas:
Fase Expand — Adicione novos elementos ao schema sem remover os antigos:
-- Adicionar nova coluna sem remover a antiga
ALTER TABLE usuarios ADD COLUMN email_novo VARCHAR(255) DEFAULT NULL;
-- Criar nova tabela paralela
CREATE TABLE usuarios_v2 (LIKE usuarios INCLUDING ALL);
Fase Migrate — Sincronize dados entre os schemas antigo e novo:
-- Backfill em lotes
UPDATE usuarios SET email_novo = email WHERE email_novo IS NULL LIMIT 1000;
-- Trigger para sincronização contínua
CREATE OR REPLACE FUNCTION sincronizar_email()
RETURNS TRIGGER AS $$
BEGIN
NEW.email_novo := NEW.email;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_sinc_email
BEFORE INSERT OR UPDATE ON usuarios
FOR EACH ROW EXECUTE FUNCTION sincronizar_email();
Fase Contract — Remova elementos legados após verificação:
-- Remover coluna antiga após confirmar que nenhum código a utiliza
ALTER TABLE usuarios DROP COLUMN email;
3. Técnicas para adicionar colunas e tabelas sem bloqueio
Em bancos como PostgreSQL e MySQL, ALTER TABLE ADD COLUMN com DEFAULT NULL é uma operação quase instantânea que não requer reescrita da tabela. Já adicionar uma coluna com DEFAULT não nulo pode travar a tabela inteira.
Para índices, o PostgreSQL oferece CREATE INDEX CONCURRENTLY:
-- Cria índice sem bloquear escritas
CREATE INDEX CONCURRENTLY idx_usuarios_email_novo ON usuarios(email_novo);
-- Após criação, tornar o índice utilizável
ALTER INDEX idx_usuarios_email_novo SET (fillfactor = 90);
Para novas tabelas, a replicação assíncrona permite criar a estrutura completa sem impactar a produção:
-- Criar tabela réplica em servidor secundário
CREATE TABLE pedidos_novo (LIKE pedidos INCLUDING ALL);
-- Configurar replicação lógica para manter sincronia
-- (exemplo conceitual)
SELECT * FROM pg_create_logical_replication_slot('slot_pedidos', 'pgoutput');
4. Migrações de dados pesadas: backfill e sincronização
Migrações de dados volumosas exigem processamento em lotes com janelas de baixa carga. O backfill deve ser feito em batches pequenos para evitar locks prolongados:
-- Backfill em batches de 1000 registros
DO $$
DECLARE
v_linhas_afetadas INTEGER;
BEGIN
LOOP
UPDATE usuarios
SET email_novo = email
WHERE email_novo IS NULL
LIMIT 1000;
GET DIAGNOSTICS v_linhas_afetadas = ROW_COUNT;
EXIT WHEN v_linhas_afetadas = 0;
COMMIT;
PERFORM pg_sleep(0.1); -- Pausa para reduzir contenção
END LOOP;
END;
$$;
Para sincronização em tempo real, o sistema LISTEN/NOTIFY do PostgreSQL permite notificar mudanças:
-- Notificar mudanças
CREATE OR REPLACE FUNCTION notificar_mudanca_email()
RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify('canal_migracao',
json_build_object('id', NEW.id, 'email', NEW.email)::text);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Aplicação ouve as notificações e atualiza o schema novo
-- (exemplo em Python/psycopg2)
5. Renomeação e remoção segura de colunas e tabelas
O padrão "rename-and-deprecate" é essencial para remoções seguras:
-- Fase 1: Renomear coluna antiga
ALTER TABLE usuarios RENAME COLUMN email TO email_legado;
-- Fase 2: Criar nova coluna com nome original
ALTER TABLE usuarios ADD COLUMN email VARCHAR(255);
-- Fase 3: Copiar dados (após confirmar que app usa novo nome)
UPDATE usuarios SET email = email_legado;
-- Fase 4: Remover coluna legada (após feature flag desabilitar acesso)
ALTER TABLE usuarios DROP COLUMN email_legado;
Feature flags são cruciais para coordenar a remoção com o deploy do código:
-- Exemplo de verificação de feature flag no código
-- if feature_flags.is_active('novo_email_column'):
-- return usuario.email_novo
-- else:
-- return usuario.email_legado
6. Monitoramento e validação durante a migração
Métricas essenciais a serem monitoradas:
-- Monitorar locks ativos
SELECT pid, locktype, mode, granted
FROM pg_locks
WHERE NOT granted;
-- Verificar latência de queries
SELECT query, mean_time, calls
FROM pg_stat_statements
ORDER BY mean_time DESC LIMIT 10;
-- Comparar checksums entre schemas
SELECT COUNT(*) AS total_registros,
COUNT(*) FILTER (WHERE email IS DISTINCT FROM email_novo) AS divergencias
FROM usuarios;
Estratégias de canary release: aplique a migração primeiro em uma réplica de leitura, valide os resultados e só então promova para o primário.
7. Casos complexos: migrações que alteram chaves e constraints
Migração de chaves primárias requer múltiplos passos:
-- Fase 1: Adicionar nova PK como UNIQUE
ALTER TABLE pedidos ADD COLUMN id_novo BIGSERIAL UNIQUE;
-- Fase 2: Repopular dados e criar nova PK
ALTER TABLE pedidos ADD PRIMARY KEY (id_novo);
-- Fase 3: Remover PK antiga (após verificar dependências)
ALTER TABLE pedidos DROP CONSTRAINT pedidos_pkey;
ALTER TABLE pedidos DROP COLUMN id_antigo;
Para foreign keys, use NOT VALID para evitar locks:
-- Adicionar FK sem validar dados existentes
ALTER TABLE itens_pedido
ADD CONSTRAINT fk_pedido_novo
FOREIGN KEY (pedido_id_novo) REFERENCES pedidos(id_novo)
NOT VALID;
-- Validar posteriormente
ALTER TABLE itens_pedido VALIDATE CONSTRAINT fk_pedido_novo;
Migrações de tipo de coluna seguem o padrão de coluna auxiliar:
-- Criar coluna com novo tipo
ALTER TABLE usuarios ADD COLUMN bio_text TEXT;
-- Copiar dados com conversão
UPDATE usuarios SET bio_text = bio::TEXT WHERE bio_text IS NULL;
-- Trocar nomes
ALTER TABLE usuarios RENAME COLUMN bio TO bio_legado;
ALTER TABLE usuarios RENAME COLUMN bio_text TO bio;
-- Remover coluna legada
ALTER TABLE usuarios DROP COLUMN bio_legado;
Referências
- PostgreSQL Documentation: ALTER TABLE — Documentação oficial sobre operações de ALTER TABLE, incluindo adição de colunas e constraints sem bloqueio
- Migrações de banco de dados sem downtime (AWS Blog) — Guia prático da AWS sobre estratégias Expand-Migrate-Contract e ferramentas de migração
- Zero-downtime schema changes in PostgreSQL (Heap Engineering) — Artigo técnico detalhando técnicas de migração sem downtime no PostgreSQL
- Online schema change for MySQL (GitHub Engineering) — Post do GitHub sobre como realizam migrações de schema no MySQL sem downtime
- Database Migrations with Feature Flags (LaunchDarkly Blog) — Tutorial sobre uso de feature flags para coordenar migrações de schema com deploy de código
- PostgreSQL LISTEN/NOTIFY Documentation — Documentação oficial do sistema de notificação assíncrona do PostgreSQL
- Zero-downtime database migrations (Martin Fowler) — Artigo seminal de Martin Fowler sobre evolução de banco de dados sem downtime