Curso
Escrevibastante sobre normalização e por que ela é uma base tão confiável para a integridade dos dados. Não estou me contradizendo aqui. Ainda acho que um esquema bem normalizado é o ponto de partida certo para a maioria dos sistemas transacionais. O que este artigo aborda é a escolha deliberada de ir contra essa pureza em situações bem específicas, onde o desempenho de leitura é mais importante do que a forma normal estrita.
Desnormalização não é pular a normalização. É uma otimização de desempenho que você aplica a um modelo normalizado quando consultas reais, usuários reais e SLAs reais indicam que as junções e os cálculos instantâneos são muito lentos ou muito caros. Na prática, você troca leituras mais rápidas e consultas mais simples por mais armazenamento, gravações mais complexas e trabalho extra de consistência.
Neste artigo, vou mostrar onde a desnormalização é útil e quando não é, e como implementá-la com segurança em bancos de dados SQL. O objetivo não é deixar de lado um bom design, mas usar atalhos quando a carga de trabalho justificar.
O que é desnormalização em bancos de dados?
Se você já conhece normalização e design de banco de dados, aqui vai a resposta curta:
Desnormalização é quando a gente adiciona dados que já estão em um esquema normalizado só pra acelerar a leitura e simplificar as consultas. É uma otimização de desempenho direcionada, não uma desculpa para ignorar uma boa modelagem!
Se você é relativamente novo no design de bancos de dados, talvez valha a pena explicar o significado de “normalizado”, “desnormalizado” e “não normalizado”. Você ouve esses três termos com frequência na internet, e é importante não confundi-los.
Normalizado: Os dados são divididos em tabelas bem organizadas que minimizam a redundância e protegem a integridade ee (veja 3NF/BCNF).
Desnormalizado: Você volta a usar redundância seletiva com colunas extras, valores pré-calculados ou tabelas pré-unidas, além desse modelo normalizado, para deixar as leituras comuns mais rápidas. Com a desnormalização, você mantém uma única fonte de verdade (as tabelas normalizadas) e, em seguida, mantém uma ou mais representações mais rápidas para caminhos populares, como painéis, listas de produtos, pesquisa, etc. Você troca complexidade de armazenamento + gravação por velocidade de leitura + consultas mais simples.
Não normalizado: Dados brutos, ad hoc ou desorganizados, cuja estrutura e restrições nunca foram adequadamente projetadas. Não é isso que estamos fazendo aqui.
Exemplo em SQL
Aqui está um pequeno exemplo em SQL para ajudá-lo a visualizar a diferença entre modelagem normalizada e desnormalizada.
Modelagem normalizada
-- Source of truth
CREATE TABLE customers (
customer_id BIGINT PRIMARY KEY,
name TEXT NOT NULL,
tier TEXT NOT NULL
);
CREATE TABLE orders (
order_id BIGINT PRIMARY KEY,
customer_id BIGINT NOT NULL REFERENCES customers(customer_id),
order_total NUMERIC(12,2) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT now()
);
-- Typical report needs a join
SELECT c.name, c.tier, SUM(o.order_total) AS revenue
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
GROUP BY c.name, c.tier;
Desnormalizado para um caminho de relatório
-- Add redundant fields for faster reads
ALTER TABLE orders
ADD COLUMN customer_name TEXT,
ADD COLUMN customer_tier TEXT;
-- Now most reports avoid the join
SELECT customer_name, customer_tier, SUM(order_total) AS revenue
FROM orders
GROUP BY customer_name, customer_tier;
Nesse exemplo, você manteria os clientes como a fonte da verdade e garantiria que orders.customer_name / orders.customer_tier ficassem sincronizados (por exemplo, gatilho, tarefa CDC ou um preenchimento programado). A mesma verdade, duas representações, cada uma otimizada para uma função diferente.
A desnormalização é um mau design?
Tem um equívoco comum de que a desnormalização é só resultado de um projeto ruim. Não é o caso se for medido, avaliado e mantido. Sim, a desnormalização muitas vezes vai contra as formas normais superiores (esse é o objetivo), mas é de propósito e tem um plano para manter a consistência. Um design ruim é pular a normalização ou espalhar redundância sem uma estratégia de sincronização.
Se você quiser saber mais sobre design de banco de dados , nosso curso para iniciantes sobre oassunto é um ótimo lugar para começar!
Normalização vs. Desnormalização
Aqui tá a comparação lado a lado. Não é exaustivo, mas ajudará você a entender as vantagens e desvantagens de cada abordagem.
|
Aspecto |
Normalização |
Desnormalização |
|
Objetivo principal |
Integridade, redundância mínima, atualizações fáceis e corretas |
Leituras mais rápidas, consultas mais simples em caminhos populares |
|
Ideal para |
Sistemas OLTP com gravações/atualizações frequentes |
Painéis com muito texto, páginas de pesquisa/listagem, relatórios/análises |
|
Ler desempenho |
Muitas vezes precisa de junções, funciona bem com os índices certos. |
Menos junções, pode ser bem mais rápido e previsível |
|
Escreva complexidade |
Simples: fonte única de verdade |
Mais alto: precisa atualizar/sincronizar cópias ou agregados redundantes |
|
Armazenamento |
Enxuto |
Maior (colunas/tabelas extras, visualizações pré-calculadas) |
|
Integridade dos dados |
Robusto por padrão (restrições 3NF/BCNF) |
Precisa de mecanismos pra evitar desvios (gatilhos, CDC, tarefas) |
|
Mudança de velocidade |
As renomeações/atualizações de colunas são localizadas |
As alterações podem se espalhar pelos dados duplicados. |
|
Despesas operacionais |
Inferior: menos peças móveis |
Mais alto: atualizar políticas, preenchimentos, monitoramento |
|
Modos de falha |
N+1 consultas, junções lentas, índices ausentes |
Dados obsoletos, inconsistência, amplificação de gravação |
|
Evolução do esquema |
Previsível, fácil de refatorar |
Precisa de planos de migração para representações redundantes |
|
Exemplos típicos |
Pedidos, clientes, transações |
Listas de produtos pré-agrupadas; tabelas de vendas agregadas; visualizações materializadas/indexadas |
Por que e quando desnormalizar
A desnormalização é super útil quando usuários reais e consultas reais ficam bloqueados por junções, agregações ou pesquisas repetidas. Depois de confirmar que a indexação, o ajuste de consultas e o armazenamento em cache não são suficientes, você pode recorrer à desnormalização para otimizar a velocidade de leitura em padrões de acesso previsíveis.
Quando a desnormalização pode ajudar
- Cargas de trabalho com muita leitura: latências p95/p99 dominadas por junções ou agregações, CPU gasta em junções hash/merge, alta rotatividade do cache do buffer.
- Formatos de consulta estáveis: Os mesmos painéis/terminais funcionam o dia todo com filtros parecidos (por exemplo, vendas de ontem por categoria).
- Junções em leque: Uma tabela ativa se junta a outras 3-5 só para mostrar um cartão ou uma lista.
- Pontos de agregação: Você fica calculando totais, contagens ou valores mais recentes em grandes intervalos.
- Pressão SLA: O produto precisa de respostas abaixo de 200 ms, enquanto os planos atuais despejam no disco ou fazem muitas varreduras.
Casos de uso clássicos
- Painéis e relatórios de BI (OLAP/análise): Faça cálculos antecipados de agregados diários/mensais, mantenha visualizações materializadas de agrupamentos caros ou armazene tabelas de fatos desnormalizadas para fatias comuns.
- Comércio eletrônico/catálogo e páginas de pesquisa/listagem: Duplicar nome_da_categoria, nome_da_marca, preço_com_impostoou uma projeção de produto pré-unida para listas e filtros rápidos.
- CMS/blog/feeds de notícias: Loja autor_nome, tópico_principalou trecho_renderizado na tabela de artigos/postagens para evitar junções ou transformações em tempo de execução.
- Feeds e contadores de atividades: Manter like_count, follower_count, ou último_comentário_em como atributos derivados, em vez de recalcular.
- Análise de eventos/logs em escala (OLAP/nosql): Simplifique os dados aninhados para armazenamentos em colunas e mantenha linhas largas compatíveis com partições para tornar as varreduras previsíveis.
Quando não desnormalizar e usar outra coisa primeiro
- OLTP com muitas gravações e consistência rigorosa (pedidos, pagamentos, ajustes de estoque).
- O problema é a falta de índices ou índices ruins, consultas N+1 ou ORM com muita comunicação. Conserte esses primeiro.
- Áreas de alta volatilidade (por exemplo, disponibilidade de produtos que muda a cada segundo) onde a duplicação aumenta a rotatividade.
- Equipes sem um plano claro de responsabilidade e sincronização (gatilhos, CDC/tarefas, políticas de atualização, monitoramento de desvios). Sem isso, você corre o risco de fazer mais mal do que bem!
- O conjunto de dados é pequeno o suficiente para que um índice de cobertura ou cache já o torne rápido.
Você realmente deve usar a desnormalização quando ela te der a maior velocidade de leitura com o menor esforço operacional extra, e só depois de ter descartado soluções mais baratas.
Se você percebeu que a desnormalização não é bem o que você precisa agora, fique tranquilo! Vamos ver as alternativas à desnormalização na próxima seção.
Alternativas à desnormalização
A desnormalização não é a resposta para todos os seus problemas. Antes de adicionar redundância, você precisa extrair tudo o que puder do mecanismo e do seu aplicativo. Essas correções são mais baratas de manter e, muitas vezes, trazem os mesmos benefícios.
1) Indexação
-
Índices compostos/de cobertura: Nas suas consultas SQL, coloque primeiro as colunas de filtro e, em seguida, as colunas
GROUP BY/ORDER BY. Eu coloco colunas de lista de seleção pra que o mecanismo possa responder à consulta só com o índice. -
Índices filtrados/parciais: Indexe só a parte mais importante (por exemplo,
status = 'ACTIVE'), mantendo o índice pequeno e rápido. -
Índices de expressão/funcionais: Crie um índice em
LOWER(email)oudate_trunc('day', created_at)para evitar varreduras computadas.
2) Ajuste de consultas e paginação
-
Evite
SELECT *. Pega só o que você mostra. -
Troque as junções desnecessárias por
EXISTS/SEMIquando você só precisa verificar a presença. -
Empurre os predicados para baixo: filtre cedo, agregue tarde.
-
Use a paginação por teclado (
WHERE created_at < ? ORDER BY created_at DESC LIMIT 50) para uma rolagem estável e rápida.
3) Armazenamento em cache
- Use o cache do aplicativo (por exemplo, Redis) para consultas frequentes e fragmentos renderizados.
- Use o o cache HTTP (ETag/Last-Modified) para páginas públicas e painéis.
- Os caches de curta duração (30–120 s) muitas vezes eliminam a necessidade de alterações no esquema.
4) Ler réplicas
- Descarregue leituras pesadas para réplicas. Ótimo para painéis e exportações.
5) Particionamento e poda
- Faça o range/hash partition de tabelas grandes para que suas varreduras só mexam nas partições relevantes (por exemplo, os últimos 30 dias). A propósito, isso não é desnormalização, é só reduzir a quantidade de dados verificados.
6) Armazenamentos colunares/OLAP
- Envie análises pesadas para o Snowflake/BigQuery/ClickHouse (via ELT/dbt). Mantenha o OLTP normalizado e deixe o warehouse lidar com formatos amplos e fáceis de escanear.
7) Higiene ORM
- Matar as consultas N+1 (carregamento antecipado ou em lote), defina listas de seleção sensatas listas de seleçãoe limite tamanhos de página. Uma camada ORM limpa pode eliminar a necessidade de desnormalização.
8) Colunas calculadas/geradas (mantidas pelo banco de dados)
-
Deixe o banco de dados manter valores derivados (por exemplo,
price_with_tax) como colunas geradas/calculadas ou por meio de índices de expressão. Isso vai te dar leituras rápidas sem precisar de sincronização no aplicativo.
Técnicas de desnormalização
Aqui estão as maneiras mais comuns de adicionar redundância controlada a um modelo normalizado. Para cada um deles, vou mostrar o que faz, quando usar e como manter tudo sincronizado.
Configuração: imagine um núcleo normalizado com clientes, pedidos, itens de pedidos, produtos.
1) Tabelinhas “projeção” achatadas/pré-montadas
O quê: Crie uma tabela que pré-une as colunas que você precisa para leituras rápidas (por exemplo, lista de produtos ou painel de pedidos).
Quando: Seu caminho quente junta de 3 a 5 tabelas de maneiras previsíveis.
Como (PostgreSQL):
-- Read-optimised projection for a typical orders list
CREATE TABLE orders_projection (
order_id BIGINT PRIMARY KEY,
created_day DATE NOT NULL,
customer_id BIGINT NOT NULL,
customer_name TEXT NOT NULL,
total_amount NUMERIC(12,2) NOT NULL
);
-- Initial backfill
INSERT INTO orders_projection (order_id, created_day, customer_id, customer_name, total_amount)
SELECT o.order_id,
o.created_at::date AS created_day,
c.customer_id,
c.name AS customer_name,
SUM(oi.quantity * oi.unit_price) AS total_amount
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
JOIN order_items oi ON oi.order_id = o.order_id
GROUP BY o.order_id, created_day, c.customer_id, c.name;
-- Index for fast filtering by day/customer
CREATE INDEX ON orders_projection (created_day, customer_id);
Opções de sincronização:
- Trabalho pós-gravação (enfileirar “order.updated” → recalcular linha)
- Trabalho em lote noturno/15 minutos (dbt/cron)
2) Colunas redundantes (copiar alguns atributos)
O quê: Duplique alguns atributos que você lê com frequência em outra tabela pra evitar junções (por exemplo, orders.customer_name).
Quando: Você só precisa de 1 ou 2 valores e não quer uma projeção completa.
Como (exemplo de gatilho PostgreSQL):
ALTER TABLE orders
ADD COLUMN customer_name TEXT,
ADD COLUMN customer_tier TEXT;
-- Keep the redundant fields correct on insert/update
CREATE OR REPLACE FUNCTION sync_order_customer_fields()
RETURNS TRIGGER AS $
BEGIN
SELECT name, tier INTO NEW.customer_name, NEW.customer_tier
FROM customers WHERE customer_id = NEW.customer_id;
RETURN NEW;
END;
$ LANGUAGE plpgsql;
CREATE TRIGGER trg_orders_sync_customer
BEFORE INSERT OR UPDATE OF customer_id ON orders
FOR EACH ROW EXECUTE FUNCTION sync_order_customer_fields();
-- Propagate customer name/tier changes to existing orders
CREATE OR REPLACE FUNCTION propagate_customer_changes()
RETURNS TRIGGER AS $
BEGIN
UPDATE orders
SET customer_name = NEW.name,
customer_tier = NEW.tier
WHERE customer_id = NEW.customer_id;
RETURN NEW;
END;
$ LANGUAGE plpgsql;
CREATE TRIGGER trg_customer_propagate
AFTER UPDATE OF name, tier ON customers
FOR EACH ROW EXECUTE FUNCTION propagate_customer_changes();
Compromisso: A leitura é simples, mas a gravação se espalha quando um cliente atualiza.
3) Atributos derivados (armazenam valores calculados)
O quê: Persista nos valores que você normalmente calcula na hora (por exemplo, price_with_tax , latest_comment_at, item_count).
Quando: O cálculo é feito constantemente e é determinístico.
Dois padrões comuns:
- Coluna gerada (O banco de dados recalcula na gravação: você tem menos controle, sem referências entre tabelas)
- Coluna armazenada + gatilho/tarefa (você controla quando/como ela muda)
-- Generated column example (Postgres 12+; same-table expressions)
ALTER TABLE products
ADD COLUMN price_with_tax NUMERIC(12,2) GENERATED ALWAYS AS (price * 1.20) STORED;
-- Counter maintained by triggers (likes per post)
ALTER TABLE posts ADD COLUMN like_count INTEGER NOT NULL DEFAULT 0;
CREATE OR REPLACE FUNCTION inc_like_count() RETURNS TRIGGER AS $
BEGIN
UPDATE posts SET like_count = like_count + 1 WHERE post_id = NEW.post_id;
RETURN NEW;
END; $ LANGUAGE plpgsql;
CREATE TRIGGER trg_like_insert
AFTER INSERT ON post_likes
FOR EACH ROW EXECUTE FUNCTION inc_like_count();
CREATE OR REPLACE FUNCTION dec_like_count() RETURNS TRIGGER AS $
BEGIN
UPDATE posts SET like_count = GREATEST(like_count - 1, 0) WHERE post_id = OLD.post_id;
RETURN OLD;
END; $ LANGUAGE plpgsql;
CREATE TRIGGER trg_like_delete
AFTER DELETE ON post_likes
FOR EACH ROW EXECUTE FUNCTION dec_like_count();
4) Tabelas agregadas/resumo
O quê: Faça cálculos prévios de rollups, como receita diária, usuários ativos por dia, pedidos por categoria.
Quando: Os painéis repetem os mesmos agrupamentos e as varreduras brutas são caras.
CREATE TABLE daily_sales (
sales_day DATE PRIMARY KEY,
gross_amount NUMERIC(14,2) NOT NULL,
order_count INTEGER NOT NULL
);
-- Incremental upsert for "yesterday" (run hourly/15-min)
INSERT INTO daily_sales (sales_day, gross_amount, order_count)
SELECT (o.created_at AT TIME ZONE 'UTC')::date AS sales_day,
SUM(oi.quantity * oi.unit_price) AS gross_amount,
COUNT(DISTINCT o.order_id) AS order_count
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
WHERE o.created_at >= date_trunc('day', now() - interval '1 day')
AND o.created_at < date_trunc('day', now())
GROUP BY sales_day
ON CONFLICT (sales_day) DO UPDATE
SET gross_amount = EXCLUDED.gross_amount,
order_count = EXCLUDED.order_count;
Opções de sincronização: tarefa agendada (cron/dbt), atualizações em tempo real (CDC) ou agregadores orientados por eventos.
5) Visualizações materializadas (resultados de consultas persistentes)
O quê: Guarde o resultado de uma consulta cara como uma tabela física que você possa indexar.
Quando: O conjunto de resultados é caro, relativamente estável e você pode tolerar janelas de atualização.
-- Expensive report
CREATE MATERIALIZED VIEW mv_category_sales AS
SELECT p.category_id,
date_trunc('day', o.created_at) AS day,
SUM(oi.quantity * oi.unit_price) AS revenue
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
JOIN products p ON p.product_id = oi.product_id
GROUP BY p.category_id, date_trunc('day', o.created_at);
-- Make it fast to query
CREATE INDEX ON mv_category_sales (category_id, day);
-- Refresh patterns
-- Full, blocking:
REFRESH MATERIALIZED VIEW mv_category_sales;
-- Non-blocking reads (requires unique index on the MV):
-- 1) ensure a unique index exists (e.g., (category_id, day))
-- 2) then:
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_category_sales;
Compromisso: Você vai controlar a frequência de atualização, porque a atualização incremental não é nativa no Postgres. Dito isso, é bem fácil quando você usa o dbt ou padrões personalizados de “apenas acrescentar + reconstrução em janela”.
6) Projeções de “documentos” JSON pré-calculadas
O quê: Armazene um blob JSON desnormalizado que atenda às suas necessidades de API/UI (por exemplo, cartão de produto), junto com índices JSON direcionados.
Quando: Sua API serve a mesma forma repetidamente, e você quer uma leitura.
CREATE TABLE product_cards (
product_id BIGINT PRIMARY KEY,
card_json JSONB NOT NULL
);
-- Build or rebuild card documents
INSERT INTO product_cards (product_id, card_json)
SELECT p.product_id,
jsonb_build_object(
'id', p.product_id,
'name', p.name,
'brand', b.name,
'price_with_tax', p.price * 1.20,
'category', c.name
)
FROM products p
JOIN brands b ON b.brand_id = p.brand_id
JOIN categories c ON c.category_id = p.category_id
ON CONFLICT (product_id) DO UPDATE SET card_json = EXCLUDED.card_json;
-- GIN index for JSON queries if needed
CREATE INDEX idx_product_cards_gin ON product_cards USING GIN (card_json);
Opções de sincronização: reconstrução orientada por eventos em caso de alterações no produto/marca/categoria ou atualização frequente em lote para produtos atualizados recentemente.
Escolhendo uma estratégia de sincronização (guia rápido)
- Gatilhos: São imediatos e garantem a consistência das transações. Eles geralmente são ótimos para pequenas distribuições e baixo volume de gravação, mas podem prejudicar a latência de gravação se forem usados em excesso.
- Aplicação de gravação dupla: O aplicativo grava tanto a fonte quanto a desnormalização. É simples, mas mais arriscado. Você pode resolver isso com tentativas idempotentes e padrões de caixa de saída/CDC.
- CDC → funcionário: Essa estratégia syn é confiável e escalável, pois propaga as alterações de forma assíncrona. É ideal quando a consistência eventual é aceitável.
- Tarefas programadas: A opção mais simples para agregados e visualizações materializadas. É só escolher as janelas de atualização que combinam com a sua tolerância de experiência do usuário.
Eu recomendo que você sempre inclua observabilidade na sua solução. Não tem nada pior do que ficar na dúvida se as alterações foram processadas ou replicadas corretamente. Também pode te ajudar a controlar suas verificações de desvio e scripts de preenchimento.
Como implementar a desnormalização passo a passo
Como sempre, mexer no seu banco de dados é bem arriscado. Eu recomendo muito que você faça uma cópia do seu banco de dados e tente implementar a desnormalização primeiro na cópia, só pra ter certeza de que tudo está funcionando como você espera. Se você tem um ambiente de desenvolvimento, melhor ainda!
O processo é bem simples: medir → mudar o mínimo possível → manter correto → verificar → monitorar → repetir.
1) Defina um perfil e estabeleça uma meta de sucesso
- Capture a consulta exata (texto + parâmetros) e sua frequência.
- Registre uma linha de base: EXPLIQUE (ANALISE, BUFFERS) plano, latência p95, CPU/IO, linhas verificadas.
- Combine um critério de sucesso (por exemplo, “p95 < 120 ms com custo de gravação ≤1,2×”).
- Confirme a tolerância de consistência (por exemplo, “as análises podem estar desatualizadas em 5 minutos”).
2) Escolha a menor desnormalização que funcionar
- O ideal é tentar uma coluna redundante ou uma tabela resumida em uma projeção grande.
- Esboce a nova forma:
- Colunas redundantes? (quais, por quê)
- Tabela agregada ou visualização materializada? (granulação, chaves, janela de atualização)
- Escolha o modelo de consistência:
- Forte (gatilho/transacional) vs eventual (CDC/tarefa/atualização de visualização materializada).
- Anote o orçamento de amplificação de gravação (quantas gravações extras por evento são aceitáveis).
3) Defina o caminho de sincronização
- Gatilhos (consistência forte, pequeno fan-out).
- CDC/caixa de saída → trabalhador (boa escalabilidade, consistência eventual):
- O aplicativo grava nas tabelas de origem e em uma linha da caixa de saída na mesma transação.
- Um trabalhador lê a caixa de saída e atualiza o destino desnormalizado de forma idempotente.
- Tarefas programadas / atualização MV (ótimo para agregados):
- Defina a frequência de atualização, o intervalo e a estratégia de preenchimento.
A idempotência não é negociável. As atualizações devem ser repetíveis com segurança (por exemplo, UPSERT com recálculo determinístico). Confia em mim, você vai se agradecer mais tarde.
4) Criar, preencher e validar
- Crie estruturas (tabelas, colunas, índices, gatilhos/tarefas), mas não mexa no aplicativo.
- Preenchimento a partir da fonte de verdade.
- Validar a paridade com invariantes:
-- Example parity check: orders_projection vs live join
SELECT COUNT(*) AS mismatches
FROM orders_projection p
JOIN orders o ON o.order_id = p.order_id
JOIN customers c ON c.customer_id = o.customer_id
WHERE p.customer_name <> c.name
OR p.total_amount <> (
SELECT SUM(oi.quantity * oi.unit_price)
FROM order_items oi WHERE oi.order_id = o.order_id
);
4. Corrija as incompatibilidades; repita até chegar a zero (ou dentro de um limite de erro combinado).
5) Faça o corte com cuidado
- Envie a alteração do aplicativo por trás de um sinalizador de recurso ou canário (por exemplo, 10% do tráfego lê a partir do caminho desnormalizado).
- Executar leituras em segundo plano: calcular o resultado antigo em segundo plano e comparar hashes/agregados para um subconjunto de solicitações.
- Mantenha um retorno rápido (volte instantaneamente para o caminho normalizado).
6) Fique de olho nas coisas certas
Crie um pequeno painel na sua ferramenta de observabilidade preferida:
- Leia: latência p50/p95, linhas lidas, acertos de buffer, estabilidade do plano de consulta.
- Escrever: tempo extra de gravação, erros de acionamento/tarefa, atraso na fila (CDC), carimbo de data/hora da última atualização do MV.
- Qualidade dos dados: contadores de desvio (verificações diárias de paridade), número de linhas preenchidas, % de incompatibilidades.
- Custo/pegadatamanhos das tabelas, aumento do índice, crescimento do tamanho do MV.
7) Operar e iterar
- Atribuir responsabilidades (equipe/sistema de plantão) e um manual de procedimentos (como reconstruir, reabastecer, reparar). Isso é muitas vezes esquecido, mas você vai querer poder responder o mais rápido possível quando acontecer um incidente.
- Reavaliar trimestralmente: ainda é necessário? Ainda tá com o formato certo? A gente pode simplificar? Documente a decisão e crie um link para os painéis.
Prós e contras da desnormalização
A gente falou sobre os detalhes em Normalização vs. Desnormalização em uma seção anterior, então aqui vai um resumo rápido.
O que você ganha
- Leituras mais rápidas e latências p95/p99 mais previsíveis
- Consultas mais simples em caminhos populares (menos junções/agregações)
- Menos CPU/IO por leitura e consultas analíticas mais baratas
Quanto custa
- Amplificação de gravação (atualizações/inserções extras)
- Risco de inconsistência (dados desatualizados/inconsistentes, a menos que você sincronize corretamente)
- Aumento do armazenamento (colunas/tabelas/visualizações duplicadas)
- Custos operacionais (atualizações, preenchimentos, monitoramento)
- Alterar fan-out (as atualizações do esquema/lógica devem ser espelhadas)
Ferramentas e tecnologias que ajudam na desnormalização
Nem todo banco de dados te ajuda a desnormalizar da mesma maneira. Alguns oferecem recursos de primeira para guardar resultados de consultas caras, enquanto outros esperam que você mesmo faça isso com tarefas, gatilhos ou pipelines.
PostgreSQL
O Postgres oferece várias maneiras de materializar formas otimizadas para leitura.
As visualizações materializadas mantêm o resultado de uma consulta, então as leituras são instantâneas. Você escolhe quando atualizar (em uma programação ou ad hoc) e pode até atualizar ao mesmo tempo para que os leitores não fiquem bloqueados.
Para uma duplicação leve, as colunas geradas e os índices de expressão permitem que o banco de dados mantenha valores derivados e acelere filtros comuns sem escrever lógica de sincronização extra.
Quando você precisa de uma consistência forte entre a fonte e uma cópia desnormalizada, pode usar gatilhos para manter tudo sincronizado (mas lembre-se, eles adicionam trabalho ao seu caminho de gravação!). Para atualizações baseadas em eventos ou em lote, você pode juntar o Postgres com um agendador (por exemplo, pg_cron ou um executor de tarefas externo) ou com replicação lógica/CDC para transmitir alterações para um trabalhador que atualiza suas tabelas desnormalizadas de forma assíncrona.
Se você quiser colocar a mão na massa e criar seus próprios bancos de dados e testar a desnormalização, dêuma olhada no nosso curso de PostgreSQL.
SQL Server
O SQL Server usa bastante as visualizações indexadas, que são tipo visualizações materializadas sempre ativas, onde o mecanismo mantém a visualização sincronizada em cada inserção/atualização/exclusão, então as leituras são super rápidas. O outro lado da moeda é que agora as gravações mantêm tanto as tabelas base quanto a visualização, então cargas de trabalho OLTP pesadas podem ficar mais lentas.
Além disso, você pode usar colunas computadas persistentes para cobrir atributos derivados de uma única linha.
Para relatórios em grande escala, os índices de armazenamento em coluna são uma alternativa à desnormalização: eles compactam e examinam grandes conjuntos de dados de forma muito eficiente, o que pode eliminar a necessidade de duplicar os dados.
Oracle
As Visualizações Materializadas da Oracle são uma opção bem legal com atualização “ FAST ” que usa registros de alterações para atualizar só o que mudou, ou atualização “ COMPLETE ” quando você não se importa em reconstruir tudo.
Com o Query Rewrite, o otimizador pode usar de forma transparente sua visualização materializada quando alguém consulta as tabelas subjacentes, para que as equipes obtenham o aumento de velocidade sem alterar o SQL do aplicativo. Como sempre, leituras mais rápidas significam trabalho extra em algum lugar: manter registros e atualizar visualizações aumenta a sobrecarga de gravação e operacional.
MySQL / MariaDB
O MySQL não tem visualizações materializadas nativas, então a desnormalização geralmente é feita com tabelas físicas e tarefas programadas ou gatilhos para mantê-las atualizadas.
As colunas geradas ajudam com valores derivados simples. Isso funciona bem para painéis e resumos previsíveis, mas, novamente, vale a pena ser intencional quanto à cadência de atualização e à complexidade do gatilho, para que você não sobrecarregue acidentalmente os caminhos de gravação críticos. Muitas equipes juntam isso com réplicas de leitura pra tirar totalmente o peso das consultas de relatórios.
Snowflake / BigQuery (camada de análise)
Os warehouses colunares são feitos pra dados largos, fáceis de escanear e desnormalizados. Normalmente, você modela tabelas de fatos amplas com atributos úteis incorporados e, em seguida, usa o particionamento/agrupamento para filtrar o que é verificado.
Ambas as plataformas suportam visualizações materializadas e tarefas/consultas programadas para manter os agregados atualizados sem mexer no sistema OLTP. Você troca os custos de armazenamento e atualização por leituras bem previsíveis e baratas em grande escala. É perfeito para painéis e BI.
Este curso é um pouco mais avançado do que o nosso curso sobre PostgreSQL, então, se você já está familiarizado com bancos de dados, fique à vontade para experimentar nossa introdução à modelagem de dados com o Snowflake.
dbt (modelagem e orquestração)
O dbt é conhecido como a camada de “infraestrutura como código” para análises desnormalizadas. Você define os modelos uma vez, escolhe uma materialização (tabela, visualização, incremental) e deixa o dbt cuidar das compilações, dependências e testes. Os modelos incrementais são super úteis para tabelas resumidas que só precisam de novos dados anexados e mesclados. É facilmente uma das minhas ferramentas favoritas!
CDC e pipelines de dados
Quando você quer que as alterações do aplicativo fluam para estruturas desnormalizadas de forma confiável, o Change Data Capture é a solução ideal. Ferramentas como Debezium (auto-hospedado) ou conectores gerenciados (Fivetran, Airbyte) transmitem alterações no nível da linha do OLTP para trabalhadores ou warehouse que atualizam tabelas de projeção, contadores ou agregados. Isso geralmente é consistente no final das contas (ótimo para painéis e feeds) e se adapta muito melhor do que enfiar tudo em gatilhos. Faça atualizações idempotentes, monitore o atraso e mantenha um caminho de preenchimento para eventos atrasados ou perdidos.
Principais conclusões e considerações finais
A desnormalização é uma otimização prática que vem em cima de um design normalizado, não uma rejeição dele. Você está trocando complexidade de armazenamento e gravação por leituras mais rápidas e simples em caminhos específicos e bem compreendidos.
Lembre-se:
- Comece normalizado. Use a desnormalização só quando uma consulta nomeada e medida precisar dela.
- Esgote primeiro as soluções mais baratas (índices, reescritas de consultas, cache, réplicas, OLAP).
- Escolha a menor desnormalização que valha a pena (uma coluna redundante, um pequeno agregado ou uma visualização materializada).
- Seja bem claro sobre a atualização (consistência forte versus eventual) e crie uma sincronização idempotente.
- Meça antes e depois, monitore o desvio e mantenha um botão de reversão à mão.
- Trate as tabelas normalizadas como a fonte da verdade! Pedaços desnormalizados são cópias otimizadas para leitura que você pode reconstruir.

Sou um líder técnico voltado para produtos, especializado no crescimento de startups em estágio inicial, desde o primeiro protótipo até a adequação do produto ao mercado e além. Tenho uma curiosidade infinita sobre como as pessoas usam a tecnologia e adoro trabalhar com fundadores e equipes multifuncionais para dar vida a ideias ousadas. Quando não estou criando produtos, estou em busca de inspiração em novos cantos do mundo ou desabafando no estúdio de ioga.


