Desnormalização: quando e por quê

1. O que é Desnormalização e Por Que Ela Existe?

Desnormalização é o processo deliberado de introduzir redundância em um banco de dados que antes estava normalizado. Diferentemente da má modelagem — que ocorre por desconhecimento ou descuido — a desnormalização intencional é uma decisão arquitetural baseada em requisitos de performance.

O objetivo principal é reduzir o número de junções (JOINs) necessárias para responder a consultas frequentes. Em bancos de dados relacionais, a normalização visa eliminar redundância e garantir consistência, mas isso frequentemente exige que dados espalhados por várias tabelas sejam reunidos via JOINs. A desnormalização sacrifica parte dessa pureza normalizada em troca de velocidade de leitura.

A diferença fundamental entre má modelagem e desnormalização está no controle: na desnormalização, você sabe exatamente quais dados estão duplicados, por que foram duplicados e como a consistência será mantida.

2. O Custo das Junções (JOINs) em Ambientes de Alto Volume

JOINs são operações caras. Cada junção adicional exige que o banco de dados compare linhas entre tabelas, muitas vezes usando índices, hash tables ou mergesort. Em uma consulta típica de e-commerce, você pode precisar juntar:

  • pedidos com clientes (para obter o nome)
  • pedidos com itens_pedido (para somar totais)
  • itens_pedido com produtos (para obter descrições)
  • produtos com categorias (para agrupar por departamento)

Isso resulta em 4 JOINs. Em uma tabela normalizada, cada JOIN adiciona latência. Em um sistema OLTP com milhares de requisições por segundo, essa latência se acumula.

Exemplo de consulta normalizada (4 JOINs):

SELECT c.nome, SUM(ip.quantidade * p.preco) AS total_gasto
FROM clientes c
JOIN pedidos pe ON c.id = pe.cliente_id
JOIN itens_pedido ip ON pe.id = ip.pedido_id
JOIN produtos p ON ip.produto_id = p.id
WHERE c.uf = 'SP'
GROUP BY c.nome;

Versão desnormalizada (0 JOINs):

SELECT nome_cliente, total_gasto
FROM clientes_resumo
WHERE uf = 'SP';

Em testes reais com 10 milhões de registros, a versão desnormalizada pode ser 5 a 20 vezes mais rápida, dependendo da indexação e da carga do sistema.

3. Cenários Típicos para Aplicar Desnormalização

Data Warehousing e Star Schemas

O modelo estrela (star schema) é a forma mais clássica de desnormalização controlada. As tabelas dimensão são desnormalizadas propositalmente para conter todos os atributos descritivos, eliminando JOINs em cascata. A tabela fato, por sua vez, contém chaves estrangeiras e medidas.

Sistemas de Leitura Intensiva

Dashboards em tempo real, relatórios gerenciais e feeds de dados consomem consultas de leitura constantemente. Nesses sistemas, a taxa de leitura vs. escrita pode chegar a 100:1. Desnormalizar as tabelas mais consultadas reduz drasticamente o tempo de resposta.

Aplicações com Latência Extremamente Baixa

Sistemas de recomendação, motores de busca e plataformas de trading exigem respostas em milissegundos. Cada JOIN adiciona latência imprevisível, tornando a desnormalização uma necessidade, não uma opção.

4. Padrões Comuns de Desnormalização

Pré-cálculo de Agregados

Armazenar totais, médias e contagens diretamente na tabela pai evita recálculos constantes.

-- Tabela normalizada: calcular total a cada consulta
SELECT cliente_id, SUM(valor) FROM pedidos GROUP BY cliente_id;

-- Tabela desnormalizada: coluna total_gasto já calculada
SELECT cliente_id, total_gasto FROM clientes;

Incorporação de Atributos

Incluir o nome do cliente na tabela de pedidos elimina um JOIN frequente:

CREATE TABLE pedidos (
    id SERIAL PRIMARY KEY,
    cliente_id INT,
    nome_cliente VARCHAR(200),  -- desnormalizado
    data_pedido DATE,
    valor_total DECIMAL(10,2)
);

Duplicação para Isolamento de Consultas

Criar cópias específicas de dados para diferentes contextos (ex.: uma tabela produtos_vendas com dados otimizados para o módulo de vendas, separada da tabela produtos_estoque).

5. Os Riscos e Desvantagens da Desnormalização

Anomalias de Atualização

Se um cliente muda de nome, você precisa atualizar todas as tabelas que contêm nome_cliente duplicado. Esquecer uma atualização gera inconsistência.

Aumento de Armazenamento

Duplicar colunas como nome_cliente (VARCHAR(200)) em uma tabela com 10 milhões de pedidos adiciona aproximadamente 2 GB de armazenamento desnecessário.

Complexidade no Código da Aplicação

O código precisa gerenciar múltiplas fontes de verdade. Cada operação de escrita deve garantir que todas as cópias sejam atualizadas, aumentando a superfície de bugs.

6. Estratégias para Manter a Consistência em Dados Desnormalizados

Gatilhos (Triggers)

Triggers no banco de dados podem sincronizar automaticamente colunas desnormalizadas:

CREATE OR REPLACE FUNCTION atualiza_nome_cliente()
RETURNS TRIGGER AS $$
BEGIN
    UPDATE pedidos SET nome_cliente = NEW.nome
    WHERE cliente_id = NEW.id;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_atualiza_nome_cliente
AFTER UPDATE OF nome ON clientes
FOR EACH ROW EXECUTE FUNCTION atualiza_nome_cliente();

Jobs de Reconciliação

Para dados onde a consistência eventual é aceitável, jobs ETL noturnos podem corrigir divergências:

-- Job diário: reconcilia nomes de clientes na tabela de pedidos
UPDATE pedidos p
SET nome_cliente = c.nome
FROM clientes c
WHERE p.cliente_id = c.id
  AND p.nome_cliente <> c.nome;

Transações Distribuídas

Em sistemas críticos, use transações que atualizam todas as tabelas afetadas atomicamente:

BEGIN;
UPDATE clientes SET nome = 'Novo Nome' WHERE id = 123;
UPDATE pedidos SET nome_cliente = 'Novo Nome' WHERE cliente_id = 123;
COMMIT;

7. Quando NÃO Desnormalizar (Alternativas Modernas)

Índices Parciais e de Expressão

Antes de desnormalizar, considere índices que aceleram JOINs sem duplicar dados:

CREATE INDEX idx_pedidos_cliente_data
ON pedidos (cliente_id, data_pedido DESC);

Materialized Views

No PostgreSQL, views materializadas oferecem o melhor dos dois mundos: dados pré-calculados com atualização sob demanda:

CREATE MATERIALIZED VIEW resumo_clientes AS
SELECT c.id, c.nome, SUM(p.valor) AS total_gasto
FROM clientes c
JOIN pedidos p ON c.id = p.cliente_id
GROUP BY c.id, c.nome;

-- Atualizar quando necessário
REFRESH MATERIALIZED VIEW resumo_clientes;

Cache na Aplicação

Redis ou Memcached podem armazenar resultados de consultas frequentes, evitando a complexidade da desnormalização no banco:

# Pseudocódigo: cache na aplicação
dados = redis.get("resumo_cliente:123")
if not dados:
    dados = banco.consulta("SELECT ... WHERE cliente_id = 123")
    redis.set("resumo_cliente:123", dados, ttl=300)

8. Checklist: Decisão de Desnormalizar

Perguntas-chave:

  1. A consulta que você quer acelerar representa mais de 70% do tráfego de leitura?
  2. A taxa de leitura vs. escrita é superior a 50:1?
  3. Você pode tolerar consistência eventual (segundos/minutos)?
  4. O ganho de performance justifica o aumento de armazenamento?
  5. Você tem mecanismos de sincronização (triggers, jobs) implementados?

Métricas para monitorar:

  • Tempo médio de consulta antes e depois
  • Taxa de erros de consistência (dados divergentes)
  • Espaço em disco utilizado
  • Throughput de consultas por segundo

Exemplo de decisão documentada:

Contexto Decisão Justificativa
Dashboard de vendas em tempo real Desnormalizar 1000 leituras/s, 5 escritas/s
Cadastro de clientes Manter normalizado Muitas atualizações, consistência crítica
Relatório mensal de faturamento Materialized view Consistência eventual aceitável

A desnormalização não é uma falha de modelagem — é uma ferramenta. Use-a com critério, documente cada decisão e monitore os resultados.

Referências