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 é:

  1. Adicionar a coluna como nullable (operação praticamente instantânea no PostgreSQL):
ALTER TABLE pedidos ADD COLUMN status VARCHAR(20);
  1. Preencher os valores em lote, fora do horário de pico:
UPDATE pedidos SET status = 'pendente' WHERE status IS NULL;
  1. 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:

  1. Criar a nova tabela particionada
  2. Criar uma view que unifica ambas as tabelas
  3. Usar triggers para rotear novos dados para a tabela particionada
  4. 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 EXPLAIN e monitore locks com pg_locks ou SHOW 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