Schema evolution: estratégias para mudar estruturas em produção
1. Fundamentos da Evolução de Schema
Schema evolution é o processo de modificar a estrutura de um banco de dados enquanto ele está em produção, sem interromper o serviço ou causar perda de dados. Em sistemas modernos, essa prática é inevitável: requisitos de negócio mudam, novos campos são necessários, e otimizações de desempenho exigem reorganizações.
A principal distinção que um engenheiro de dados precisa fazer é entre mudanças breaking e non-breaking. Mudanças non-breaking (como adicionar uma coluna nullable) podem ser aplicadas sem afetar aplicações existentes. Mudanças breaking (como remover uma coluna ou alterar seu tipo) exigem planejamento cuidadoso e múltiplas fases.
O princípio fundamental é a compatibilidade retroativa: qualquer mudança deve permitir que versões antigas do código continuem funcionando até que sejam atualizadas.
2. Versionamento e Gerenciamento de Migrações
Toda migração deve ser versionada, ordenada e rastreável. A abordagem mais comum é usar scripts SQL numerados sequencialmente:
-- V001__criar_tabela_usuarios.sql
CREATE TABLE usuarios (
id SERIAL PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL
);
-- V002__adicionar_telefone.sql
ALTER TABLE usuarios ADD COLUMN telefone VARCHAR(20);
Ferramentas como Flyway e Liquibase automatizam esse processo, mantendo uma tabela de controle (flyway_schema_history ou DATABASECHANGELOG) que registra quais migrações foram aplicadas.
Para rollback seguro, cada migração deve ter seu script reverso correspondente:
-- V002__adicionar_telefone.sql (forward)
ALTER TABLE usuarios ADD COLUMN telefone VARCHAR(20);
-- V002__rollback.sql (reverse)
ALTER TABLE usuarios DROP COLUMN telefone;
3. Adicionando Novas Colunas com Segurança
Adicionar uma coluna parece simples, mas pode causar problemas graves em tabelas grandes. No PostgreSQL, adicionar uma coluna com DEFAULT não-nulo exige reescrever toda a tabela, gerando locks prolongados:
-- PROBLEMÁTICO em tabelas grandes (> 1 milhão de linhas)
ALTER TABLE pedidos ADD COLUMN status VARCHAR(20) DEFAULT 'pendente' NOT NULL;
A estratégia segura é:
- Adicionar a coluna como nullable (operação praticamente instantânea no PostgreSQL):
ALTER TABLE pedidos ADD COLUMN status VARCHAR(20);
- Preencher os valores em lote, fora do horário de pico:
UPDATE pedidos SET status = 'pendente' WHERE status IS NULL;
- Adicionar a constraint NOT NULL após validar que não há nulos:
ALTER TABLE pedidos ALTER COLUMN status SET NOT NULL;
No MySQL, colunas com DEFAULT podem causar locks de metadados mesmo em operações simples. A alternativa é criar uma nova tabela e migrar os dados gradualmente.
4. Removendo ou Renomeando Colunas e Tabelas
Remover uma coluna ou tabela exige o padrão expandir-contratar (expand-contract):
Fase 1: Expandir — Adicione a nova estrutura mantendo a antiga.
Fase 2: Migrar — Atualize todas as aplicações para usar a nova estrutura.
Fase 3: Contratar — Remova a estrutura antiga.
Para renomear colunas com segurança, use views como ponte:
-- 1. Criar view com o novo nome
CREATE VIEW usuarios_v2 AS
SELECT id, nome AS nome_completo, email FROM usuarios;
-- 2. Aplicações migram para usar a view
-- 3. Quando todas as aplicações estiverem atualizadas:
ALTER TABLE usuarios RENAME COLUMN nome TO nome_completo;
DROP VIEW usuarios_v2;
Cuidado especial com dependências ocultas: stored procedures, triggers, relatórios em dashboards e ORMs que mapeiam colunas por nome. Uma busca completa no código-fonte é essencial antes de qualquer remoção.
5. Alterando Tipos de Dados e Restrições
Alterar o tipo de uma coluna requer estratégia cuidadosa. Por exemplo, migrar de INT para BIGINT:
-- 1. Adicionar nova coluna com o tipo desejado
ALTER TABLE transacoes ADD COLUMN valor_novo BIGINT;
-- 2. Atualizar em lote (com validação de overflow)
UPDATE transacoes SET valor_novo = valor WHERE valor IS NOT NULL;
-- 3. Remover coluna antiga e renomear
ALTER TABLE transacoes DROP COLUMN valor;
ALTER TABLE transacoes RENAME COLUMN valor_novo TO valor;
Para adicionar constraints como NOT NULL ou CHECK sem downtime, valide os dados existentes primeiro:
-- Validar antes de aplicar a constraint
SELECT COUNT(*) FROM usuarios WHERE email IS NULL;
-- Se não houver nulos, aplicar é seguro
ALTER TABLE usuarios ALTER COLUMN email SET NOT NULL;
No PostgreSQL, constraints CHECK podem ser adicionadas com NOT VALID e depois validadas em segundo plano:
ALTER TABLE pedidos ADD CONSTRAINT check_valor_positivo
CHECK (valor > 0) NOT VALID;
-- Validar em lote (não bloqueia escritas)
ALTER TABLE pedidos VALIDATE CONSTRAINT check_valor_positivo;
6. Particionamento e Reorganização de Tabelas
Migrar uma tabela sem partição para uma tabela particionada é um dos desafios mais complexos. A estratégia com downtime zero envolve:
- Criar a nova tabela particionada
- Criar uma view que unifica ambas as tabelas
- Usar triggers para rotear novos dados para a tabela particionada
- Migrar dados antigos em lote
-- 1. Criar tabela particionada
CREATE TABLE pedidos_particionado (
id SERIAL,
data_pedido DATE NOT NULL,
valor DECIMAL(10,2),
PRIMARY KEY (id, data_pedido)
) PARTITION BY RANGE (data_pedido);
-- 2. Criar partições
CREATE TABLE pedidos_2024 PARTITION OF pedidos_particionado
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');
-- 3. Trigger para rotear novos inserts
CREATE OR REPLACE FUNCTION inserir_pedido_particionado()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO pedidos_particionado VALUES (NEW.*);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_rotear_pedido
INSTEAD OF INSERT ON pedidos_view
FOR EACH ROW EXECUTE FUNCTION inserir_pedido_particionado();
7. Testes e Validação de Migrações
Nenhuma migração deve ir para produção sem ser testada em ambiente de staging idêntico. O checklist mínimo inclui:
- Teste de locking: Execute
EXPLAINe monitore locks compg_locksouSHOW PROCESSLIST - Teste de desempenho: Meça o tempo de execução em staging com volume de dados similar ao de produção
- Teste de rollback: Verifique se o script reverso funciona e restaura o estado anterior
Monitoramento pós-migração é igualmente crítico:
-- PostgreSQL: verificar consultas lentas após migração
SELECT query, calls, total_time / calls AS avg_time
FROM pg_stat_statements
WHERE query LIKE '%pedidos%'
ORDER BY avg_time DESC;
Configure alertas para erros de aplicação, aumento de latência e deadlocks nas primeiras horas após a migração.
Referências
- Flyway Documentation - Migrations — Documentação oficial do Flyway sobre versionamento e execução de migrações SQL
- PostgreSQL ALTER TABLE Documentation — Referência completa sobre operações ALTER TABLE e seus comportamentos de locking
- Liquibase - Best Practices for Database Schema Changes — Guia de boas práticas do Liquibase para mudanças de schema em produção
- MySQL ALTER TABLE Online DDL — Documentação do MySQL sobre operações DDL online e seus impactos de performance
- Schema Evolution in Distributed Systems (Martin Fowler) — Artigo clássico de Martin Fowler sobre estratégias de evolução de schema em sistemas distribuídos
- PostgreSQL Partitioning Guide — Documentação oficial sobre particionamento de tabelas no PostgreSQL
- Expanding-Contract Pattern for Database Migrations — Guia prático do Prisma sobre o padrão expandir-contratar para migrações seguras