Database refactoring: renomear, dividir e reorganizar com segurança

1. Fundamentos do Database Refactoring

Database refactoring é o processo de alterar incrementalmente o esquema de um banco de dados em produção, preservando seu comportamento funcional e sem causar interrupções. Diferente do refactoring de código, que permite múltiplas versões simultâneas via branches, o banco de dados é um recurso compartilhado e centralizado. Qualquer alteração mal planejada pode quebrar consultas, stored procedures, relatórios ou até mesmo causar perda de dados.

Os riscos mais comuns incluem:
- Quebra de queries: aplicações que esperam colunas ou nomes de tabelas antigos
- Perda de dados: migrações com DELETE ou DROP sem backup prévio
- Downtime não planejado: locks prolongados durante ALTER TABLE em tabelas grandes

A abordagem segura exige planejamento, scripts de reversão e validação de dependências.

2. Renomeação Segura de Objetos

Renomear tabelas ou colunas exige cautela. A estratégia recomendada é "renomear e manter sinônimos" usando views.

Exemplo: Renomear coluna name para full_name

Passo 1: Criar view de compatibilidade antes de renomear

CREATE VIEW users_v AS
SELECT id, name AS full_name, email, created_at
FROM users;

Passo 2: Renomear a coluna original

ALTER TABLE users RENAME COLUMN name TO full_name;

Passo 3: Atualizar a view para espelhar a nova estrutura

CREATE OR REPLACE VIEW users_v AS
SELECT id, full_name, email, created_at
FROM users;

Plano de rollback:

-- Reverter renomeação
ALTER TABLE users RENAME COLUMN full_name TO name;
DROP VIEW IF EXISTS users_v;

Validação de dependências: use consultas ao information_schema para localizar stored procedures, triggers ou relatórios que referenciam a coluna antiga:

SELECT routine_name, routine_definition
FROM information_schema.routines
WHERE routine_definition ILIKE '%name%';

3. Divisão de Tabelas

Divisão Vertical

Separa colunas grandes (BLOBs, textos longos) das colunas de acesso frequente.

Exemplo: Dividir tabela products em products_core e products_details

-- Criar tabela com colunas de uso frequente
CREATE TABLE products_core AS
SELECT id, name, price, category_id
FROM products;

-- Criar tabela com colunas grandes
CREATE TABLE products_details AS
SELECT id, description, image_data
FROM products;

-- View unificadora
CREATE VIEW products_v AS
SELECT c.id, c.name, c.price, c.category_id, d.description, d.image_data
FROM products_core c
JOIN products_details d ON c.id = d.id;

Divisão Horizontal (Particionamento)

Para tabelas grandes, use particionamento por data.

Exemplo: Particionar tabela orders por ano

-- Criar tabela particionada
CREATE TABLE orders (
    id INT,
    customer_id INT,
    order_date DATE,
    total DECIMAL(10,2)
) PARTITION BY RANGE (YEAR(order_date));

-- Criar partições
CREATE TABLE orders_2023 PARTITION OF orders
FOR VALUES FROM (2023) TO (2024);

CREATE TABLE orders_2024 PARTITION OF orders
FOR VALUES FROM (2024) TO (2025);

-- Migrar dados
INSERT INTO orders SELECT * FROM legacy_orders;

View unificadora para migração gradual:

CREATE VIEW all_orders AS
SELECT * FROM orders_active
UNION ALL
SELECT * FROM orders_archive;

4. Reorganização de Esquema

Merge de Tabelas Redundantes

Consolidar duas tabelas que armazenam dados similares.

-- Validar integridade antes do merge
SELECT id FROM customers_premium
EXCEPT
SELECT id FROM customers_standard;

-- Merge com transação
BEGIN;
INSERT INTO customers (id, name, email, type)
SELECT id, name, email, 'premium' FROM customers_premium;

INSERT INTO customers (id, name, email, type)
SELECT id, name, email, 'standard' FROM customers_standard;

DROP TABLE customers_premium;
DROP TABLE customers_standard;
COMMIT;

Split de Tabelas Superlotadas

Separar dados históricos em tabela de arquivo.

BEGIN;
-- Criar tabela filha
CREATE TABLE orders_archive (LIKE orders);

-- Mover dados antigos
INSERT INTO orders_archive
SELECT * FROM orders WHERE order_date < '2023-01-01';

-- Remover da tabela principal
DELETE FROM orders WHERE order_date < '2023-01-01';
COMMIT;

Normalização Controlada

Separar endereços em tabela própria.

-- Criar tabela addresses
CREATE TABLE addresses (
    id SERIAL PRIMARY KEY,
    user_id INT REFERENCES users(id),
    street VARCHAR(200),
    city VARCHAR(100),
    zip_code VARCHAR(20)
);

-- Migrar dados
INSERT INTO addresses (user_id, street, city, zip_code)
SELECT id, street, city, zip_code FROM users;

-- Remover colunas da tabela original
ALTER TABLE users DROP COLUMN street,
                 DROP COLUMN city,
                 DROP COLUMN zip_code;

5. Estratégias de Migração com Zero Downtime

O padrão "Expand-Migrate-Contract" (EMC) é a abordagem mais segura.

Fase 1: Expand

Adicione a nova estrutura sem remover a antiga.

-- Adicionar nova coluna
ALTER TABLE users ADD COLUMN full_name VARCHAR(200);

-- Manter coluna antiga
-- name continua existindo

Fase 2: Migrate

Sincronize dados usando triggers ou CDC.

-- Trigger para manter sincronia
CREATE OR REPLACE FUNCTION sync_full_name()
RETURNS TRIGGER AS $$
BEGIN
    NEW.full_name := NEW.name;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_sync_full_name
BEFORE INSERT OR UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION sync_full_name();

Fase 3: Contract

Remova a estrutura antiga após validação.

-- Após confirmar que nenhuma aplicação usa a coluna antiga
ALTER TABLE users DROP COLUMN name;
DROP TRIGGER trg_sync_full_name;

6. Ferramentas e Automação

Ferramentas open-source para versionamento de esquemas:

  • Sqitch: baseado em tags, permite rollback granular
  • Flyway: migrations SQL versionadas com checksum
  • Liquibase: suporte a XML, YAML e JSON para definição de mudanças

Exemplo de script de validação pós-refactoring:

-- Verificar consistência de dados
SELECT COUNT(*) FROM orders
WHERE NOT EXISTS (SELECT 1 FROM customers WHERE customers.id = orders.customer_id);

-- Verificar performance
EXPLAIN ANALYZE SELECT * FROM orders WHERE order_date > '2024-01-01';

Integração com CI/CD:

# .gitlab-ci.yml
stages:
  - test-migration
  - deploy

test-migration:
  stage: test-migration
  script:
    - sqitch deploy db:pg://test-db
    - pgbench -c 10 -t 1000 -f queries.sql

7. Casos Práticos e Exemplos de Código

Exemplo 1: Renomear coluna com view de compatibilidade

Já demonstrado na seção 2.

Exemplo 2: Dividir tabela orders com particionamento

-- Criar tabela particionada
CREATE TABLE orders (
    id INT,
    customer_id INT,
    order_date DATE,
    total DECIMAL(10,2)
) PARTITION BY RANGE (order_date);

-- Partições mensais
CREATE TABLE orders_2024_01 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

CREATE TABLE orders_2024_02 PARTITION OF orders
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

-- Migrar dados
INSERT INTO orders SELECT * FROM legacy_orders;

Exemplo 3: Reorganizar tabela users separando endereço

Já demonstrado na seção 4 (Normalização Controlada).

8. Monitoramento e Rollback em Produção

Métricas de sucesso

  • Tempo de query: comparar antes e depois do refactoring
  • Taxa de erro: monitorar logs de aplicação
  • Locks no banco: usar pg_stat_activity para detectar bloqueios
SELECT pid, state, wait_event, query
FROM pg_stat_activity
WHERE wait_event IS NOT NULL;

Plano de contingência

Script de rollback rápido:

-- rollback_rename_column.sql
BEGIN;
ALTER TABLE users RENAME COLUMN full_name TO name;
DROP VIEW IF EXISTS users_v;
COMMIT;

Lições aprendidas

Documente cada refactoring com:
- Data e hora da execução
- Scripts aplicados (forward e rollback)
- Resultados dos testes de performance
- Problemas encontrados e soluções

Referências