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

Projeto aplicado com vendas: receita, ticket médio, mix de produtos e sazonalidade

Capítulo 22

Tempo estimado de leitura: 0 minutos

+ Exercício

Neste projeto aplicado, você vai construir um conjunto de análises de vendas que respondem perguntas recorrentes do dia a dia: quanto faturamos (receita), qual é o ticket médio, como está o mix de produtos (participação por categoria/sku) e como a sazonalidade afeta o desempenho ao longo do tempo. A proposta é sair de métricas isoladas e montar um “pacote” de consultas que se conectam: primeiro garantimos uma base confiável de vendas, depois derivamos indicadores e, por fim, detalhamos o que explica as variações (mix e sazonalidade).

Escopo do projeto e perguntas de negócio

Você vai produzir consultas que respondem, no mínimo, a estas perguntas:

  • Receita: qual foi a receita bruta e a receita líquida por período?
  • Ticket médio: quanto, em média, cada pedido gerou de receita?
  • Mix de produtos: quais categorias e produtos mais contribuem para a receita e para a quantidade vendida?
  • Sazonalidade: quais meses/semanas/dias têm picos e vales? Existe padrão por dia da semana?
  • Diagnóstico: quando a receita muda, foi por volume (número de pedidos), por preço (ticket) ou por mudança de mix?

Para manter o projeto prático, vamos assumir um modelo transacional típico de e-commerce/PDV com pedidos e itens. Ajuste os nomes das tabelas/colunas conforme seu banco.

Modelo de dados mínimo (referência)

Considere as tabelas abaixo como referência conceitual:

  • orders: order_id, customer_id, order_date, status, channel, store_id
  • order_items: order_id, product_id, quantity, unit_price, discount_amount
  • products: product_id, product_name, category, brand
  • returns (opcional): order_id, product_id, return_qty, return_amount

O ponto central do projeto é transformar linhas de itens em métricas de pedido e de período. Em análises de receita e ticket, a unidade de análise precisa estar clara: receita pode ser somada em nível de item; ticket médio normalmente é receita por pedido (não por item).

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

Passo a passo 1: construir a base de vendas (fato de itens)

Comece criando uma visão lógica (ou CTE) que calcule o valor do item e normalize o que será considerado receita. Uma prática comum é separar:

  • Receita bruta: quantity * unit_price
  • Desconto: discount_amount (por item ou rateado)
  • Receita líquida: receita bruta - desconto - devoluções (se aplicável)

Exemplo (sem devoluções):

WITH item_sales AS (  SELECT     oi.order_id,     o.order_date,     o.status,     o.channel,     oi.product_id,     oi.quantity,     oi.unit_price,     COALESCE(oi.discount_amount, 0) AS discount_amount,     (oi.quantity * oi.unit_price) AS gross_revenue,     (oi.quantity * oi.unit_price) - COALESCE(oi.discount_amount, 0) AS net_revenue   FROM order_items oi   JOIN orders o     ON o.order_id = oi.order_id   WHERE o.status IN ('paid','shipped','delivered') ) SELECT * FROM item_sales;

Notas práticas:

  • Defina claramente quais status entram como venda (ex.: excluir cancelados e pendentes).
  • Se desconto estiver em nível de pedido, você precisará ratear pelos itens (por proporção do valor do item). Isso muda o cálculo e deve ser documentado.
  • Se existir devolução, trate como ajuste negativo de receita (ou crie receita líquida “pós-devolução”).

Passo a passo 2: transformar itens em métricas por pedido (base para ticket)

Ticket médio exige consolidar itens por pedido. Crie uma segunda camada agregando por order_id. Isso também ajuda a separar “quantidade de pedidos” de “quantidade de itens”.

WITH item_sales AS (  SELECT     oi.order_id,     o.order_date,     o.channel,     oi.product_id,     oi.quantity,     (oi.quantity * oi.unit_price) AS gross_revenue,     COALESCE(oi.discount_amount, 0) AS discount_amount,     (oi.quantity * oi.unit_price) - COALESCE(oi.discount_amount, 0) AS net_revenue   FROM order_items oi   JOIN orders o ON o.order_id = oi.order_id   WHERE o.status IN ('paid','shipped','delivered') ), order_sales AS (  SELECT     order_id,     MIN(order_date) AS order_date,     MIN(channel) AS channel,     SUM(gross_revenue) AS gross_revenue,     SUM(discount_amount) AS discount_amount,     SUM(net_revenue) AS net_revenue,     SUM(quantity) AS items_qty,     COUNT(DISTINCT product_id) AS distinct_products   FROM item_sales   GROUP BY order_id ) SELECT * FROM order_sales;

O que você ganha com essa camada:

  • Ticket fica direto: net_revenue por pedido.
  • Você consegue medir complexidade do carrinho: itens_qty e distinct_products.
  • Você pode segmentar por canal/loja sem duplicar pedidos (desde que channel seja consistente por pedido).

Receita por período (diária, semanal, mensal)

Com a base por pedido, você pode gerar relatórios de receita por período. A granularidade depende do uso: diário para operação, semanal para acompanhamento tático, mensal para planejamento.

Receita e pedidos por mês

WITH order_sales AS (  SELECT     o.order_id,     o.order_date,     o.channel,     SUM(oi.quantity * oi.unit_price) - SUM(COALESCE(oi.discount_amount,0)) AS net_revenue   FROM orders o   JOIN order_items oi ON oi.order_id = o.order_id   WHERE o.status IN ('paid','shipped','delivered')   GROUP BY o.order_id, o.order_date, o.channel ) SELECT   DATE_TRUNC('month', order_date) AS month,   SUM(net_revenue) AS revenue,   COUNT(*) AS orders,   SUM(net_revenue) / NULLIF(COUNT(*),0) AS avg_ticket FROM order_sales GROUP BY 1 ORDER BY 1;

Interpretação:

  • revenue mostra o total do período.
  • orders mostra volume.
  • avg_ticket ajuda a entender se a receita subiu por mais pedidos ou por pedidos maiores.

Se seu banco não suportar DATE_TRUNC, substitua por funções equivalentes (por exemplo, construir “ano-mês” com YEAR/MONTH ou FORMAT).

Ticket médio: variações e segmentações úteis

Ticket médio pode ser enganoso se você misturar canais, lojas ou tipos de pedido. Em vez de um único número, crie cortes que façam sentido operacional.

Ticket por canal

WITH order_sales AS (  SELECT     o.order_id,     o.order_date,     o.channel,     SUM(oi.quantity * oi.unit_price) - SUM(COALESCE(oi.discount_amount,0)) AS net_revenue   FROM orders o   JOIN order_items oi ON oi.order_id = o.order_id   WHERE o.status IN ('paid','shipped','delivered')   GROUP BY o.order_id, o.order_date, o.channel ) SELECT   channel,   COUNT(*) AS orders,   SUM(net_revenue) AS revenue,   SUM(net_revenue) / NULLIF(COUNT(*),0) AS avg_ticket FROM order_sales GROUP BY channel ORDER BY revenue DESC;

Uso prático:

  • Se o ticket do canal “app” cair, pode indicar mais compras de reposição (mix mais barato) ou promoções.
  • Se o ticket do canal “loja” subir, pode ser efeito de upsell ou mudança no sortimento.

Distribuição de ticket (faixas)

Média esconde dispersão. Uma análise simples é criar faixas de ticket para ver concentração.

WITH order_sales AS (  SELECT     o.order_id,     SUM(oi.quantity * oi.unit_price) - SUM(COALESCE(oi.discount_amount,0)) AS net_revenue   FROM orders o   JOIN order_items oi ON oi.order_id = o.order_id   WHERE o.status IN ('paid','shipped','delivered')   GROUP BY o.order_id ), bucketed AS (  SELECT     order_id,     net_revenue,     CASE       WHEN net_revenue < 50 THEN '0-49'       WHEN net_revenue < 100 THEN '50-99'       WHEN net_revenue < 200 THEN '100-199'       WHEN net_revenue < 500 THEN '200-499'       ELSE '500+'     END AS ticket_bucket   FROM order_sales ) SELECT   ticket_bucket,   COUNT(*) AS orders,   SUM(net_revenue) AS revenue FROM bucketed GROUP BY ticket_bucket ORDER BY orders DESC;

Isso ajuda a responder: a receita vem de muitos pedidos pequenos ou poucos pedidos grandes?

Mix de produtos: participação por categoria e por produto

“Mix” é a composição das vendas. Você pode olhar mix por:

  • Receita: participação no faturamento.
  • Quantidade: participação em unidades.
  • Pedidos: em quantos pedidos o item aparece (penetração).

O cuidado aqui é não confundir “quantidade de itens” com “quantidade de pedidos com aquele item”. Para penetração, conte pedidos distintos.

Mix por categoria (receita e unidades)

WITH item_sales AS (  SELECT     o.order_date,     p.category,     oi.product_id,     oi.quantity,     (oi.quantity * oi.unit_price) - COALESCE(oi.discount_amount,0) AS net_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   WHERE o.status IN ('paid','shipped','delivered') ) , totals AS (  SELECT     SUM(net_revenue) AS total_revenue,     SUM(quantity) AS total_units   FROM item_sales ) SELECT   category,   SUM(net_revenue) AS revenue,   SUM(quantity) AS units,   SUM(net_revenue) / NULLIF((SELECT total_revenue FROM totals),0) AS revenue_share,   SUM(quantity) / NULLIF((SELECT total_units FROM totals),0) AS unit_share FROM item_sales GROUP BY category ORDER BY revenue DESC;

Leituras comuns:

  • Categoria com revenue_share alto e unit_share baixo tende a ter preço médio maior.
  • Categoria com unit_share alto e revenue_share baixo pode ser “geradora de tráfego” (itens baratos e frequentes).

Top produtos por receita com participação acumulada (curva ABC)

Uma forma prática de priorizar é medir quanto do faturamento vem dos produtos líderes e qual a concentração.

WITH item_sales AS (  SELECT     oi.product_id,     SUM((oi.quantity * oi.unit_price) - COALESCE(oi.discount_amount,0)) AS revenue   FROM orders o   JOIN order_items oi ON oi.order_id = o.order_id   WHERE o.status IN ('paid','shipped','delivered')   GROUP BY oi.product_id ), ranked AS (  SELECT     product_id,     revenue,     SUM(revenue) OVER () AS total_revenue,     SUM(revenue) OVER (ORDER BY revenue DESC) AS cum_revenue   FROM item_sales ) SELECT   product_id,   revenue,   revenue / NULLIF(total_revenue,0) AS revenue_share,   cum_revenue / NULLIF(total_revenue,0) AS cum_share,   CASE     WHEN (cum_revenue / NULLIF(total_revenue,0)) <= 0.80 THEN 'A'     WHEN (cum_revenue / NULLIF(total_revenue,0)) <= 0.95 THEN 'B'     ELSE 'C'   END AS abc_class FROM ranked ORDER BY revenue DESC;

Aplicações:

  • Classe A: foco em disponibilidade, preço e campanhas.
  • Classe C: cauda longa; pode ser importante para variedade, mas com menor impacto no faturamento.

Penetração: produtos mais presentes nos pedidos

Um produto pode não ser top em receita, mas aparecer em muitos pedidos (alto “attach rate”). Isso é útil para cross-sell e layout de loja.

WITH item_orders AS (  SELECT     oi.product_id,     oi.order_id   FROM orders o   JOIN order_items oi ON oi.order_id = o.order_id   WHERE o.status IN ('paid','shipped','delivered')   GROUP BY oi.product_id, oi.order_id ), totals AS (  SELECT COUNT(DISTINCT order_id) AS total_orders   FROM orders   WHERE status IN ('paid','shipped','delivered') ) SELECT   product_id,   COUNT(*) AS orders_with_product,   COUNT(*) / NULLIF((SELECT total_orders FROM totals),0) AS penetration FROM item_orders GROUP BY product_id ORDER BY orders_with_product DESC;

Sazonalidade: como medir e comparar períodos

Sazonalidade é a repetição de padrões ao longo do tempo (mês, semana, dia da semana, feriados). Para análise prática, você quer:

  • Identificar picos e vales por período.
  • Comparar o período atual com um período anterior equivalente (mês contra mês anterior, ou mesmo mês do ano anterior).
  • Separar efeito de volume (pedidos) e efeito de ticket.

Sazonalidade mensal: receita, pedidos e ticket lado a lado

WITH order_sales AS (  SELECT     o.order_id,     o.order_date,     SUM(oi.quantity * oi.unit_price) - SUM(COALESCE(oi.discount_amount,0)) AS net_revenue   FROM orders o   JOIN order_items oi ON oi.order_id = o.order_id   WHERE o.status IN ('paid','shipped','delivered')   GROUP BY o.order_id, o.order_date ), monthly AS (  SELECT     DATE_TRUNC('month', order_date) AS month,     SUM(net_revenue) AS revenue,     COUNT(*) AS orders,     SUM(net_revenue) / NULLIF(COUNT(*),0) AS avg_ticket   FROM order_sales   GROUP BY 1 ) SELECT   month, revenue, orders, avg_ticket FROM monthly ORDER BY month;

Esse relatório é a “espinha dorsal” da sazonalidade. A partir dele, você pode aprofundar com comparações e decomposições.

Índice sazonal: mês como % da média do ano

Um jeito simples de ver sazonalidade é comparar cada mês com a média mensal do ano (ou da série). Isso cria um índice: acima de 1 indica mês forte; abaixo de 1 indica mês fraco.

WITH order_sales AS (  SELECT     o.order_id,     o.order_date,     SUM(oi.quantity * oi.unit_price) - SUM(COALESCE(oi.discount_amount,0)) AS net_revenue   FROM orders o   JOIN order_items oi ON oi.order_id = o.order_id   WHERE o.status IN ('paid','shipped','delivered')   GROUP BY o.order_id, o.order_date ), monthly AS (  SELECT     DATE_TRUNC('month', order_date) AS month,     EXTRACT(YEAR FROM order_date) AS year,     EXTRACT(MONTH FROM order_date) AS month_num,     SUM(net_revenue) AS revenue   FROM order_sales   GROUP BY 1,2,3 ), yearly_avg AS (  SELECT     year,     AVG(revenue) AS avg_month_revenue   FROM monthly   GROUP BY year ) SELECT   m.year,   m.month_num,   m.revenue,   m.revenue / NULLIF(y.avg_month_revenue,0) AS seasonal_index FROM monthly m JOIN yearly_avg y ON y.year = m.year ORDER BY m.year, m.month_num;

Uso prático:

  • Planejamento de estoque e metas: meses com índice 1,3 tendem a exigir mais capacidade.
  • Comparações justas: não comparar um mês historicamente fraco com um mês historicamente forte sem ajustar expectativa.

Sazonalidade por dia da semana

Para operação, dia da semana costuma ser mais acionável do que mês. Você pode medir receita média por dia da semana e também pedidos.

WITH order_sales AS (  SELECT     o.order_id,     o.order_date,     SUM(oi.quantity * oi.unit_price) - SUM(COALESCE(oi.discount_amount,0)) AS net_revenue   FROM orders o   JOIN order_items oi ON oi.order_id = o.order_id   WHERE o.status IN ('paid','shipped','delivered')   GROUP BY o.order_id, o.order_date ) SELECT   EXTRACT(DOW FROM order_date) AS dow,   COUNT(*) AS orders,   SUM(net_revenue) AS revenue,   AVG(net_revenue) AS avg_ticket FROM order_sales GROUP BY 1 ORDER BY 1;

Observação: o mapeamento de DOW varia por banco (domingo=0 ou segunda=1). Ajuste a interpretação e, se necessário, crie uma tabela de apoio para nomes dos dias.

Diagnóstico de variação: receita mudou por volume, ticket ou mix?

Quando a receita sobe ou cai, as causas mais comuns são:

  • Volume: mais/menos pedidos.
  • Ticket: pedidos maiores/menores (preço, desconto, quantidade por pedido).
  • Mix: participação maior de categorias caras/baratas.

Uma decomposição simples e muito usada no dia a dia é colocar lado a lado, por período, receita, pedidos e ticket. Se a receita caiu e os pedidos ficaram estáveis, o ticket caiu. Se o ticket ficou estável e os pedidos caíram, é volume. Se ambos mudaram pouco, mas categorias mudaram bastante, é mix.

Relatório mensal com mix por categoria (para explicar mudanças)

WITH item_sales AS (  SELECT     DATE_TRUNC('month', o.order_date) AS month,     p.category,     (oi.quantity * oi.unit_price) - COALESCE(oi.discount_amount,0) AS net_revenue,     oi.quantity AS units   FROM orders o   JOIN order_items oi ON oi.order_id = o.order_id   JOIN products p ON p.product_id = oi.product_id   WHERE o.status IN ('paid','shipped','delivered') ), month_totals AS (  SELECT     month,     SUM(net_revenue) AS total_revenue   FROM item_sales   GROUP BY month ) SELECT   s.month,   s.category,   SUM(s.net_revenue) AS revenue,   SUM(s.units) AS units,   SUM(s.net_revenue) / NULLIF(t.total_revenue,0) AS revenue_share FROM item_sales s JOIN month_totals t ON t.month = s.month GROUP BY s.month, s.category, t.total_revenue ORDER BY s.month, revenue DESC;

Como usar para diagnóstico:

  • Compare meses consecutivos: se a receita caiu, veja se a revenue_share migrou para categorias mais baratas.
  • Se a participação de uma categoria de alto valor caiu, o ticket tende a cair mesmo com pedidos estáveis.
  • Se unidades subiram mas receita não, pode haver aumento de descontos ou troca para itens mais baratos.

Checklist de entrega do projeto (consultas finais)

Ao final, você deve ter um conjunto de consultas reutilizáveis:

  • Base de itens com receita bruta, desconto e receita líquida (item_sales).
  • Base por pedido com receita líquida e contagens (order_sales).
  • Receita, pedidos e ticket por mês (e por canal, se aplicável).
  • Mix por categoria com participação no faturamento e em unidades.
  • Top produtos por receita com participação acumulada (ABC).
  • Penetração por produto (pedidos com o produto / total de pedidos).
  • Sazonalidade: índice mensal e padrão por dia da semana.
  • Relatório mensal de mix por categoria para explicar variações de receita.

Esse pacote cobre o essencial para relatórios recorrentes e também para investigações rápidas quando “algo mudou” nas vendas. A partir daqui, você consegue plugar filtros por canal, loja, região, campanha e criar versões comparáveis por período, mantendo a mesma lógica de base.

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

Ao calcular ticket médio em uma análise de vendas, qual abordagem garante a unidade correta de análise?

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

Você errou! Tente novamente.

Ticket médio é uma métrica por pedido. Para calculá-lo corretamente, primeiro consolide os itens por order_id (receita líquida do pedido) e então divida a receita total pela quantidade de pedidos.

Próximo capitúlo

Projeto aplicado com atendimento: SLAs, filas, resolução e qualidade de serviço

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