Modelagem de dados: normalização vs. desnormalização
1. Introdução aos conceitos fundamentais de modelagem de dados
A modelagem de dados é a arte de estruturar informações de forma que reflitam corretamente o mundo real e atendam aos requisitos de um sistema. Em bancos relacionais, o foco está na integridade referencial e na redução de redundâncias. Em bancos NoSQL, a prioridade costuma ser a escalabilidade horizontal e a velocidade de leitura.
A normalização busca eliminar duplicações e garantir consistência através da decomposição de tabelas em estruturas menores e mais coesas. Cada dado é armazenado uma única vez, e referências são feitas por chaves estrangeiras.
A desnormalização, por outro lado, introduz redundância controlada para evitar joins complexos e acelerar consultas. Dados que seriam obtidos por múltiplas tabelas são agrupados em uma única estrutura, sacrificando espaço de armazenamento em prol da performance.
2. Normalização: princípios e formas normais
Primeira Forma Normal (1FN)
Uma tabela está na 1FN quando cada atributo contém apenas valores atômicos (indivisíveis) e existe uma chave primária que identifica unicamente cada registro.
-- Tabela NÃO normalizada (viola 1FN)
CREATE TABLE pedidos_errado (
id_pedido INT,
cliente VARCHAR(100),
itens VARCHAR(500) -- "camiseta, calça, sapato" (valores múltiplos)
);
-- Tabela na 1FN
CREATE TABLE pedidos_1fn (
id_pedido INT,
id_item INT,
cliente VARCHAR(100),
item VARCHAR(100),
PRIMARY KEY (id_pedido, id_item)
);
Segunda Forma Normal (2FN)
Uma tabela está na 2FN se está na 1FN e todos os atributos não-chave dependem completamente da chave primária (não apenas de parte dela). Isso é relevante quando a chave primária é composta.
-- Tabela NÃO normalizada (viola 2FN)
CREATE TABLE pedidos_itens (
id_pedido INT,
id_item INT,
nome_item VARCHAR(100), -- depende apenas de id_item, não de id_pedido
quantidade INT,
PRIMARY KEY (id_pedido, id_item)
);
-- Tabela na 2FN (nome_item movido para tabela separada)
CREATE TABLE itens (
id_item INT PRIMARY KEY,
nome_item VARCHAR(100)
);
CREATE TABLE pedidos_itens_2fn (
id_pedido INT,
id_item INT,
quantidade INT,
PRIMARY KEY (id_pedido, id_item),
FOREIGN KEY (id_item) REFERENCES itens(id_item)
);
Terceira Forma Normal (3FN)
Uma tabela está na 3FN se está na 2FN e nenhum atributo não-chave depende transitivamente de outro atributo não-chave.
-- Tabela NÃO normalizada (viola 3FN)
CREATE TABLE pedidos_errado_3fn (
id_pedido INT PRIMARY KEY,
id_cliente INT,
nome_cliente VARCHAR(100), -- depende de id_cliente, não de id_pedido
cidade_cliente VARCHAR(100) -- também depende de id_cliente
);
-- Tabela na 3FN
CREATE TABLE clientes (
id_cliente INT PRIMARY KEY,
nome_cliente VARCHAR(100),
cidade_cliente VARCHAR(100)
);
CREATE TABLE pedidos_3fn (
id_pedido INT PRIMARY KEY,
id_cliente INT,
FOREIGN KEY (id_cliente) REFERENCES clientes(id_cliente)
);
3. Desnormalização: estratégias e casos de uso
Duplicação controlada de dados
A desnormalização pode ser aplicada para armazenar campos derivados ou frequentemente consultados junto com os dados principais.
-- Schema normalizado (joins necessários)
SELECT p.id_pedido, c.nome, c.cidade
FROM pedidos p
JOIN clientes c ON p.id_cliente = c.id_cliente;
-- Schema desnormalizado (dados do cliente no pedido)
CREATE TABLE pedidos_desnormalizado (
id_pedido INT PRIMARY KEY,
id_cliente INT,
nome_cliente VARCHAR(100),
cidade_cliente VARCHAR(100),
total_pedido DECIMAL(10,2)
);
Tabelas agregadas e sumarizadas
Para relatórios de desempenho, tabelas pré-calculadas armazenam totais e médias, evitando scans completos.
-- Tabela de sumarização para relatórios mensais
CREATE TABLE resumo_vendas_mensal (
ano INT,
mes INT,
total_vendas DECIMAL(12,2),
total_pedidos INT,
ticket_medio DECIMAL(10,2),
PRIMARY KEY (ano, mes)
);
Desnormalização em bancos NoSQL
No MongoDB, documentos aninhados são a forma mais comum de desnormalização.
-- Documento desnormalizado no MongoDB
{
"_id": "pedido_001",
"cliente": {
"nome": "João Silva",
"cidade": "São Paulo"
},
"itens": [
{ "produto": "Camiseta", "quantidade": 2, "preco": 49.90 },
{ "produto": "Calça", "quantidade": 1, "preco": 89.90 }
],
"total": 189.70
}
4. Comparação prática: quando usar cada abordagem
| Critério | Normalização | Desnormalização |
|---|---|---|
| Frequência de escritas | Ideal (evita anomalias) | Cuidado (atualizações complexas) |
| Frequência de leituras | Joins podem ser lentos | Leituras rápidas e simples |
| Consistência | Forte (ACID) | Eventual (BASE) |
| Espaço de armazenamento | Mínimo necessário | Maior (dados duplicados) |
Cenário 1: Sistema bancário com muitas transações — normalização é obrigatória para garantir consistência imediata.
Cenário 2: Catálogo de produtos com milhões de leituras por segundo — desnormalização em cache ou NoSQL é preferível.
5. Exemplos de código: modelagem normalizada vs. desnormalizada
Exemplo 1: Schema normalizado (tabelas separadas com joins)
-- Tabelas normalizadas
CREATE TABLE clientes (
id_cliente INT PRIMARY KEY,
nome VARCHAR(100),
email VARCHAR(100)
);
CREATE TABLE pedidos (
id_pedido INT PRIMARY KEY,
id_cliente INT,
data_pedido DATE,
FOREIGN KEY (id_cliente) REFERENCES clientes(id_cliente)
);
CREATE TABLE itens_pedido (
id_pedido INT,
id_produto INT,
quantidade INT,
preco_unitario DECIMAL(10,2),
PRIMARY KEY (id_pedido, id_produto)
);
-- Consulta com joins
SELECT c.nome, p.id_pedido, ip.quantidade, ip.preco_unitario
FROM clientes c
JOIN pedidos p ON c.id_cliente = p.id_cliente
JOIN itens_pedido ip ON p.id_pedido = ip.id_pedido
WHERE p.data_pedido >= '2024-01-01';
Exemplo 2: Schema desnormalizado (documento único no Elasticsearch)
-- Documento indexado no Elasticsearch
{
"pedido_id": "PED123",
"cliente": {
"nome": "Maria Oliveira",
"email": "maria@email.com"
},
"data_pedido": "2024-03-15",
"itens": [
{"produto": "Notebook", "quantidade": 1, "preco": 4500.00},
{"produto": "Mouse", "quantidade": 2, "preco": 89.90}
],
"total": 4679.80
}
-- Consulta no Elasticsearch (sem joins)
GET /pedidos/_search
{
"query": {
"range": {
"data_pedido": {
"gte": "2024-01-01"
}
}
}
}
Exemplo 3: Atualizações e anomalias em dados desnormalizados
-- Problema: atualizar nome do cliente em múltiplos pedidos desnormalizados
UPDATE pedidos_desnormalizado
SET nome_cliente = 'João Silva Atualizado'
WHERE id_cliente = 1;
-- Se houver 1000 pedidos do mesmo cliente, são 1000 linhas para atualizar
-- Risco de inconsistência se a atualização falhar parcialmente
-- Solução com consistência eventual (MongoDB)
db.pedidos.updateMany(
{ "cliente.id": 1 },
{ $set: { "cliente.nome": "João Silva Atualizado" } }
);
6. Padrões híbridos e boas práticas modernas
Materialized views e índices
Bancos relacionais modernos permitem combinar os dois mundos através de materialized views (visões materializadas) que armazenam resultados de joins pré-calculados.
-- Materialized view no PostgreSQL
CREATE MATERIALIZED VIEW pedidos_resumo AS
SELECT
c.id_cliente,
c.nome AS nome_cliente,
COUNT(p.id_pedido) AS total_pedidos,
SUM(ip.quantidade * ip.preco_unitario) AS valor_total
FROM clientes c
JOIN pedidos p ON c.id_cliente = p.id_cliente
JOIN itens_pedido ip ON p.id_pedido = ip.id_pedido
GROUP BY c.id_cliente, c.nome;
-- Refresh programado
REFRESH MATERIALIZED VIEW pedidos_resumo;
Denormalização parcial em bancos relacionais
Colunas calculadas (generadas) podem armazenar valores derivados automaticamente.
CREATE TABLE pedidos (
id_pedido INT PRIMARY KEY,
quantidade INT,
preco_unitario DECIMAL(10,2),
total_calculado DECIMAL(10,2) GENERATED ALWAYS AS (quantidade * preco_unitario) STORED
);
Normalização em bancos NoSQL
Mesmo em bancos NoSQL, para dados altamente relacionados (como grafos), a normalização pode ser benéfica.
-- No Neo4j, relacionamentos são normalizados naturalmente
CREATE (c:Cliente {nome: "Ana", cidade: "Rio"})
CREATE (p:Pedido {id: "PED456", data: "2024-03-20"})
CREATE (c)-[:FEZ]->(p)
CREATE (p)-[:CONTEM]->(i:Item {produto: "Livro", quantidade: 3})
7. Conclusão e recomendações finais
A escolha entre normalização e desnormalização deve considerar:
- Volume de dados — Grandes volumes favorecem desnormalização para evitar joins caros.
- Padrão de acesso — Muitas leituras vs. muitas escritas determinam a abordagem.
- Requisitos de consistência — ACID exige normalização; BASE permite desnormalização.
Checklist prático:
- [ ] O sistema exige consistência imediata? → Prefira normalização.
- [ ] As consultas são previsíveis e repetitivas? → Considere desnormalização.
- [ ] O volume de escritas é baixo? → Desnormalização é mais segura.
- [ ] Há necessidade de relatórios em tempo real? → Materialized views ou tabelas agregadas.
Tendências atuais: A modelagem orientada a domínio (DDD) e o uso de schemas flexíveis (JSON, documentos) permitem que times escolham o nível de normalização adequado para cada agregado, combinando o melhor dos dois mundos.
Referências
- Database Normalization Explained (GeeksforGeeks) — Guia completo sobre as formas normais com exemplos práticos em SQL.
- MongoDB Schema Design: Data Modeling Best Practices — Documentação oficial sobre modelagem de dados, incluindo embedded documents (desnormalização).
- Materialized Views in PostgreSQL (PostgreSQL Documentation) — Como criar e gerenciar views materializadas para equilibrar normalização e performance.
- Denormalization vs. Normalization in Data Warehousing (DataCamp) — Comparação detalhada com foco em data warehouses e BI.
- Elasticsearch Data Modeling: Denormalization Best Practices — Guia oficial do Elasticsearch sobre modelagem desnormalizada para buscas rápidas.