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

Funil de conversão e taxas por etapa com eventos e status operacionais

Capítulo 17

Tempo estimado de leitura: 0 minutos

+ Exercício

Um funil de conversão descreve a passagem de entidades (geralmente usuários, leads, pedidos ou tickets) por uma sequência de etapas até um objetivo final, como “compra concluída”, “cadastro aprovado” ou “entrega realizada”. Em SQL para análise do dia a dia, o funil serve para responder perguntas como: quantos iniciam o processo, quantos avançam em cada etapa, onde ocorre a maior perda e qual é a taxa de conversão entre etapas e do início ao fim.

Neste capítulo, vamos construir funis usando duas fontes comuns em empresas: (1) eventos (logs de ações do usuário/sistema) e (2) status operacionais (mudanças de estado de um pedido, proposta, atendimento, entrega etc.). Embora pareçam semelhantes, eles têm diferenças importantes: eventos podem acontecer várias vezes e fora de ordem; status operacionais costumam representar um “estado atual” ou uma linha do tempo de transições que precisa ser consolidada para evitar contagens infladas.

O que define uma etapa de funil (e por que isso muda o resultado)

Antes de escrever SQL, defina com precisão o que é “passar” por uma etapa. Existem três padrões comuns:

  • Etapa por ocorrência: basta ter ao menos um evento/status daquela etapa (ex.: “visualizou página de checkout”).

  • Etapa por ordem: a etapa só conta se ocorreu após a etapa anterior (ex.: “pagamento iniciado” depois de “checkout iniciado”).

    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

  • Etapa por janela de tempo: a etapa só conta se ocorreu dentro de um período após a anterior (ex.: “pagamento aprovado” até 30 min após “pagamento iniciado”).

Também é essencial definir a unidade do funil (o “quem”): usuário, sessão, lead, pedido, entrega. Funil por usuário tende a ser mais “alto” (um usuário pode tentar várias vezes), enquanto funil por pedido tende a ser mais “operacional” (cada pedido percorre estados).

Modelo 1: Funil baseado em eventos (event logs)

Imagine uma tabela de eventos com colunas típicas:

  • user_id

  • event_time (timestamp)

  • event_name (ex.: view_product, add_to_cart, start_checkout, purchase)

  • session_id (opcional)

  • order_id (opcional, quando existe)

O desafio aqui é que um usuário pode disparar o mesmo evento várias vezes. Para funil, normalmente você quer contar uma vez por entidade em cada etapa (ex.: um usuário “atingiu” a etapa se fez o evento ao menos uma vez no período).

Passo a passo: funil simples por usuário (atingiu a etapa)

Objetivo: medir, em um período, quantos usuários fizeram cada evento-chave e as taxas por etapa. A estratégia é transformar eventos em uma tabela “wide” por usuário, com flags (0/1) para cada etapa.

WITH base AS (  SELECT    user_id,    event_name  FROM events  WHERE event_time >= DATE '2026-01-01'    AND event_time <  DATE '2026-02-01'    AND event_name IN ('view_product','add_to_cart','start_checkout','purchase')), per_user AS (  SELECT    user_id,    MAX(CASE WHEN event_name = 'view_product' THEN 1 ELSE 0 END) AS did_view,    MAX(CASE WHEN event_name = 'add_to_cart' THEN 1 ELSE 0 END) AS did_cart,    MAX(CASE WHEN event_name = 'start_checkout' THEN 1 ELSE 0 END) AS did_checkout,    MAX(CASE WHEN event_name = 'purchase' THEN 1 ELSE 0 END) AS did_purchase  FROM base  GROUP BY user_id)SELECT  'view_product' AS step,  SUM(did_view) AS usersFROM per_userUNION ALLSELECT  'add_to_cart' AS step,  SUM(did_cart) AS usersFROM per_userUNION ALLSELECT  'start_checkout' AS step,  SUM(did_checkout) AS usersFROM per_userUNION ALLSELECT  'purchase' AS step,  SUM(did_purchase) AS usersFROM per_user;

Esse funil responde “quantos usuários fizeram cada coisa”, mas não garante ordem. Um usuário pode ter “purchase” sem “start_checkout” no log (por falha de tracking) e ainda assim será contado na etapa final. Isso é útil para auditoria de instrumentação, mas pode distorcer conversões.

Taxas por etapa (conversão entre etapas)

Para taxas, você precisa de um denominador consistente. Em funis, é comum usar:

  • Conversão etapa a etapa: etapa atual / etapa anterior.

  • Conversão acumulada: etapa atual / primeira etapa.

Uma forma prática é produzir uma tabela com as contagens e depois calcular as taxas. Exemplo (mantendo a ideia simples):

WITH base AS (  SELECT    user_id,    event_name  FROM events  WHERE event_time >= DATE '2026-01-01'    AND event_time <  DATE '2026-02-01'    AND event_name IN ('view_product','add_to_cart','start_checkout','purchase')), per_user AS (  SELECT    user_id,    MAX(CASE WHEN event_name = 'view_product' THEN 1 ELSE 0 END) AS did_view,    MAX(CASE WHEN event_name = 'add_to_cart' THEN 1 ELSE 0 END) AS did_cart,    MAX(CASE WHEN event_name = 'start_checkout' THEN 1 ELSE 0 END) AS did_checkout,    MAX(CASE WHEN event_name = 'purchase' THEN 1 ELSE 0 END) AS did_purchase  FROM base  GROUP BY user_id), counts AS (  SELECT 'view_product' AS step, 1 AS step_order, SUM(did_view) AS users FROM per_user  UNION ALL  SELECT 'add_to_cart', 2, SUM(did_cart) FROM per_user  UNION ALL  SELECT 'start_checkout', 3, SUM(did_checkout) FROM per_user  UNION ALL  SELECT 'purchase', 4, SUM(did_purchase) FROM per_user)SELECT  step,  users,  users * 1.0 / NULLIF(LAG(users) OVER (ORDER BY step_order), 0) AS step_to_step_rate,  users * 1.0 / NULLIF(FIRST_VALUE(users) OVER (ORDER BY step_order), 0) AS cumulative_rateFROM countsORDER BY step_order;

Na prática, você vai querer formatar as taxas (por exemplo, multiplicar por 100) no seu BI ou na camada de apresentação.

Funil ordenado por tempo (garantindo sequência)

Quando a ordem importa, você precisa garantir que a etapa 2 ocorreu após a etapa 1 para o mesmo usuário (ou sessão/pedido). Uma abordagem robusta é capturar o primeiro timestamp de cada etapa e exigir que eles sejam crescentes.

Exemplo: funil por usuário, exigindo ordem temporal.

WITH filtered AS (  SELECT    user_id,    event_time,    event_name  FROM events  WHERE event_time >= DATE '2026-01-01'    AND event_time <  DATE '2026-02-01'    AND event_name IN ('view_product','add_to_cart','start_checkout','purchase')), first_times AS (  SELECT    user_id,    MIN(CASE WHEN event_name = 'view_product' THEN event_time END) AS t_view,    MIN(CASE WHEN event_name = 'add_to_cart' THEN event_time END) AS t_cart,    MIN(CASE WHEN event_name = 'start_checkout' THEN event_time END) AS t_checkout,    MIN(CASE WHEN event_name = 'purchase' THEN event_time END) AS t_purchase  FROM filtered  GROUP BY user_id), qualified AS (  SELECT    user_id,    CASE WHEN t_view IS NOT NULL THEN 1 ELSE 0 END AS s1_view,    CASE WHEN t_view IS NOT NULL AND t_cart IS NOT NULL AND t_cart >= t_view THEN 1 ELSE 0 END AS s2_cart,    CASE WHEN t_view IS NOT NULL AND t_cart IS NOT NULL AND t_checkout IS NOT NULL AND t_checkout >= t_cart THEN 1 ELSE 0 END AS s3_checkout,    CASE WHEN t_view IS NOT NULL AND t_cart IS NOT NULL AND t_checkout IS NOT NULL AND t_purchase IS NOT NULL AND t_purchase >= t_checkout THEN 1 ELSE 0 END AS s4_purchase  FROM first_times)SELECT  'view_product' AS step, SUM(s1_view) AS users FROM qualifiedUNION ALLSELECT  'add_to_cart', SUM(s2_cart) FROM qualifiedUNION ALLSELECT  'start_checkout', SUM(s3_checkout) FROM qualifiedUNION ALLSELECT  'purchase', SUM(s4_purchase) FROM qualified;

Esse padrão reduz falsos positivos e torna as taxas mais interpretáveis como “avanço no processo”. Em contrapartida, ele pode “punir” problemas de tracking (eventos faltando) e casos legítimos fora do padrão (ex.: compra via link direto sem passar por páginas intermediárias).

Funil por sessão (quando o usuário pode repetir o processo)

Se o usuário pode iniciar várias tentativas no período, funil por usuário mistura jornadas diferentes. Funil por sessão (ou por “tentativa”) costuma ser mais fiel. A lógica é a mesma, trocando user_id por session_id (ou outro identificador de tentativa). Se não existir sessão, às vezes você cria uma “tentativa” por heurística (ex.: reinicia após 30 min sem eventos), mas isso exige regras adicionais.

Modelo 2: Funil com status operacionais (pipeline/esteira)

Status operacionais são comuns em pedidos, entregas, propostas, tickets e processos internos. Em vez de “eventos de clique”, você tem transições de estado, como:

  • created (pedido criado)

  • paid (pagamento aprovado)

  • picking (separação)

  • shipped (enviado)

  • delivered (entregue)

  • canceled (cancelado)

O cuidado principal: uma mesma entidade pode ter múltiplas linhas de status ao longo do tempo. Se você simplesmente contar linhas, você infla o funil. O correto é consolidar por entidade e etapa (por exemplo, “o pedido atingiu o status X ao menos uma vez”).

Passo a passo: funil operacional por pedido (atingiu status)

Suponha uma tabela order_status_history com:

  • order_id

  • status

  • status_time

Queremos um funil: created → paid → shipped → delivered. E também queremos enxergar cancelamentos como “saída” do funil.

WITH filtered AS (  SELECT    order_id,    status,    status_time  FROM order_status_history  WHERE status_time >= DATE '2026-01-01'    AND status_time <  DATE '2026-02-01'    AND status IN ('created','paid','shipped','delivered','canceled')), per_order AS (  SELECT    order_id,    MAX(CASE WHEN status = 'created' THEN 1 ELSE 0 END) AS s_created,    MAX(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) AS s_paid,    MAX(CASE WHEN status = 'shipped' THEN 1 ELSE 0 END) AS s_shipped,    MAX(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) AS s_delivered,    MAX(CASE WHEN status = 'canceled' THEN 1 ELSE 0 END) AS s_canceled  FROM filtered  GROUP BY order_id)SELECT  'created' AS step, SUM(s_created) AS orders FROM per_orderUNION ALLSELECT  'paid', SUM(s_paid) FROM per_orderUNION ALLSELECT  'shipped', SUM(s_shipped) FROM per_orderUNION ALLSELECT  'delivered', SUM(s_delivered) FROM per_orderUNION ALLSELECT  'canceled', SUM(s_canceled) FROM per_order;

Esse relatório mostra volume por etapa, mas ainda não garante ordem. Em status operacionais, ordem costuma ser relevante (um pedido não deveria ser entregue antes de ser enviado), mas dados reais podem ter correções e reprocessamentos.

Funil operacional ordenado (usando o primeiro momento de cada status)

Para impor sequência, capture o primeiro timestamp de cada status e exija que seja crescente. Isso também ajuda a medir tempos entre etapas (SLA), embora aqui foquemos em conversão.

WITH filtered AS (  SELECT    order_id,    status,    status_time  FROM order_status_history  WHERE status_time >= DATE '2026-01-01'    AND status_time <  DATE '2026-02-01'    AND status IN ('created','paid','shipped','delivered','canceled')), first_times AS (  SELECT    order_id,    MIN(CASE WHEN status = 'created' THEN status_time END) AS t_created,    MIN(CASE WHEN status = 'paid' THEN status_time END) AS t_paid,    MIN(CASE WHEN status = 'shipped' THEN status_time END) AS t_shipped,    MIN(CASE WHEN status = 'delivered' THEN status_time END) AS t_delivered,    MIN(CASE WHEN status = 'canceled' THEN status_time END) AS t_canceled  FROM filtered  GROUP BY order_id), qualified AS (  SELECT    order_id,    CASE WHEN t_created IS NOT NULL THEN 1 ELSE 0 END AS s1_created,    CASE WHEN t_created IS NOT NULL AND t_paid IS NOT NULL AND t_paid >= t_created THEN 1 ELSE 0 END AS s2_paid,    CASE WHEN t_created IS NOT NULL AND t_paid IS NOT NULL AND t_shipped IS NOT NULL AND t_shipped >= t_paid THEN 1 ELSE 0 END AS s3_shipped,    CASE WHEN t_created IS NOT NULL AND t_paid IS NOT NULL AND t_shipped IS NOT NULL AND t_delivered IS NOT NULL AND t_delivered >= t_shipped THEN 1 ELSE 0 END AS s4_delivered,    CASE WHEN t_created IS NOT NULL AND t_canceled IS NOT NULL AND t_canceled >= t_created THEN 1 ELSE 0 END AS s_cancel_any  FROM first_times)SELECT  'created' AS step, SUM(s1_created) AS orders FROM qualifiedUNION ALLSELECT  'paid', SUM(s2_paid) FROM qualifiedUNION ALLSELECT  'shipped', SUM(s3_shipped) FROM qualifiedUNION ALLSELECT  'delivered', SUM(s4_delivered) FROM qualifiedUNION ALLSELECT  'canceled_anytime', SUM(s_cancel_any) FROM qualified;

Note que canceled_anytime não é uma etapa linear do funil (é um desvio). Em relatórios operacionais, é comum mostrar cancelamentos em paralelo e calcular taxas específicas, como “% cancelado antes do envio”.

Taxas por etapa com desvios (cancelado, devolvido, falha)

Funis reais raramente são uma linha reta. Há desvios como cancelamento, chargeback, devolução, reprovação antifraude, falha de pagamento. Uma forma prática de lidar com isso é definir:

  • Etapas principais (o caminho feliz).

  • Status de saída (motivos de perda) medidos em relação a uma etapa base.

Exemplo: “% cancelado antes de shipped” e “% cancelado após shipped”. Para isso, compare o timestamp do cancelamento com o timestamp do envio.

WITH filtered AS (  SELECT    order_id,    status,    status_time  FROM order_status_history  WHERE status_time >= DATE '2026-01-01'    AND status_time <  DATE '2026-02-01'    AND status IN ('created','paid','shipped','delivered','canceled')), first_times AS (  SELECT    order_id,    MIN(CASE WHEN status = 'created' THEN status_time END) AS t_created,    MIN(CASE WHEN status = 'shipped' THEN status_time END) AS t_shipped,    MIN(CASE WHEN status = 'canceled' THEN status_time END) AS t_canceled  FROM filtered  GROUP BY order_id), classified AS (  SELECT    order_id,    CASE WHEN t_created IS NOT NULL THEN 1 ELSE 0 END AS has_created,    CASE WHEN t_canceled IS NOT NULL AND (t_shipped IS NULL OR t_canceled < t_shipped) THEN 1 ELSE 0 END AS canceled_before_ship,    CASE WHEN t_canceled IS NOT NULL AND t_shipped IS NOT NULL AND t_canceled >= t_shipped THEN 1 ELSE 0 END AS canceled_after_ship  FROM first_times)SELECT  SUM(has_created) AS created_orders,  SUM(canceled_before_ship) AS canceled_before_ship_orders,  SUM(canceled_before_ship) * 1.0 / NULLIF(SUM(has_created), 0) AS canceled_before_ship_rate,  SUM(canceled_after_ship) AS canceled_after_ship_orders,  SUM(canceled_after_ship) * 1.0 / NULLIF(SUM(has_created), 0) AS canceled_after_ship_rateFROM classified;

Esse padrão é muito usado em operação porque separa perdas “cedo” (problema de pagamento, desistência) de perdas “tarde” (logística, ruptura, endereço).

Quando eventos e status coexistem: funil híbrido

Em muitos produtos, o topo do funil é digital (eventos) e o fundo é operacional (status). Exemplo: usuário inicia checkout (evento), cria pedido (registro transacional), pedido é pago/enviado/entregue (status). O cuidado aqui é escolher uma chave de ligação confiável (por exemplo, order_id gerado no checkout) e evitar duplicidades quando um evento se repete.

Uma abordagem comum é:

  • Consolidar eventos por order_id (ou por session_id que gerou o pedido).

  • Consolidar status por order_id.

  • Unir as duas consolidações em uma linha por pedido e calcular etapas.

WITH event_steps AS (  SELECT    order_id,    MAX(CASE WHEN event_name = 'start_checkout' THEN 1 ELSE 0 END) AS did_start_checkout,    MAX(CASE WHEN event_name = 'submit_payment' THEN 1 ELSE 0 END) AS did_submit_payment  FROM events  WHERE event_time >= DATE '2026-01-01'    AND event_time <  DATE '2026-02-01'    AND order_id IS NOT NULL    AND event_name IN ('start_checkout','submit_payment')  GROUP BY order_id), status_steps AS (  SELECT    order_id,    MAX(CASE WHEN status = 'created' THEN 1 ELSE 0 END) AS s_created,    MAX(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) AS s_paid,    MAX(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) AS s_delivered  FROM order_status_history  WHERE status_time >= DATE '2026-01-01'    AND status_time <  DATE '2026-02-01'    AND status IN ('created','paid','delivered')  GROUP BY order_id), per_order AS (  SELECT    COALESCE(s.order_id, e.order_id) AS order_id,    COALESCE(did_start_checkout, 0) AS did_start_checkout,    COALESCE(did_submit_payment, 0) AS did_submit_payment,    COALESCE(s_created, 0) AS s_created,    COALESCE(s_paid, 0) AS s_paid,    COALESCE(s_delivered, 0) AS s_delivered  FROM status_steps s  LEFT JOIN event_steps e ON e.order_id = s.order_id)SELECT  'start_checkout' AS step, SUM(did_start_checkout) AS orders FROM per_orderUNION ALLSELECT  'submit_payment', SUM(did_submit_payment) FROM per_orderUNION ALLSELECT  'created', SUM(s_created) FROM per_orderUNION ALLSELECT  'paid', SUM(s_paid) FROM per_orderUNION ALLSELECT  'delivered', SUM(s_delivered) FROM per_order;

Esse funil híbrido é útil para identificar “buracos” entre camadas: por exemplo, muitos submit_payment mas poucos paid pode indicar falhas no gateway; muitos start_checkout mas poucos created pode indicar abandono ou erro na criação do pedido.

Armadilhas comuns em funis com eventos e status (e como evitar)

1) Contagem inflada por múltiplas ocorrências

Se você contar linhas de eventos/status diretamente, um usuário com 10 cliques vira “10 conversões”. A correção é consolidar por entidade e etapa (flags) ou usar contagem distinta por entidade na etapa. Em funis, prefira sempre “atingiu a etapa ao menos uma vez”.

2) Etapas com definição ambígua

“Checkout iniciado” pode ser evento de página, evento de API, ou criação de carrinho. Defina a regra e documente. Em operação, “pago” pode significar “autorizado” ou “capturado”. Se misturar, a taxa vira ruído.

3) Período de análise mal definido

Você pode filtrar por data do evento (topo do funil) ou por data de criação do pedido (base transacional). Em status operacionais, filtrar por status_time pode incluir pedidos criados antes do período que apenas mudaram de status agora. Para relatórios de conversão, muitas vezes faz mais sentido ancorar no início do funil (ex.: pedidos criados no mês) e acompanhar o que aconteceu com eles depois, mesmo que o status final ocorra em outro mês. Isso exige uma regra de coorte operacional (ex.: “pedidos criados em janeiro”).

4) Reprocessamentos e regressões de status

Pedidos podem ir de “shipped” para “picking” por correção. Se você usar “primeiro timestamp” e exigir ordem, pode excluir casos. Se você usar “atingiu status”, pode contar etapas incompatíveis. Uma prática é escolher entre: (a) funil de “atingiu” (mais tolerante) ou (b) funil “sequencial” (mais estrito). Em operação, muitas vezes você mantém os dois: um para volume e outro para qualidade do fluxo.

5) Status de saída competindo com etapas finais

Um pedido pode ser “delivered” e depois “returned”. Se seu objetivo é “entregue”, retorno é um desvio pós-conversão. Se seu objetivo é “pedido bem-sucedido”, você pode redefinir a última etapa como “entregue e não devolvido em X dias”. Isso muda completamente a taxa final, então precisa estar explícito.

Checklist prático para montar um funil confiável

  • Escolha a entidade: usuário, sessão, pedido, ticket.

  • Liste etapas e desvios: caminho feliz e saídas (cancelado, falha, devolução).

  • Defina regra de passagem: ocorrência, ordem, janela de tempo.

  • Consolide antes de contar: uma linha por entidade com flags/timestamps por etapa.

  • Calcule taxas com denominadores claros: etapa anterior e primeira etapa.

  • Valide coerência: etapas não podem crescer; se crescerem, há duplicidade, definição errada ou tracking inconsistente.

  • Separe funil de produto e funil operacional: eventos explicam comportamento; status explicam execução.

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

Ao montar um funil usando histórico de status operacionais, qual prática evita contagens infladas quando um mesmo pedido tem várias linhas de status ao longo do tempo?

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

Você errou! Tente novamente.

Em status operacionais, um pedido pode aparecer várias vezes. Para não inflar o funil, consolida-se em uma linha por pedido com flags (ou timestamps) indicando se atingiu cada etapa, e só então se soma por etapa.

Próximo capitúlo

Validação de resultados e checagens de consistência para evitar erros em relatórios

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