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
- PostgreSQL Documentation: Data Definition — Documentação oficial sobre criação de tabelas, constraints e índices no PostgreSQL
- Use the Index, Luke! — Guia prático sobre estratégias de indexação para bancos relacionais
- Database Normalization: A Practical Guide — Tutorial passo a passo sobre as formas normais com exemplos SQL
- Martín Fowler: Patterns of Enterprise Application Architecture — Catálogo de padrões de arquitetura incluindo herança de dados e tabelas de junção
- SQL Performance Explained — Livro online sobre otimização de consultas e modelagem para performance
- dbdiagram.io Documentation — Ferramenta para criação e compartilhamento de diagramas ER com sintaxe DSL
- Migrações sem Downtime no PostgreSQL — Artigo técnico sobre estratégias de migração de schema sem interrupção de serviço