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:

  1. Volume de dados — Grandes volumes favorecem desnormalização para evitar joins caros.
  2. Padrão de acesso — Muitas leituras vs. muitas escritas determinam a abordagem.
  3. 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