Capa do Ebook gratuito SQL para Análise de Dados no Dia a Dia: Consultas, Relatórios e Insights com Dados Reais

SQL para Análise de Dados no Dia a Dia: Consultas, Relatórios e Insights com Dados Reais

Novo curso

26 páginas

Chaves, cardinalidade e duplicidades: como evitar resultados inflados em JOINs

Capítulo 5

Tempo estimado de leitura: 0 minutos

+ Exercício

Por que resultados “inflados” acontecem em JOINs

Quando um relatório “explode” em número de linhas ou soma valores acima do esperado após um JOIN, quase sempre a causa é uma combinação de: (1) chaves mal definidas (ou usadas de forma incompleta), (2) cardinalidade diferente da assumida (por exemplo, você achava que era 1:1, mas é 1:N), e (3) duplicidades no lado que deveria ser único. O JOIN em si não “erra”: ele apenas combina linhas que satisfazem a condição. Se uma linha de uma tabela encontra várias correspondências na outra, ela será repetida uma vez para cada correspondência. Isso é correto do ponto de vista relacional, mas pode ser desastroso para métricas como receita, quantidade vendida, número de clientes e qualquer soma/contagem que você faça depois.

O objetivo deste capítulo é te dar um método prático para: identificar a cardinalidade real antes de juntar, validar unicidade de chaves, detectar duplicidades e escolher estratégias para evitar multiplicação de linhas e somas infladas.

Chaves: o que você precisa dominar para JOINs seguros

Chave primária, chave candidata e chave composta

Em análise de dados, você nem sempre controla o modelo, mas precisa entender o papel das chaves:

  • Chave primária (PK): coluna(s) que identificam unicamente cada linha de uma tabela. Ex.: customers.customer_id.
  • Chave candidata: qualquer conjunto de coluna(s) que também seria único, mesmo que não esteja declarado como PK (ex.: email pode ser candidato, mas nem sempre é confiável).
  • Chave composta: quando a unicidade depende de mais de uma coluna. Ex.: em uma tabela de itens de pedido, o identificador único pode ser (order_id, line_number) ou (order_id, product_id) dependendo do sistema.

Um erro comum é juntar usando apenas parte de uma chave composta. Isso cria correspondências indevidas e multiplica linhas.

Chaves naturais vs. chaves substitutas (surrogate keys)

Você pode encontrar:

Continue em nosso aplicativo

Você poderá ouvir o audiobook com a tela desligada, ganhar gratuitamente o certificado deste curso e ainda ter acesso a outros 5.000 cursos online gratuitos.

ou continue lendo abaixo...
Download App

Baixar o aplicativo

  • Chaves naturais: têm significado de negócio (ex.: CPF, e-mail, código do produto). Podem mudar, ter formatação inconsistente ou duplicar.
  • Chaves substitutas: IDs internos (ex.: customer_id). Em geral, são mais estáveis para JOIN.

Para evitar resultados inflados, prefira chaves substitutas quando disponíveis. Se precisar usar chave natural, valide duplicidade e padronização (ex.: remover espaços, normalizar caixa) antes de confiar no JOIN.

Cardinalidade: a “matemática” por trás da multiplicação

O que é cardinalidade em JOIN

Cardinalidade descreve quantas linhas de um lado podem se relacionar com quantas do outro:

  • 1:1: cada linha em A corresponde a no máximo uma em B, e vice-versa.
  • 1:N: uma linha em A corresponde a várias em B.
  • N:N: várias linhas em A correspondem a várias em B (normalmente via tabela de associação).

Resultados inflados acontecem quando você assume 1:1, mas na prática é 1:N ou N:N. Ex.: você junta orders com order_items e depois soma orders.order_total. Cada pedido aparece repetido para cada item, então o total do pedido é somado várias vezes.

Exemplo mental rápido: por que a soma infla

Imagine:

  • Pedido 10 tem order_total = 100
  • Pedido 10 tem 3 itens em order_items

Ao juntar pedidos com itens, o pedido 10 vira 3 linhas. Se você somar order_total após o JOIN, você obtém 300 para esse pedido, mesmo que o correto seja 100. O JOIN não está errado; a métrica é que está sendo calculada no nível de detalhe errado.

Duplicidades: quando o “lado 1” não é realmente 1

Mesmo que o relacionamento de negócio seja 1:N, você ainda pode ter inflação adicional quando a tabela do lado “1” tem duplicidades na chave que deveria ser única, ou quando a tabela do lado “N” tem duplicidades inesperadas (ex.: registros repetidos por reprocessamento, múltiplas versões, linhas de log).

Exemplos típicos de duplicidade que causam inflação:

  • Dimensão com múltiplas linhas por entidade: tabela de clientes com histórico (SCD), onde um cliente aparece várias vezes por mudança de endereço.
  • Tabela de preços com múltiplos preços ativos para o mesmo produto e data.
  • Tabela de pagamentos com múltiplas tentativas, estornos e reprocessamentos sem um filtro claro de status.
  • Chave natural suja: e-mails duplicados, CPFs com formatação diferente, códigos com zeros à esquerda inconsistentes.

Checklist prático antes de fazer JOIN (passo a passo)

Use este roteiro sempre que uma consulta envolver métricas agregadas (soma/contagem) após JOIN.

Passo 1 — Defina o “grão” (nível de detalhe) do resultado

Antes de escrever o JOIN, responda: “uma linha do meu resultado final representa o quê?” Exemplos:

  • Uma linha por pedido
  • Uma linha por cliente por mês
  • Uma linha por produto por dia

Se você quer “uma linha por pedido”, então qualquer JOIN que traga tabelas no nível de item (order_items) precisa ser agregado de volta ao nível de pedido antes (ou depois, com cuidado) para não multiplicar.

Passo 2 — Verifique se a chave do lado esperado como único é realmente única

Para validar unicidade, conte ocorrências por chave. Se houver valores com contagem maior que 1, você tem duplicidade.

-- Verificando se customer_id é único em customers (deveria ser 1 linha por cliente)  SELECT customer_id, COUNT(*) AS qtd  FROM customers  GROUP BY customer_id  HAVING COUNT(*) > 1;

Se aparecerem linhas, você precisa entender por quê: histórico? múltiplos cadastros? dados sujos? A estratégia de correção depende do caso (ver seções adiante).

Passo 3 — Meça a cardinalidade real do relacionamento

Uma forma prática é medir “quantas correspondências em média” cada chave encontra do outro lado.

-- Quantos itens por pedido? (esperado: 1:N)  SELECT oi.order_id, COUNT(*) AS itens  FROM order_items oi  GROUP BY oi.order_id  ORDER BY itens DESC;

Para relações que você espera 1:1, qualquer valor acima de 1 é sinal de risco.

-- Você espera 1:1 entre orders e shipments (1 pedido, 1 envio). Confirme:  SELECT s.order_id, COUNT(*) AS envios  FROM shipments s  GROUP BY s.order_id  HAVING COUNT(*) > 1;

Passo 4 — Faça um “JOIN de auditoria” e compare contagens

Antes de calcular métricas, compare:

  • Linhas da tabela base
  • Linhas após o JOIN
  • Quantidade de chaves distintas antes/depois
-- Exemplo: base é orders (1 linha por pedido)  SELECT COUNT(*) AS linhas_orders,         COUNT(DISTINCT order_id) AS pedidos_distintos  FROM orders;  -- Depois do JOIN com order_items  SELECT COUNT(*) AS linhas_join,         COUNT(DISTINCT o.order_id) AS pedidos_distintos  FROM orders o  JOIN order_items oi ON oi.order_id = o.order_id;

Se linhas_join for muito maior que linhas_orders, isso pode ser esperado (se você trouxe itens), mas é um alerta de que qualquer métrica no nível de pedido precisa ser tratada.

Padrões de erro mais comuns e como corrigir

Erro 1 — Somar uma métrica do lado “1” depois de juntar com o lado “N”

Sintoma: receita do pedido inflada ao juntar pedidos com itens.

Correção: agregue primeiro no nível correto, ou calcule métricas no nível do item (não do pedido) e depois agregue.

Exemplo: você quer receita total por dia, mas tem orders.order_total e também order_items (para outras análises). Se você juntar e somar order_total, vai inflar. Solução: calcule a receita por dia apenas com orders, e junte itens apenas se necessário e de forma agregada.

-- Receita por dia no grão correto (pedido)  SELECT order_date, SUM(order_total) AS receita  FROM orders  GROUP BY order_date;

Se você precisa de alguma informação de itens (ex.: quantidade total de itens por pedido) para depois agrupar por dia, agregue itens por pedido antes e só então junte:

WITH itens_por_pedido AS (   SELECT order_id,          SUM(quantity) AS qtd_itens,          SUM(quantity * unit_price) AS valor_itens   FROM order_items   GROUP BY order_id ) SELECT o.order_date,        SUM(o.order_total) AS receita_pedidos,        SUM(i.qtd_itens) AS itens_vendidos,        SUM(i.valor_itens) AS receita_itens  FROM orders o  LEFT JOIN itens_por_pedido i ON i.order_id = o.order_id  GROUP BY o.order_date;

Note que agora o JOIN é 1:1 (uma linha por pedido em itens_por_pedido), evitando multiplicação.

Erro 2 — JOIN com dimensão “histórica” sem filtrar a versão correta

Sintoma: ao juntar vendas com clientes, o número de linhas aumenta mais do que o esperado, e clientes aparecem repetidos.

Causa típica: tabela de clientes tem múltiplas linhas por customer_id (por exemplo, uma por período de vigência).

Correção: escolha uma regra para selecionar uma única linha por entidade (por exemplo, a mais recente) antes do JOIN.

WITH clientes_unicos AS (   SELECT *   FROM (     SELECT c.*,            ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY updated_at DESC) AS rn     FROM customers c   ) t   WHERE rn = 1 ) SELECT o.order_id, o.customer_id, cu.customer_name, o.order_total FROM orders o LEFT JOIN clientes_unicos cu ON cu.customer_id = o.customer_id;

Esse padrão (usar ROW_NUMBER()) é uma das formas mais seguras de “desduplicar” dimensões quando você precisa de uma única versão por chave.

Erro 3 — JOIN usando chave incompleta (especialmente em tabelas com chave composta)

Sintoma: itens ou preços se multiplicam de forma inesperada.

Causa típica: a tabela correta exige mais colunas para identificar unicamente o registro (ex.: preço por product_id e price_date, ou por store_id e product_id).

Correção: inclua todas as colunas necessárias na condição do JOIN e/ou pré-filtre a tabela para o recorte correto.

-- Errado: junta preço só por product_id (pode haver vários preços por data/loja)  SELECT oi.order_id, oi.product_id, p.price  FROM order_items oi  JOIN product_prices p ON p.product_id = oi.product_id;  -- Melhor: inclui a data (exemplo)  SELECT oi.order_id, oi.product_id, p.price  FROM order_items oi  JOIN orders o ON o.order_id = oi.order_id  JOIN product_prices p    ON p.product_id = oi.product_id   AND p.price_date = o.order_date;

Se o preço depende também de loja, canal ou região, essas colunas também precisam entrar no JOIN.

Erro 4 — N:N acidental por causa de duplicidade em ambos os lados

Sintoma: a multiplicação é muito maior do que o esperado, e mesmo agregando um lado, ainda há inflação.

Causa típica: você tem duplicidade na chave em A e em B, então o JOIN vira N:N. Ex.: duas linhas de cliente com mesmo customer_id e três linhas de status com mesmo customer_id geram 6 combinações.

Correção: tornar pelo menos um lado único antes do JOIN (ou ambos), com regra explícita.

WITH a_unico AS (   SELECT * FROM (     SELECT a.*, ROW_NUMBER() OVER (PARTITION BY key_id ORDER BY updated_at DESC) rn     FROM tabela_a a   ) x WHERE rn = 1 ), b_unico AS (   SELECT * FROM (     SELECT b.*, ROW_NUMBER() OVER (PARTITION BY key_id ORDER BY updated_at DESC) rn     FROM tabela_b b   ) y WHERE rn = 1 ) SELECT a_unico.key_id, a_unico.col1, b_unico.col2 FROM a_unico JOIN b_unico ON b_unico.key_id = a_unico.key_id;

Técnicas para evitar duplicidade e inflação (com quando usar)

1) Pré-agregar o lado “muitos” antes do JOIN

Quando usar: sempre que você precisa de métricas do lado “muitos” no grão do lado “um” (ex.: itens por pedido, pagamentos por pedido, tickets por cliente).

Vantagens: simplifica o JOIN para 1:1 e reduz volume.

WITH pagamentos_por_pedido AS (   SELECT order_id,          SUM(amount) AS total_pago,          COUNT(*) AS qtd_pagamentos   FROM payments   WHERE status = 'CONFIRMED'   GROUP BY order_id ) SELECT o.order_id, o.order_total, p.total_pago, p.qtd_pagamentos FROM orders o LEFT JOIN pagamentos_por_pedido p ON p.order_id = o.order_id;

2) Desduplicar com janela (ROW_NUMBER) e regra de escolha

Quando usar: dimensões com múltiplas linhas por chave (cadastro com versões, tabelas de eventos onde você quer “o último status”, etc.).

Ponto de atenção: a regra precisa ser defensável (mais recente, status prioritário, vigente na data do fato).

WITH ultimo_status AS (   SELECT * FROM (     SELECT s.*,            ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY status_datetime DESC) rn     FROM order_status_history s   ) t   WHERE rn = 1 ) SELECT o.order_id, o.order_date, us.status FROM orders o LEFT JOIN ultimo_status us ON us.order_id = o.order_id;

3) Usar EXISTS para filtrar sem multiplicar linhas

Quando usar: você só precisa saber se existe correspondência, não precisa trazer colunas do outro lado.

Benefício: evita multiplicação porque não materializa as combinações.

-- Pedidos que têm ao menos um item da categoria 'ELETRONICOS'  SELECT o.order_id, o.order_date, o.order_total  FROM orders o  WHERE EXISTS (   SELECT 1   FROM order_items oi   JOIN products pr ON pr.product_id = oi.product_id   WHERE oi.order_id = o.order_id     AND pr.category = 'ELETRONICOS' );

4) Contar/somar com cuidado: DISTINCT como “curativo”, não como solução padrão

Quando usar: em casos específicos, para contagens de entidades únicas após JOIN (ex.: contar pedidos distintos), ou quando você tem certeza de que a duplicidade é apenas efeito do JOIN e não um problema de modelagem.

Risco: SUM(DISTINCT valor) quase nunca é uma boa ideia em métricas financeiras, porque valores iguais em pedidos diferentes seriam colapsados indevidamente.

-- Contar pedidos após juntar com itens: use DISTINCT no identificador  SELECT COUNT(DISTINCT o.order_id) AS pedidos  FROM orders o  JOIN order_items oi ON oi.order_id = o.order_id;

Se você precisa somar receita por pedido, o correto é somar no grão de pedido (ou garantir 1:1), não aplicar DISTINCT em valores.

5) Validar chaves com “testes” em SQL (rotina de qualidade)

Em análises recorrentes, vale criar consultas de validação que você roda sempre que atualizar dados ou mudar JOINs. Exemplos de testes úteis:

  • Chave única em dimensões: HAVING COUNT(*) > 1
  • Chaves órfãs (fatos sem dimensão): pedidos com customer_id inexistente
  • Cardinalidade inesperada: pedidos com mais de 1 envio quando deveria ser 1
-- Chaves órfãs: pedidos sem cliente correspondente  SELECT o.customer_id, COUNT(*) AS qtd_pedidos  FROM orders o  LEFT JOIN customers c ON c.customer_id = o.customer_id  WHERE c.customer_id IS NULL  GROUP BY o.customer_id;
-- Cardinalidade inesperada: mais de 1 envio por pedido  SELECT order_id, COUNT(*) AS envios  FROM shipments  GROUP BY order_id  HAVING COUNT(*) > 1;

Diagnóstico guiado: como investigar um relatório que inflou

1) Reproduza o problema no menor recorte possível

Escolha uma chave específica (um pedido, um cliente, um dia) onde você sabe o valor correto e investigue apenas aquele recorte. Isso reduz ruído e acelera o diagnóstico.

-- Investigue um pedido específico  SELECT o.order_id, o.order_total, oi.product_id, oi.quantity, oi.unit_price  FROM orders o  JOIN order_items oi ON oi.order_id = o.order_id  WHERE o.order_id = 12345;

2) Conte quantas linhas surgem por chave após o JOIN

Se o resultado final deveria ter 1 linha por pedido, descubra quantas linhas por pedido você está gerando.

SELECT o.order_id, COUNT(*) AS linhas_apos_join  FROM orders o  JOIN order_items oi ON oi.order_id = o.order_id  GROUP BY o.order_id  ORDER BY linhas_apos_join DESC;

3) Identifique qual tabela está “multiplicando”

Se você tem múltiplos JOINs (ex.: pedidos + itens + pagamentos + cupons), a multiplicação pode vir de mais de um lado. Um padrão eficiente é ir juntando uma tabela por vez e medindo o crescimento.

-- Base  SELECT COUNT(*) FROM orders;  -- Após itens  SELECT COUNT(*)  FROM orders o  JOIN order_items oi ON oi.order_id = o.order_id;  -- Após itens + pagamentos  SELECT COUNT(*)  FROM orders o  JOIN order_items oi ON oi.order_id = o.order_id  JOIN payments p ON p.order_id = o.order_id;

Se ao adicionar payments o número de linhas dispara, você provavelmente criou um N:N (itens x pagamentos por pedido). Nesse caso, pré-agregue itens por pedido e pagamentos por pedido antes de juntar.

Exemplo completo: evitando N:N ao combinar itens e pagamentos

Cenário: você quer um relatório por pedido com: total do pedido, quantidade de itens, total pago e número de pagamentos confirmados. Tabelas:

  • orders: 1 linha por pedido
  • order_items: várias linhas por pedido
  • payments: várias linhas por pedido

Se você juntar as três diretamente, cada item combina com cada pagamento do mesmo pedido, criando N:N dentro do pedido. A solução é agregar order_items por pedido e payments por pedido, e então juntar tudo em 1:1.

WITH itens AS (   SELECT order_id,          SUM(quantity) AS qtd_itens,          SUM(quantity * unit_price) AS valor_itens   FROM order_items   GROUP BY order_id ), pagamentos AS (   SELECT order_id,          SUM(amount) AS total_pago,          COUNT(*) AS qtd_pagamentos   FROM payments   WHERE status = 'CONFIRMED'   GROUP BY order_id ) SELECT o.order_id,        o.order_date,        o.order_total,        i.qtd_itens,        i.valor_itens,        p.total_pago,        p.qtd_pagamentos FROM orders o LEFT JOIN itens i ON i.order_id = o.order_id LEFT JOIN pagamentos p ON p.order_id = o.order_id;

Agora você tem uma linha por pedido, sem multiplicação, e pode agregar por dia, por cliente, por canal etc. com segurança.

Regras de ouro para JOINs sem inflação

  • Regra 1: saiba o grão do seu resultado e mantenha as tabelas alinhadas a esse grão (pré-agregue quando necessário).
  • Regra 2: valide unicidade das chaves do lado “1” antes de confiar em 1:1 ou 1:N.
  • Regra 3: se você juntar duas tabelas “muitas” pelo mesmo identificador (ex.: itens e pagamentos por pedido), você está a um passo de um N:N; agregue primeiro.
  • Regra 4: use ROW_NUMBER() para escolher uma linha por chave quando houver histórico/versões.
  • Regra 5: prefira EXISTS quando só precisa de presença/ausência, não de colunas.
  • Regra 6: use COUNT(DISTINCT id) para contagens de entidades após JOIN; evite “consertar” soma com DISTINCT em valores.

Agora responda o exercício sobre o conteúdo:

Ao juntar orders (1 linha por pedido) com order_items (várias linhas por pedido), qual prática evita que a soma de order_total fique inflada?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Ao juntar 1:N, cada pedido pode se repetir para cada item, inflando somas no nível do pedido. A forma segura é manter o grão do resultado: pré-agregar o lado muitos para 1 linha por pedido e então fazer um JOIN 1:1.

Próximo capitúlo

Agregações e métricas essenciais com COUNT, SUM e AVG

Arrow Right Icon
Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.