Boas práticas para modelagem de banco de dados relacional

1. Fundamentos da Normalização e Integridade de Dados

1.1. Primeira, segunda e terceira formas normais: quando aplicar e quando quebrar

A normalização é o alicerce da modelagem relacional. A primeira forma normal (1FN) exige que cada coluna contenha valores atômicos. A segunda forma normal (2FN) remove dependências parciais, enquanto a terceira forma normal (3FN) elimina dependências transitivas.

Exemplo de violação da 1FN:

Tabela: pedidos
| pedido_id | cliente   | itens                    |
|-----------|-----------|--------------------------|
| 1         | João      | "camisa, calça, sapato"  |

Correção aplicando 1FN:

Tabela: pedidos
| pedido_id | cliente   |
|-----------|-----------|
| 1         | João      |

Tabela: itens_pedido
| pedido_id | item   |
|-----------|--------|
| 1         | camisa |
| 1         | calça  |
| 1         | sapato |

Quebrar a normalização é aceitável em cenários de performance crítica, como relatórios analíticos, onde joins excessivos comprometem a velocidade de leitura.

1.2. Chaves primárias, estrangeiras e índices únicos

Chaves primárias garantem unicidade e identificam cada linha. Chaves estrangeiras mantêm integridade referencial entre tabelas.

CREATE TABLE clientes (
    cliente_id SERIAL PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    nome VARCHAR(100) NOT NULL
);

CREATE TABLE pedidos (
    pedido_id SERIAL PRIMARY KEY,
    cliente_id INTEGER NOT NULL REFERENCES clientes(cliente_id),
    data_pedido DATE NOT NULL,
    valor_total NUMERIC(10,2)
);

Índices únicos previnem duplicatas em colunas que não são chave primária, como CPF ou email.

1.3. Evitando anomalias de atualização, inserção e exclusão

Anomalias ocorrem quando o design permite inconsistências. Por exemplo, armazenar o nome do cliente repetido em cada pedido causa anomalia de atualização — alterar o nome exige modificar múltiplas linhas.

Modelo anômalo:

Tabela: pedidos
| pedido_id | cliente_nome | produto   | valor |
|-----------|--------------|-----------|-------|
| 1         | João Silva   | Camisa   | 50.00 |
| 2         | João Silva   | Calça    | 80.00 |

Modelo normalizado (3FN):

Tabela: clientes
| cliente_id | nome       |
|------------|------------|
| 1          | João Silva |

Tabela: pedidos
| pedido_id | cliente_id | produto | valor |
|-----------|------------|---------|-------|
| 1         | 1          | Camisa  | 50.00 |
| 2         | 1          | Calça   | 80.00 |

2. Estratégias de Indexação para Performance

2.1. Índices B-tree, compostos e parciais

Índices B-tree são padrão para consultas de igualdade e intervalo. Índices compostos aceleram consultas que filtram por múltiplas colunas.

CREATE INDEX idx_pedidos_cliente_data 
ON pedidos (cliente_id, data_pedido);

Índices parciais são úteis para subconjuntos de dados:

CREATE INDEX idx_pedidos_ativos 
ON pedidos (data_pedido) 
WHERE status = 'ativo';

2.2. Índices de cobertura (covering indexes)

Um covering index contém todas as colunas necessárias para uma consulta, evitando acesso à tabela principal.

CREATE INDEX idx_pedidos_cobertura 
ON pedidos (cliente_id) 
INCLUDE (valor_total, data_pedido);

2.3. Cuidados com superindexação

Cada índice adicional aumenta o custo de operações INSERT, UPDATE e DELETE. Monitore o impacto usando pg_stat_user_indexes no PostgreSQL ou sys.dm_db_index_usage_stats no SQL Server.

3. Modelagem de Relacionamentos Complexos

3.1. Relacionamentos muitos-para-muitos

Tabelas de junção resolvem relacionamentos N:N:

Tabela: estudantes
| estudante_id | nome     |
|--------------|----------|
| 1            | Maria    |

Tabela: cursos
| curso_id | nome      |
|----------|-----------|
| 1        | Matemática|

Tabela: matriculas (junção)
| estudante_id | curso_id | data_matricula |
|--------------|----------|----------------|
| 1            | 1        | 2024-01-15     |

3.2. Hierarquias e árvores

Adjacency list (simples, mas consultas recursivas):

Tabela: categorias
| categoria_id | nome     | parent_id |
|--------------|----------|-----------|
| 1            | Eletrônicos | NULL    |
| 2            | Celulares  | 1        |

Closure tables (consultas eficientes, maior espaço):

Tabela: categorias_fechamento
| ancestor_id | descendant_id | depth |
|-------------|---------------|-------|
| 1           | 1             | 0     |
| 1           | 2             | 1     |

3.3. Herança de dados

Single table inheritance (todos os tipos em uma tabela):

Tabela: veiculos
| veiculo_id | tipo | motor | portas | carga_maxima |
|------------|------|-------|--------|--------------|
| 1          | carro| 2.0   | 4      | NULL         |
| 2          | caminhao| 5.0 | 2      | 5000         |

Class table inheritance (tabela base + tabelas específicas) é mais normalizada, mas exige joins.

4. Tipos de Dados e Restrições Semânticas

4.1. Escolha adequada de tipos

UUIDs são excelentes para sistemas distribuídos, mas ocupam mais espaço que SERIAL. Use TIMESTAMP WITH TIME ZONE para dados temporais globais.

CREATE TABLE usuarios (
    usuario_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    criado_em TIMESTAMPTZ DEFAULT NOW()
);

4.2. Constraints de domínio

CHECK constraints garantem regras de negócio no banco:

CREATE TABLE produtos (
    produto_id SERIAL PRIMARY KEY,
    preco NUMERIC(10,2) CHECK (preco > 0),
    status VARCHAR(20) CHECK (status IN ('ativo', 'inativo', 'descontinuado'))
);

4.3. Uso de tipos JSONB/arrays

JSONB é útil para dados semi-estruturados, mas evite consultas frequentes a campos internos sem índices GIN.

CREATE TABLE logs (
    log_id SERIAL PRIMARY KEY,
    dados JSONB,
    criado_em TIMESTAMPTZ
);

CREATE INDEX idx_logs_dados ON logs USING GIN (dados);

5. Design para Escrita e Leitura Otimizadas

5.1. Modelagem orientada a consultas

Identifique consultas frequentes e modele tabelas para atendê-las. Se relatórios mensais exigem agregações pesadas, considere tabelas de sumarização.

5.2. Desnormalização controlada

Campos derivados como total_pedidos em uma tabela clientes podem ser mantidos via triggers ou atualizações periódicas.

ALTER TABLE clientes ADD COLUMN total_pedidos INTEGER DEFAULT 0;

5.3. Particionamento de tabelas

Particionamento range para dados temporais:

CREATE TABLE vendas (
    venda_id SERIAL,
    data_venda DATE NOT NULL,
    valor NUMERIC(10,2)
) PARTITION BY RANGE (data_venda);

CREATE TABLE vendas_2024 PARTITION OF vendas
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

6. Integridade Transacional e Concorrência

6.1. Níveis de isolamento

Use READ COMMITTED como padrão. SERIALIZABLE garante consistência máxima, mas reduz concorrência.

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
-- operações críticas
COMMIT;

6.2. Transações longas e deadlocks

Mantenha transações curtas e acesse tabelas sempre na mesma ordem para evitar deadlocks.

6.3. Triggers para consistência

Triggers podem manter campos derivados ou validar regras complexas:

CREATE OR REPLACE FUNCTION atualiza_total_pedidos()
RETURNS TRIGGER AS $$
BEGIN
    UPDATE clientes SET total_pedidos = (
        SELECT COUNT(*) FROM pedidos WHERE cliente_id = NEW.cliente_id
    ) WHERE cliente_id = NEW.cliente_id;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_atualiza_total
AFTER INSERT ON pedidos
FOR EACH ROW EXECUTE FUNCTION atualiza_total_pedidos();

7. Evolução do Schema e Migrações

7.1. Migrações sem downtime

Adicione colunas como nullable primeiro, depois preencha dados e só então adicione NOT NULL.

ALTER TABLE clientes ADD COLUMN telefone VARCHAR(20);
UPDATE clientes SET telefone = '0000-0000' WHERE telefone IS NULL;
ALTER TABLE clientes ALTER COLUMN telefone SET NOT NULL;

7.2. Adição de colunas com defaults

No PostgreSQL, adicionar coluna com DEFAULT não bloqueia a tabela:

ALTER TABLE clientes ADD COLUMN ativo BOOLEAN DEFAULT TRUE;

7.3. Deprecação de colunas

Renomeie colunas antigas com prefixo _obsoleto antes de removê-las em versões futuras.

8. Documentação e Padronização do Modelo

8.1. Nomenclatura consistente

Use snake_case para tabelas e colunas, prefixos como idx_ para índices e fk_ para foreign keys.

CREATE TABLE clientes_pf (
    cliente_id SERIAL PRIMARY KEY,
    nome_completo VARCHAR(200)
);

CREATE INDEX idx_clientes_pf_nome ON clientes_pf (nome_completo);

8.2. Diagramas ER e dicionário de dados

Mantenha diagramas atualizados com ferramentas como dbdiagram.io ou draw.io. Documente cada coluna com descrição e tipo esperado.

8.3. Revisões de schema

Integre revisões de schema no pull request, com checklist de verificação de normalização, índices e migrações.

Referências