Validar resultados em SQL significa aplicar checagens sistemáticas para confirmar que uma consulta está correta, completa e consistente com as regras de negócio antes de virar um relatório, dashboard ou decisão. Na prática, a maioria dos erros em relatórios não vem de “SQL que não roda”, e sim de SQL que roda e entrega números plausíveis, porém errados: duplicidades silenciosas, filtros incompletos, períodos desalinhados, mudanças de definição, registros fora do escopo, ou dados faltantes que passam despercebidos.
Neste capítulo, o foco é criar um repertório de validações rápidas e repetíveis. A ideia é tratar cada entrega como um pequeno processo de auditoria: você não precisa “desconfiar de tudo”, mas precisa ter um conjunto padrão de testes que detecte as falhas mais comuns com o menor esforço possível.
O que validar: tipos de consistência que evitam erros
1) Consistência de escopo (o que entra e o que fica de fora)
Todo relatório tem um escopo: período, unidade de negócio, status elegíveis, canais, países, produtos. Um erro frequente é o escopo ficar implícito na cabeça de quem escreve a query, mas não estar corretamente implementado (ou estar implementado de forma diferente do combinado). Validação de escopo é provar que o conjunto de linhas analisado é exatamente o esperado.
2) Consistência de granularidade (o nível de detalhe)
Granularidade é o “tamanho do grão” da tabela final: por dia? por pedido? por cliente? por item do pedido? Muitos números errados surgem quando você calcula uma métrica em um grão e exibe em outro, ou quando mistura tabelas em grãos diferentes sem perceber. Validar granularidade é garantir que cada linha do resultado representa uma entidade única e que as métricas foram agregadas no nível correto.
3) Consistência de chaves e unicidade (sem duplicidades silenciosas)
Mesmo que você já tenha cuidado com duplicidades em junções, a validação aqui é operacional: provar com testes que a chave que você está usando é única no resultado e que não houve multiplicação de linhas. Isso inclui checar se uma dimensão tem mais de um registro “ativo” para a mesma chave, se há versões, se há histórico, etc.
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...Baixar o aplicativo
4) Consistência temporal (datas, fusos, janelas e cortes)
Relatórios quebram com facilidade quando há datas em colunas diferentes (criação, pagamento, envio), fusos horários, ou quando o período é definido por “mês fechado” versus “mês corrente”. Validar consistência temporal é garantir que o recorte de datas está alinhado com a definição da métrica e que não há vazamentos (linhas fora do período) nem buracos (dias faltando).
5) Consistência de regras de negócio (definições e exceções)
Uma métrica como “receita” pode excluir cancelamentos, incluir frete, considerar apenas pagamentos aprovados, ou reconhecer receita na data de faturamento. A validação aqui é transformar a regra em testes: quantos registros estão em cada status? há valores negativos? há pedidos sem pagamento? há itens sem preço?
6) Consistência estatística (sanity checks)
São checagens de plausibilidade: totais não podem ser negativos (em geral), taxas não podem passar de 100%, ticket médio não pode ser maior do que o máximo observado, contagens não podem exceder o número de entidades possíveis. Não é “prova formal”, mas detecta rapidamente erros grosseiros.
Estratégia prática: um checklist de validação em camadas
Uma forma eficiente de validar é usar camadas, do mais simples ao mais específico. Você executa as checagens na ordem; se uma falhar, você corrige antes de avançar.
- Camada A — Escopo e volume: contagens básicas, datas mínimas/máximas, distribuição por status/canal.
- Camada B — Unicidade e duplicidade: chave do resultado é única? houve multiplicação de linhas?
- Camada C — Reconciliação de totais: totais batem com uma fonte de referência ou com uma query alternativa?
- Camada D — Regras de negócio e exceções: casos borda, valores nulos, negativos, outliers.
- Camada E — Reprodutibilidade: a query é estável, documentada e fácil de revalidar no próximo mês?
Passo a passo prático: validando um relatório de vendas (exemplo fictício)
Suponha um cenário comum: você precisa entregar um relatório mensal com receita, número de pedidos e ticket médio por canal e dia. Você já tem uma query que gera a tabela final. Agora você vai validar.
Vamos assumir tabelas fictícias: orders (pedido), order_items (itens), payments (pagamentos), channels (dimensão de canal). A validação não depende do nome exato, e sim do método.
Passo 1 — Fixe o contrato do relatório (definição explícita)
Antes de testar, escreva (nem que seja em comentário interno) a definição do que você está medindo. Exemplo de contrato:
- Período: mês calendário (00:00 do dia 1 até 23:59 do último dia) na data de pagamento aprovado.
- Receita: soma do valor pago aprovado (sem estornos), em moeda local.
- Pedidos: contagem de pedidos com pelo menos um pagamento aprovado no período.
- Canal: canal do pedido no momento da criação.
Esse contrato guia as checagens: se você filtrar por created_at em vez de paid_at, a validação temporal deve acusar divergência.
Passo 2 — Checagens de escopo e volume (sanity checks iniciais)
Comece verificando se o período está correto e se o volume parece razoável. Mesmo sem histórico, você consegue detectar erros como “puxei 2 anos em vez de 1 mês”.
SELECT MIN(paid_at) AS min_paid_at, MAX(paid_at) AS max_paid_at, COUNT(*) AS rows_payments_approved FROM payments WHERE status = 'approved' AND paid_at >= DATE '2025-01-01' AND paid_at < DATE '2025-02-01';O que observar:
- min/max devem cair dentro do mês esperado.
- count não deve ser absurdamente alto/baixo comparado ao normal (se você tiver referência).
Uma checagem complementar é ver a distribuição por status para garantir que você não está excluindo algo indevidamente:
SELECT status, COUNT(*) AS qtd FROM payments WHERE paid_at >= DATE '2025-01-01' AND paid_at < DATE '2025-02-01' GROUP BY status ORDER BY qtd DESC;Passo 3 — Valide a granularidade do resultado final
Se o relatório final é “por dia e canal”, então a chave natural do resultado é (dia, canal). Você deve garantir que não existem duplicatas dessa chave no output.
WITH report AS ( SELECT CAST(p.paid_at AS DATE) AS dia, o.channel_id, SUM(p.amount) AS receita, COUNT(DISTINCT o.order_id) AS pedidos FROM payments p JOIN orders o ON o.order_id = p.order_id WHERE p.status = 'approved' AND p.paid_at >= DATE '2025-01-01' AND p.paid_at < DATE '2025-02-01' GROUP BY 1,2) SELECT dia, channel_id, COUNT(*) AS linhas_por_chave FROM report GROUP BY 1,2 HAVING COUNT(*) > 1;Resultado esperado: zero linhas. Se aparecer algo, o seu GROUP BY não está alinhado com o grão ou você está juntando dimensões que duplicam.
Passo 4 — Teste de multiplicação de linhas (antes e depois de juntar)
Um erro comum é juntar orders com order_items e depois somar valores do pedido, inflando a receita pelo número de itens. Mesmo que você saiba disso, a validação é criar uma checagem que detecte multiplicação.
Compare contagens de pedidos antes e depois de uma junção suspeita:
WITH base AS ( SELECT DISTINCT o.order_id FROM orders o JOIN payments p ON p.order_id = o.order_id WHERE p.status = 'approved' AND p.paid_at >= DATE '2025-01-01' AND p.paid_at < DATE '2025-02-01'), joined AS ( SELECT o.order_id FROM base b JOIN orders o ON o.order_id = b.order_id JOIN order_items i ON i.order_id = o.order_id) SELECT (SELECT COUNT(*) FROM base) AS pedidos_base, (SELECT COUNT(*) FROM joined) AS linhas_apos_join, (SELECT COUNT(DISTINCT order_id) FROM joined) AS pedidos_distintos_apos_join;Interpretação:
- Se
linhas_apos_joinfor muito maior quepedidos_distintos_apos_join, a junção com itens multiplicou linhas (o que pode ser correto, mas exige agregação no nível certo). - Se o seu cálculo de receita usa uma coluna do pedido e você juntou itens, há risco de inflar. A validação serve para acender o alerta.
Passo 5 — Reconciliação de totais por duas abordagens independentes
Uma técnica poderosa é calcular o mesmo total de duas formas diferentes e comparar. Se bater, aumenta a confiança; se divergir, você investiga.
Abordagem A: receita total direto de pagamentos aprovados.
SELECT SUM(amount) AS receita_total FROM payments WHERE status = 'approved' AND paid_at >= DATE '2025-01-01' AND paid_at < DATE '2025-02-01';Abordagem B: receita total a partir do relatório agregado (somando o que você vai reportar).
WITH report AS ( SELECT CAST(p.paid_at AS DATE) AS dia, o.channel_id, SUM(p.amount) AS receita FROM payments p JOIN orders o ON o.order_id = p.order_id WHERE p.status = 'approved' AND p.paid_at >= DATE '2025-01-01' AND p.paid_at < DATE '2025-02-01' GROUP BY 1,2) SELECT SUM(receita) AS receita_total_report FROM report;Os dois totais devem ser iguais (ou diferir apenas por arredondamento, se houver). Se não bater:
- Você pode ter perdido pagamentos por causa de junção (ex.: pedidos sem canal válido).
- Você pode estar duplicando pagamentos (ex.: múltiplas linhas por transação).
- Você pode estar filtrando datas em colunas diferentes.
Passo 6 — Checagens de integridade referencial (dimensões faltantes)
Quando você segmenta por canal, produto, região, é comum “sumir” parte do total porque algumas linhas não encontram correspondência na dimensão (ou porque o join foi feito como INNER quando deveria preservar linhas). Uma validação simples é medir quantos registros ficam sem dimensão.
SELECT COUNT(*) AS pagamentos, SUM(CASE WHEN o.channel_id IS NULL THEN 1 ELSE 0 END) AS sem_canal FROM payments p LEFT JOIN orders o ON o.order_id = p.order_id WHERE p.status = 'approved' AND p.paid_at >= DATE '2025-01-01' AND p.paid_at < DATE '2025-02-01';Se sem_canal > 0, você decide: o relatório deve agrupar em “Canal desconhecido”? Deve corrigir a origem? Deve excluir explicitamente e documentar?
Passo 7 — Checagens de consistência de métricas derivadas (taxas e médias)
Métricas como ticket médio e taxa de conversão são especialmente sensíveis a erros de denominador. Valide limites e coerência:
- Ticket médio > 0 (na maioria dos contextos).
- Taxas entre 0 e 1 (ou 0% e 100%).
- Pedidos ≤ pagamentos (dependendo do modelo) ou pelo menos coerente com a regra.
Exemplo de checagem para ticket médio por canal no mês:
WITH report AS ( SELECT o.channel_id, SUM(p.amount) AS receita, COUNT(DISTINCT o.order_id) AS pedidos FROM payments p JOIN orders o ON o.order_id = p.order_id WHERE p.status = 'approved' AND p.paid_at >= DATE '2025-01-01' AND p.paid_at < DATE '2025-02-01' GROUP BY 1) SELECT channel_id, receita, pedidos, CASE WHEN pedidos = 0 THEN NULL ELSE receita * 1.0 / pedidos END AS ticket_medio FROM report WHERE pedidos = 0 OR receita < 0 OR (receita * 1.0 / NULLIF(pedidos,0)) <= 0;Se aparecerem linhas, investigue: pode haver estornos incluídos, pedidos com pagamento zero, ou erro de filtro.
Passo 8 — Procure buracos e duplicações no tempo (dias faltando)
Relatórios diários podem “pular” dias por ausência de dados ou por erro de filtro. Uma checagem útil é listar dias presentes e comparar com o calendário esperado (mesmo sem uma tabela calendário, você pode ao menos verificar min/max e contagem de dias distintos).
SELECT COUNT(DISTINCT CAST(paid_at AS DATE)) AS dias_com_pagamento, MIN(CAST(paid_at AS DATE)) AS primeiro_dia, MAX(CAST(paid_at AS DATE)) AS ultimo_dia FROM payments WHERE status = 'approved' AND paid_at >= DATE '2025-01-01' AND paid_at < DATE '2025-02-01';Se você espera um mês completo e só aparecem poucos dias, pode ser:
- Filtro de data incorreto (coluna errada, timezone, limites inclusivos/exclusivos).
- Dados ainda não carregados (pipeline atrasado).
- Status aprovado não existe para parte do período (mudança operacional).
Passo 9 — Testes de casos borda (amostras e drill-down)
Depois das checagens agregadas, valide com “drill-down”: pegue um dia e um canal com valor alto e confira os registros base. O objetivo é garantir que o agregado é explicável por linhas reais.
-- Escolha um dia/canal suspeito e liste alguns pedidos e pagamentos SELECT o.order_id, p.payment_id, p.amount, p.paid_at, o.channel_id FROM payments p JOIN orders o ON o.order_id = p.order_id WHERE p.status = 'approved' AND CAST(p.paid_at AS DATE) = DATE '2025-01-15' AND o.channel_id = 3 ORDER BY p.amount DESC LIMIT 50;O que procurar:
- Pagamentos duplicados para o mesmo pedido (parcelas? retentativas?).
- Valores inesperados (muito altos/baixos).
- Datas fora do esperado (pagamento em outro mês mas caindo no filtro por conversão de fuso).
Checagens reutilizáveis (templates) para o dia a dia
Template 1 — Unicidade de chave no resultado
Use sempre que o resultado final tem uma chave definida (ex.: dia+canal, mês+produto, cliente+mês).
WITH final AS ( -- sua query final aqui ) SELECT chave1, chave2, COUNT(*) AS qtd FROM final GROUP BY 1,2 HAVING COUNT(*) > 1;Template 2 — Reconciliação: total do relatório vs total da fonte
WITH final AS ( -- sua query final aqui ), fonte AS ( -- cálculo direto na fonte, o mais simples possível ) SELECT (SELECT SUM(metrica) FROM final) AS total_final, (SELECT SUM(metrica) FROM fonte) AS total_fonte, (SELECT SUM(metrica) FROM final) - (SELECT SUM(metrica) FROM fonte) AS diff;Template 3 — Distribuição por categorias críticas (status, tipo, origem)
Ótimo para detectar filtros errados e mudanças de processo.
SELECT categoria, COUNT(*) AS qtd, SUM(valor) AS soma_valor FROM base WHERE periodo_filtro GROUP BY categoria ORDER BY qtd DESC;Template 4 — Detecção de outliers e valores inválidos
SELECT * FROM base WHERE valor < 0 OR valor IS NULL OR valor > limite_esperado;Erros comuns e como as validações os capturam
Filtro de data no campo errado
Sintoma: total do mês parece “ok”, mas distribuição diária fica estranha, ou o fechamento não bate com o financeiro. Checagens que pegam: min/max de datas, dias distintos, reconciliação por fonte (pagamentos vs pedidos).
Perda de linhas por junção com dimensão
Sintoma: total segmentado é menor que o total geral. Checagens que pegam: contagem de registros sem dimensão, comparação de total antes/depois de juntar dimensão, reconciliação de totais.
Duplicidade por relacionamento 1:N não tratado
Sintoma: receita inflada, contagens maiores que o possível. Checagens que pegam: teste de multiplicação de linhas, unicidade da chave no resultado, comparação de totais por duas abordagens.
Mudança silenciosa de definição (status, regra de elegibilidade)
Sintoma: quebra de série histórica ou salto repentino. Checagens que pegam: distribuição por status, comparação de volumes por categoria, amostras (drill-down) em dias de pico.
Como incorporar validação no fluxo de trabalho (sem virar burocracia)
Para a validação ser sustentável, ela precisa ser rápida e repetível. Algumas práticas simples ajudam:
- Crie uma seção “checks” no seu arquivo de query: mantenha consultas de validação logo abaixo da query principal, prontas para rodar.
- Padronize 5 a 8 checagens para a maioria dos relatórios: escopo, unicidade, reconciliação, dimensões faltantes, outliers, distribuição por status.
- Registre números de controle: total do mês, total por canal, contagem de pedidos. Na próxima atualização, compare rapidamente.
- Separe “cálculo” de “apresentação”: quando possível, valide primeiro uma tabela base (fato) e depois as segmentações.
- Documente exceções: se você decidiu excluir um status raro, ou agrupar “desconhecido”, deixe explícito para evitar regressões.
Mini-roteiro de validação para qualquer relatório
Use este roteiro como sequência padrão:
- (1) Escopo: min/max de datas, contagem de linhas, contagem de entidades principais.
- (2) Granularidade: chave do resultado é única?
- (3) Duplicidade: contagem antes/depois de junções críticas.
- (4) Reconciliação: total do relatório bate com total calculado direto na fonte?
- (5) Dimensões: quantos registros ficam sem correspondência em dimensões?
- (6) Sanity: limites (taxas 0–100%), valores negativos, nulos em campos essenciais.
- (7) Drill-down: amostra de um ponto alto e um ponto baixo para explicar o agregado.