6.7Capítulo 7 de 8

SQL para RevOps: seis construções resolvem noventa por cento das perguntas

6.7 SQL para RevOps: seis construções resolvem noventa por cento das perguntas

Pergunta antes de ler

Abaixo está uma tabela de 12 oportunidades e, mais adiante, uma tabela de 6 itens de contrato. Antes de ler qualquer código, responda com dois números: qual a soma do valor dos negócios ganhos, e qual a soma do valor dos negócios ganhos depois que você juntar as duas tabelas pela chave da oportunidade?

Quase todo mundo escreve o mesmo número duas vezes, porque juntar tabelas não deveria criar dinheiro. Escreva assim mesmo, e depois confira.

Os dois números são R$ 145,2 mil e R$ 331,2 mil. A segunda conta inventou R$ 186 mil sem emitir nenhum erro, sem nenhum aviso e sem que nada na tela ficasse vermelho. Esse é o erro de SQL que mais dinheiro fantasma já criou em relatório de receita, e ele é o motivo pelo qual este capítulo não começa por sintaxe. Começa por dor.

Uma advertência sobre o que este capítulo é e o que ele não é. Ele não vai fazer de você engenheiro de dados, e não precisa. Existem seis construções de SQL, e elas resolvem quase tudo que uma pessoa de RevOps precisa perguntar a uma base. A ordem em que elas aparecem aqui não é a ordem de um manual de sintaxe: é a ordem em que a dor aparece na sua semana. Cada uma entra como resposta a uma pergunta que você já tentou responder na planilha e não conseguiu.

A tabela-brinquedo, publicada por extenso

Todas as consultas deste capítulo rodam sobre a mesma tabela de 12 linhas impressa abaixo. Ela é pequena de propósito: você consegue conferir qualquer resultado no olho, o que significa que nunca vai precisar acreditar no código. Ela não reproduz o ano da Órbita, e não deveria: 12 linhas não contam nada sobre uma operação de 640 oportunidades. Ela reproduz a estrutura, que é o que se aprende. Os nomes de tabela e de coluna são os do modelo canônico que o Módulo 5 especificou, então o que você escrever aqui vale na base de verdade.

oportunidade_idconta_idsegmentoorigemdata_criacaodata_fechamento_realacvresultado
OP-1001C-0412midinbound2025-01-092025-04-15R$ 60 milganha
OP-1002C-0418midoutbound2025-01-222025-05-30R$ 74 milperdida
OP-1003C-0431smbinbound2025-02-032025-03-14R$ 19,2 milganha
OP-1004C-0447midevento2025-02-172025-07-21R$ 96 milperdida
OP-1005C-0452smbmídia paga2025-03-022025-04-02R$ 16,8 milperdida
OP-1006C-0460midinbound2025-03-192025-08-08R$ 51 milperdida
OP-1007C-0473midindicação2025-04-072025-06-25R$ 66 milganha
OP-1008C-0486smbmídia paga2025-04-282025-05-19R$ 21 milperdida
OP-1009C-0418midoutbound2025-05-122025-10-09R$ 83 milperdida
OP-1010C-0412midinbound2025-06-042025-09-16R$ 47 milperdida
OP-1011C-0517smbinbound2025-06-23(vazio)R$ 18 milaberta
OP-1012C-0524midevento2025-07-01(vazio)R$ 112 milaberta

Tabela fato_oportunidade, 12 linhas, recorte didático. A coluna acv é o valor anual de um contrato, de um cliente só, e não um total da empresa. Repare em duas coisas antes de seguir: a conta C-0412 aparece duas vezes e a conta C-0418 também, porque uma conta pode ter mais de um negócio; e duas linhas têm a data de fechamento vazia, que é diferente de zero e diferente de perdida.

oportunidade_iditemvalor_itemPor que esta linha existe
OP-1001Módulo baseR$ 32 milPrimeiro módulo do contrato. Sozinho, não é o valor do negócio.
OP-1001RoteirizaçãoR$ 18 milSegundo módulo do mesmo negócio. Aqui nasce a duplicata.
OP-1001Auditoria de freteR$ 10 milTerceiro módulo. O negócio OP-1001 agora tem três linhas.
OP-1007Módulo baseR$ 42 milOutro negócio, dois módulos.
OP-1007RoteirizaçãoR$ 24 milA segunda linha do OP-1007.
OP-1003Módulo baseR$ 19,2 milNegócio de módulo único. É o único que não duplica nada.

Tabela fato_contrato_item, 6 linhas. Existe uma linha por módulo contratado, e não uma por negócio. É essa diferença de grão que produz o número fantasma da pergunta de abertura.

Grão da linha (grain)

Grão é o que uma linha da tabela representa, dito em uma frase: uma oportunidade, uma mudança de etapa, um item de contrato, uma assinatura-mês. Toda tabela tem um grão, saibam disso ou não as pessoas que a construíram, e toda consulta produz uma saída que também tem um.

A disciplina que resolve quase tudo: antes de escrever qualquer JOIN, declare em comentário qual é o grão da saída. Depois do JOIN, confira se o COUNT(*) mudou. Se mudou e não deveria, você tem fan-out. Duas linhas de disciplina que valem mais que qualquer curso de sintaxe.

Fan-out de JOIN

Multiplicação de linhas que acontece quando se junta uma tabela de um para muitos sem agregar antes. Cada item de contrato duplica a linha da oportunidade, e qualquer soma feita sobre uma coluna da oportunidade passa a contar o mesmo valor tantas vezes quantos forem os itens.

Por que é silencioso: o total só fica maior, nunca menor, e nunca dá erro de execução. Um relatório de receita inflado parece um bom trimestre. Ninguém audita um bom trimestre.

Da pergunta ao SQL: seis ramosOrdenados pela frequência com que a pergunta aparece, não pela dificuldade da sintaxeQuais linhas satisfazem este critério?SELECT com WHEREQuantos, no total, por alguma coisa?GROUP BY com agregaçãoDe onde veio, juntando duas tabelas?JOIN com grão declarado!fan-outA conta tem três etapas encadeadas.CTE em sequênciaCresceu contra o anterior? Acumulado até aqui?LAG e SUM OVERPor safra de entrada, ao longo da vida?DATE_TRUNC e meses!data-âncoraNenhum dos seis ramos exige entender o banco. Todos exigem saber que pergunta você está fazendo.

Arraste o desenho para o lado para ver inteiro

Figura 6.7 SQL não é um vocabulário. É um roteador acionado pelo formato da pergunta. Leia da esquerda para a direita: a pergunta de gestor à esquerda, a construção que a responde à direita. O mapa é para consultar enquanto você escreve, e a ordem das linhas é a ordem em que essas perguntas costumam chegar à sua mesa, não a ordem de um manual. Os dois marcadores vermelhos são os pontos em que uma consulta correta na sintaxe devolve um número errado: junção sem grão declarado e coorte sem data-âncora definida.

Construção 1. A dor: o relatório de abertos mistura vivo com morto

A primeira dor não é analítica. É operacional. O relatório nativo do CRM lista negócios abertos, e dentro dessa lista convivem o negócio que está em negociação de verdade e o negócio que morreu há meses e ninguém fechou. Enquanto os dois estiverem na mesma lista, a cobertura de pipeline é ficção e o win rate sobe sozinho, porque o denominador encolhe sem que ninguém mexa nele.

Quais negócios abertos já passaram de dois ciclos medianos do próprio segmento e portanto deveriam estar fechados como perdidos?

-- Grão da saída: uma linha por oportunidade aberta. Nenhum JOIN, nenhum risco.
-- Os limites vêm do ciclo mediano de cada segmento, não de um número redondo.
SELECT  oportunidade_id,
        conta_id,
        segmento,
        acv,
        data_criacao,
        CURRENT_DATE - data_criacao AS dias_aberto
FROM    fato_oportunidade
WHERE   resultado = 'aberta'
  AND   CURRENT_DATE - data_criacao > CASE segmento
                                        WHEN 'smb' THEN 76
                                        WHEN 'mid' THEN 188
                                        WHEN 'enterprise' THEN 372
                                      END
ORDER BY dias_aberto DESC;
Na tabela-brinquedo, lida em 31/dez/25, a consulta devolve uma linha: OP-1011, do SMB, aberta há 191 dias contra um limite de 76 dias. OP-1012 fica de fora, e é isso que faz a consulta valer alguma coisa: ela está aberta há 183 dias, o que assusta, mas é mid-market e o limite dela é 188 dias. As duas estão na mesma lista do CRM com o mesmo rótulo. A régua do segmento separa uma da outra, e sem essa separação você fecha negócio vivo ou mantém negócio morto.

Duas notas de dialeto, e elas valem para o capítulo inteiro. Em PostgreSQL a subtração entre duas datas devolve um número inteiro de dias, como está escrito. Em BigQuery você escreve DATE_DIFF(CURRENT_DATE(), data_criacao, DAY). A lógica é a mesma e a diferença é de vocabulário. Se a sua ferramenta é Metabase apontado para uma réplica, é PostgreSQL. Se é um BigQuery de camada gratuita, é o segundo. Não decore os dois: escreva no que você tem e procure o outro quando precisar.

Construção 2. A dor: o win rate da empresa não existe

A segunda dor é a do capítulo 6.1 deste módulo, agora com ferramenta. Você precisa do win rate por contagem e por valor, lado a lado, por segmento, com o denominador absoluto ao lado de cada porcentagem. Isso é uma tabela dinâmica que a planilha faz, e a planilha faz mal: ela não guarda a definição, então quem abrir o arquivo amanhã vai remontar a dinâmica com outros filtros e chegar a outro número, sem saber que chegou.

Qual o win rate por contagem e por valor de cada segmento, considerando apenas negócios que já tiveram desfecho?

-- Grão da saída: uma linha por segmento. Nenhum JOIN.
-- WHERE fora do GROUP BY: "resolvidas" é o denominador declarado desta métrica.
SELECT  segmento,
        COUNT(*)                                        AS resolvidas,
        COUNT(*) FILTER (WHERE resultado = 'ganha')     AS ganhas,
        ROUND(100.0 * COUNT(*) FILTER (WHERE resultado = 'ganha')
                    / COUNT(*), 1)                      AS wr_contagem_pct,
        SUM(acv)                                        AS valor_resolvido,
        SUM(acv) FILTER (WHERE resultado = 'ganha')     AS valor_ganho,
        ROUND(100.0 * SUM(acv) FILTER (WHERE resultado = 'ganha')
                    / SUM(acv), 1)                      AS wr_valor_pct
FROM    fato_oportunidade
WHERE   resultado IN ('ganha', 'perdida')
GROUP BY segmento
ORDER BY segmento;
Mid-market: 28,6% por contagem (2 de 7) e 26,4% por valor (R$ 126 mil de R$ 477 mil). A divergência de 2,2 pontos é a informação, não um arredondamento: as duas maiores perdas do recorte valem R$ 96 mil e R$ 83 mil por negócio, e o time está perdendo os negócios grandes na proporção em que ganha os pequenos. Por contagem isso é invisível. Publique as duas linhas sempre, e nunca uma sem a outra.

O FILTER é PostgreSQL. Em BigQuery e em vários outros bancos, a forma equivalente é COUNTIF(resultado = 'ganha') e SUM(CASE WHEN resultado = 'ganha' THEN acv ELSE 0 END). A segunda forma funciona em toda parte e é a que vale aprender se você só vai aprender uma.

Construção 3. A dor: o total do painel não bate com o do financeiro

Agora a terceira, que é a que custa dinheiro. Você precisa juntar duas tabelas, e no instante em que faz isso o número deixa de bater. Na Órbita esse não é um problema hipotético: a empresa convive com quatro versões do ARR em sistemas diferentes, e um relatório novo que devolve um quinto número não é recebido como descoberta. É recebido como confirmação de que não dá para confiar no dado.

Quanto de receita a empresa ganhou nos negócios fechados, quando os detalhes de contrato estão numa segunda tabela?

-- ERRADO. Grão de entrada: uma linha por oportunidade.
-- Grão da saída depois do JOIN: uma linha por ITEM. Ninguém declarou, ninguém viu.
SELECT SUM(o.acv) AS receita_ganha
FROM   fato_oportunidade o
JOIN   fato_contrato_item i ON i.oportunidade_id = o.oportunidade_id
WHERE  o.resultado = 'ganha';
-- devolve 331.200

-- CERTO, jeito 1: some a coluna do grão em que você realmente está.
SELECT SUM(i.valor_item) AS receita_ganha
FROM   fato_oportunidade o
JOIN   fato_contrato_item i ON i.oportunidade_id = o.oportunidade_id
WHERE  o.resultado = 'ganha';
-- devolve 145.200

-- CERTO, jeito 2: agregue ANTES de juntar, e o grão da saída volta a ser a oportunidade.
WITH itens_por_negocio AS (
  SELECT oportunidade_id, SUM(valor_item) AS valor_itens
  FROM   fato_contrato_item
  GROUP BY oportunidade_id
)
SELECT SUM(o.acv) AS receita_ganha
FROM   fato_oportunidade o
JOIN   itens_por_negocio t ON t.oportunidade_id = o.oportunidade_id
WHERE  o.resultado = 'ganha';
-- devolve 145.200

-- O TESTE. Rode antes e depois de qualquer JOIN. Se mudou e não deveria, pare.
SELECT COUNT(*) FROM fato_oportunidade WHERE resultado = 'ganha';          -- 3
SELECT COUNT(*) FROM fato_oportunidade o
JOIN fato_contrato_item i ON i.oportunidade_id = o.oportunidade_id
WHERE o.resultado = 'ganha';                                              -- 6
A primeira consulta devolve R$ 331,2 mil. A segunda e a terceira devolvem R$ 145,2 mil, que é o número correto. A diferença de R$ 186 mil não veio de nenhum erro de digitação: veio de o JOIN ter mudado o grão da saída de oportunidade para item de contrato sem que ninguém declarasse isso. O teste que pega o erro está na última linha do bloco, e custa três segundos.
Fan-out: o mesmo dinheiro contado três vezesNenhum erro de execução, nenhum aviso, nenhuma célula vermelhaAntes do JOIN: 3 linhasOP-1001 · R$ 60 milOP-1003 · R$ 19,2 milOP-1007 · R$ 66 milSUM(acv) = R$ 145,2 milJOINDepois do JOIN: 6 linhasOP-1001 · Módulo base · o.acv = R$ 60 milOP-1001 · Roteirização · o.acv = R$ 60 milOP-1001 · Auditoria de frete · o.acv = R$ 60 milOP-1007 · Módulo base · o.acv = R$ 66 milOP-1007 · Roteirização · o.acv = R$ 66 milOP-1003 · Módulo base · o.acv = R$ 19,2 milSUM(o.acv) = R$ 331,2 mil+ R$ 186 milreceita quenão existeCOUNT(*) antes: 3. Depois: 6. Esse é o teste inteiro, e ele cabe em duas linhas.

Arraste o desenho para o lado para ver inteiro

Figura 6.7-b A junção não somou nada. Ela repetiu, e a soma fez o resto. À esquerda, os 3 negócios ganhos, um por linha, somando R$ 145,2 mil. À direita, as mesmas oportunidades depois do JOIN com a tabela de itens: 6 linhas, porque um negócio com três módulos vira três linhas. A seta vermelha é o vazamento, e ele é ao contrário do usual: em vez de perder receita, o relatório cria receita que não existe. O valor do negócio está inteiro em cada uma das repetições, e SUM não tem como saber disso.

Construção 4. A dor: a conta tem três etapas e a consulta virou ilegível

A quarta dor aparece quando a pergunta tem etapas. Você precisa calcular o ciclo de cada negócio, depois separar por resultado, depois tirar a mediana e o percentil 90 de cada grupo. Tentar fazer isso numa consulta só produz uma subconsulta dentro de outra subconsulta, ilegível para você amanhã e impossível de revisar por outra pessoa. A CTE resolve isso nomeando cada etapa. Não é otimização: é redação.

Aninhado contra encadeadoSubconsultas aninhadas: leitura de dentro para foracomece a ler aquipasso 3passo 2passo 1 fica por fora, e é o último a ser lidoCTE encadeada: leitura de cima para baixoresolvidas: calcula o ciclo de cada negóciopor_grupo: separa por segmento e resultadofinal: tira mediana e percentil 90Consulta que só você consegue ler é um ativo de uma pessoa só. A Órbita já tem um desses, e ele não tirou férias em 2025.

Arraste o desenho para o lado para ver inteiro

Figura 6.7-c A CTE não deixa a consulta mais rápida. Deixa ela revisável. À esquerda, a mesma lógica escrita em subconsultas aninhadas: para entender, a leitura começa no centro e vai para fora, que é o oposto de como se lê. À direita, três CTEs nomeadas em fila, cada uma com um nome que diz o que ela entrega. O resultado é idêntico e o tempo de execução é praticamente o mesmo. O que muda é que a versão da direita pode ser conferida por alguém que não escreveu, e a da esquerda, na prática, não.

Qual o ciclo mediano e o percentil 90 de cada segmento, medidos separadamente sobre ganhos e sobre perdas?

-- Grão da saída: uma linha por combinação de segmento e resultado.
WITH resolvidas AS (
  SELECT  oportunidade_id,
          segmento,
          resultado,
          data_fechamento_real - data_criacao AS ciclo_dias
  FROM    fato_oportunidade
  WHERE   resultado IN ('ganha', 'perdida')
)
SELECT  segmento,
        resultado,
        COUNT(*)                                                  AS negocios,
        PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY ciclo_dias)   AS mediana_dias,
        PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY ciclo_dias)   AS p90_dias,
        ROUND(AVG(ciclo_dias), 1)                                 AS media_dias
FROM    resolvidas
GROUP BY segmento, resultado
ORDER BY segmento, resultado;
No mid-market da tabela-brinquedo, a mediana dos ganhos é 87,5 dias e a das perdas é 142,0 dias. A mediana de tudo que foi resolvido é 128,0 dias. Quem publica só o número dos ganhos entrega à diretoria um ciclo 40,5 dias mais curto do que o tempo real que a operação leva para resolver um negócio, e esse número vai virar denominador da velocidade de pipeline e régua de maturidade de coorte. Um erro, dois destinos. Nesta tabela as perdas demoram mais que os ganhos; na sua base pode ser o contrário. A lição não é a direção. É que você não sabe qual é até calcular as duas.

Construção 5. A dor: cresceu em relação ao mês passado, e quantas contas são mesmo?

A quinta construção é a que a planilha faz e faz mal. Comparar cada linha com a anterior, na planilha, é escrever uma fórmula que aponta para a célula de cima. A fórmula funciona até alguém ordenar a tabela por outra coluna, e a partir daí ela continua funcionando e passa a estar errada. As funções de janela fazem a mesma coisa sem depender da posição física da linha: a ordem é declarada dentro da própria função, e por isso ela sobrevive a qualquer reordenação.

O ARR novo de cada mês de 2025 cresceu em relação ao mês anterior, e quanto já está acumulado até cada ponto do ano? E quantas contas distintas a empresa realmente trabalhou, e não quantos negócios abriu?

-- Grão da saída: uma linha por mês. LAG olha para trás, SUM OVER acumula.
SELECT  mes,
        acv_novo,
        LAG(acv_novo) OVER (ORDER BY mes)                      AS mes_anterior,
        acv_novo - LAG(acv_novo) OVER (ORDER BY mes)           AS variacao,
        ROUND(100.0 * (acv_novo::numeric
              / LAG(acv_novo) OVER (ORDER BY mes) - 1), 1)     AS variacao_pct,
        SUM(acv_novo) OVER (ORDER BY mes
              ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS acumulado
FROM    arr_novo_mensal
ORDER BY mes;

-- Mesma família, outro problema: a primeira oportunidade de cada conta.
-- Grão da saída: uma linha por conta. ROW_NUMBER numera dentro de cada partição.
WITH ordenadas AS (
  SELECT  oportunidade_id,
          conta_id,
          data_criacao,
          resultado,
          ROW_NUMBER() OVER (PARTITION BY conta_id ORDER BY data_criacao) AS ordem
  FROM    fato_oportunidade
)
SELECT  COUNT(*) AS contas_trabalhadas
FROM    ordenadas
WHERE   ordem = 1;
-- devolve 10, contra 12 linhas na tabela de origem
Duas leituras saem daqui e as duas viram decisão. A primeira: o único mês de queda relevante é jul/25, com -10,0%, e a maior alta do ano é dez/25 com 37,5%. Antes de comemorar dezembro, repare que ele é o último mês do ano fiscal e que a coluna acumulada fecha em R$ 6,9 milhões, que é exatamente o ARR novo do ano. Dezembro não é demanda: é assinatura empurrada para dentro do prazo. A segunda leitura: a tabela tem 12 linhas e 10 contas. Quem publica “contas trabalhadas” contando oportunidades infla o número em 20,0%.
mesacv_novo (R$ mil)variacao (R$ mil)variacao_pctacumulado (R$ mil)Leitura
jan/25380380Base do ano. Não há mês anterior para comparar.
fev/254406015,8%820Retomada normal depois de janeiro.
mar/25480409,1%1.300Fechamento de trimestre. Parte disso é calendário, não demanda.
abr/25520408,3%1.820Crescimento limpo, sem efeito de fim de trimestre.
mai/25560407,7%2.380Crescimento limpo.
jun/25600407,1%2.980Fechamento de trimestre outra vez.
jul/25540-60-10,0%3.520Única queda relevante do ano. Investigue antes de explicar.
ago/25580407,4%4.100Recuperação parcial. Não devolve o que julho tirou.
set/256608013,8%4.760Fechamento de trimestre e melhor mês antes de dezembro.
out/25620-40-6,1%5.380Queda pequena, no padrão de início de trimestre.
nov/25640203,2%6.020Praticamente estável.
dez/2588024037,5%6.900Fim do ano fiscal. Assinatura empurrada, não demanda nova.

Saída da primeira consulta sobre o ARR novo mensal da Órbita de 2025. Valores em R$ mil: 380 é R$ 380 mil, e o acumulado que fecha em 6.900 é R$ 6,9 milhões, que é o ARR novo do ano inteiro. A última coluna é a que impede o gráfico de ser lido como tendência: três dos doze meses têm explicação de calendário, e nenhuma delas é sobre o desempenho do funil.

Construção 6. A dor: qual safra está pior, e desde quando

A sexta é a do capítulo 6.5, agora executável. Você quer fixar a safra pela data de entrada, acompanhar o que aconteceu com ela e comparar safras no mesmo ponto da vida. Em SQL isso são duas ideias: DATE_TRUNC, que joga cada data para o começo do período e assim cria o rótulo da safra, e uma diferença de meses entre a data do evento e a data de entrada, que cria o mês de vida. Com essas duas colunas, a tabela triangular é um GROUP BY.

Qual a conversão acumulada até o ganho de cada safra mensal de criação, e quais safras ainda não podem ser lidas?

-- Grão da saída: uma linha por safra mensal de criação.
-- A data-âncora é data_criacao. Se fosse data_fechamento_real, a pergunta seria outra.
WITH safras AS (
  SELECT  DATE_TRUNC('month', data_criacao) AS safra,
          resultado
  FROM    fato_oportunidade
)
SELECT  safra,
        COUNT(*)                                            AS criadas,
        COUNT(*) FILTER (WHERE resultado <> 'aberta')       AS resolvidas,
        COUNT(*) FILTER (WHERE resultado = 'aberta')        AS ainda_abertas,
        COUNT(*) FILTER (WHERE resultado = 'ganha')         AS ganhas,
        ROUND(100.0 * COUNT(*) FILTER (WHERE resultado = 'ganha')
              / NULLIF(COUNT(*) FILTER (WHERE resultado <> 'aberta'), 0), 1)
                                                            AS conversao_acumulada_pct
FROM    safras
GROUP BY safra
ORDER BY safra;

-- Para a tabela triangular de retenção, o mês de vida sai da mesma ideia:
--   DATE_PART('month', AGE(mes_da_cobranca, mes_de_entrada))
--   + 12 * DATE_PART('year', AGE(mes_da_cobranca, mes_de_entrada))  AS mes_de_vida
-- Em BigQuery: DATE_DIFF(mes_da_cobranca, mes_de_entrada, MONTH).
A safra mar/25 converteu 0,0% (0 de 2) e a jul/25 não tem leitura nenhuma, porque a única oportunidade dela continua aberta. Repare no que a coluna de abertas faz com o denominador: no total, a conversão sai 30,0% (3 de 10). Feche OP-1011 como perdida, que é o que a construção 1 manda fazer, e ela cai para 27,3% (3 de 11). Um único negócio morto parado em aberto vale 2,7 pontos de win rate de graça. Numa base de 640 oportunidades com higiene inexistente, imagine.
safracriadasresolvidasabertasganhasconversãoO que a linha permite dizer
jan/25220150,0%Denominador de 2. Serve para conferir a consulta, não para decidir.
fev/25220150,0%Mesmo caso: duas linhas não sustentam porcentagem.
mar/2522000,0%Zero de verdade: as duas foram resolvidas e as duas se perderam.
abr/25220150,0%Safra madura pelos dois ciclos medianos de cada segmento.
mai/2511000,0%Uma linha. Sem o (0 de 1) ao lado, publicar 0,0% é desonesto.
jun/2521100,0%A aberta é o negócio parado da construção 1, fora do denominador.
jul/251010Sem resolvidas. A conversão não é zero: é inexistente.

Saída da consulta de coorte sobre a tabela-brinquedo. Duas células de conversão ficam vazias e elas são a diagonal imatura em miniatura: a safra de julho não tem nenhum negócio resolvido, então a conversão dela não é zero, é inexistente. Publicar zero ali seria o mesmo erro do capítulo 6.5, em escala menor.

ConstruçãoO que fazEquivalente na planilhaOnde a planilha quebra
SELECT com WHEREEscolhe colunas e filtra linhasFiltro da barra de ferramentasQuando o critério muda por linha, vira coluna auxiliar e ninguém sabe quantas existem
GROUP BY com agregaçãoAgrupa e conta, soma ou tira médiaTabela dinâmicaA dinâmica não guarda a definição: quem abrir amanhã remonta com outros filtros
JOINJunta tabelas por uma chavePROCV ou PROCXPROCV devolve a primeira ocorrência e esconde o um para muitos; o JOIN mostra
CTENomeia cada etapa do raciocínioAbas intermediáriasAba intermediária não é versionável e ninguém sabe qual está atualizada
LAG, SUM OVER, ROW_NUMBERCompara com a linha anterior, acumula, numera dentro do grupoFórmula que aponta para a célula de cimaQuebra em silêncio quando alguém reordena ou filtra a tabela
DATE_TRUNC com diferença de mesesMonta safra e mês de vida, e com isso a tabela de coortesDinâmica com coluna de mês de vida calculada à mãoO mês de vida precisa ser refeito a cada safra nova, e é onde a diagonal imatura entra sem ser vista

As seis construções, o equivalente na planilha e o ponto exato em que a planilha para de servir. A coluna da direita é a que importa: quase todo mundo que resiste a aprender SQL está certo sobre a planilha resolver o caso de hoje, e errado sobre ela resolver o caso de daqui a três meses.

Na práticaÓrbita Software · a mesma pergunta, três denominadores

A pergunta que chegou. O CRO quer o win rate do mid-market para a reunião de conselho. Na tabela-brinquedo, o mid tem 7 negócios resolvidos e 2 ganhos.

Resposta 1, por contagem sobre resolvidas. 2 ÷ 7 = 28,6% (2 de 7). É a leitura padrão e é honesta, desde que o denominador venha impresso ao lado.

Resposta 2, por valor. R$ 126 mil ÷ R$ 477 mil = 26,4%. Menor que a primeira em 2,2 pontos, porque as perdas do recorte são maiores que os ganhos. As duas juntas dizem uma coisa que nenhuma delas diz sozinha: o time ganha com a frequência esperada e perde os negócios que pagam a conta.

Resposta 3, por conta e não por negócio. Das 10 contas da tabela, 8 tiveram desfecho e 3 viraram cliente. 3 ÷ 8 = 37,5% (3 de 8). O numerador é o mesmo das outras duas. O denominador mudou porque a pergunta mudou: quantas empresas viraram cliente, e não quantos negócios foram ganhos.

A leitura. Três números para a mesma etapa do mesmo funil, e nenhum deles está errado. Eles respondem a “com que frequência ganhamos”, a “quanto da receita disputada capturamos” e a “quantas empresas convertemos”. O SQL não escolhe por você: ele obriga você a escrever o denominador, e é aí que a escolha aparece.

O veredito. A ficha de métrica do capítulo 6.1 e a consulta SQL são o mesmo artefato em dois formatos. Se a ficha estiver escrita, a consulta se escreve quase sozinha. Se não estiver, nenhuma quantidade de SQL resolve, porque o problema nunca foi a ferramenta.

Exercício 6.775 minutos, um editor de SQL e a tabela-brinquedo

Escreva as consultas de verdade, num editor. Todas as respostas podem ser conferidas no olho, contando linhas da tabela-brinquedo.

  1. Escreva a consulta que devolve, por origem, quantos negócios foram resolvidos e quantos foram ganhos. Declare o grão da saída em comentário antes de escrever.
  2. Troque o JOIN da construção 3 por um LEFT JOIN. Quantas linhas saem? O que acontece com SUM(i.valor_item) e o que acontece com COUNT(*)?
  3. Escreva a consulta que devolve o tempo mediano que um negócio passa em cada etapa. Se não for possível com esta tabela, diga o que falta e por quê.
  4. Escreva uma consulta sintaticamente correta que devolva um número errado sobre a conversão do funil, e depois escreva a regra que impediria alguém de publicá-la.
  5. Item adversarial. Alguém argumenta que ensinar SQL a RevOps é errado, porque transforma a função no balcão de extração de dados da empresa. Construa o argumento na versão mais forte e depois responda.
Conferir respostas
  1. Origens. É um GROUP BY com duas agregações condicionais. O esqueleto: SELECT origem, COUNT(*) AS resolvidas, COUNT(*) FILTER (WHERE resultado = 'ganha') AS ganhas FROM fato_oportunidade WHERE resultado IN ('ganha', 'perdida') GROUP BY origem ORDER BY ganhas DESC; O comentário obrigatório é o grão da saída: uma linha por origem. Na tabela-brinquedo, inbound tem 4 resolvidas e 2 ganhas, outbound 2 e 0, mídia paga 2 e 0, evento 1 e 0 e indicação 1 e 1. E a resposta madura inclui a ressalva: com denominadores de 1 a 4, nenhuma dessas porcentagens deve ser publicada.
  2. Fan-out ao contrário. Um LEFT JOIN de oportunidade com itens devolve 15 linhas: as 6 do inner join mais as 9 oportunidades sem nenhum item. SUM(i.valor_item) continua dando R$ 145,2 mil, porque os nulos não somam. Mas COUNT(i.item) e COUNT(*) passam a divergir, e quem usa COUNT(*) para contar itens conta as linhas vazias também. Regra: COUNT(*) conta linhas, COUNT(coluna) conta valores não nulos, e a diferença entre os dois é exatamente o que o LEFT JOIN trouxe.
  3. Tempo em etapa. Não dá, e reconhecer isso é a resposta. A tabela tem a data de criação e a de fechamento, e nada entre as duas. Tempo em etapa exige uma tabela de histórico de mudança de etapa, com uma linha por transição, e o cálculo é a diferença entre a data de entrada numa etapa e a data de entrada na seguinte, por LAG dentro da partição da oportunidade. Se a sua base não registra transição, o número não existe e nenhuma consulta o cria.
  4. A consulta que mente. Qualquer coisa que use o estágio atual como se fosse fluxo. Por exemplo, contar quantas oportunidades estão hoje em cada etapa e dividir uma contagem pela outra: a consulta executa, o número sai em porcentagem e ele mede tempo de permanência disfarçado de conversão. A defesa é escrita e não técnica: toda métrica publicada declara se é estoque ou fluxo, e o capítulo 6.2 diz por quê.
  5. O argumento contra você. A crítica mais forte é a de custo de oportunidade: uma pessoa de RevOps que aprende SQL passa a ser a pessoa que extrai dado, e extrair dado é trabalho que não escala e que sequestra o tempo de quem deveria estar desenhando processo. É uma crítica boa e ela descreve um destino real. A resposta tem duas partes. Primeira, o objetivo do capítulo não é você virar o extrator: é você conseguir escrever a especificação e conferir o resultado de quem extrai, e ninguém confere uma consulta que não sabe ler. Segunda, a defesa contra virar o balcão de pedidos não é ignorar SQL. É a mesma de sempre: dicionário de métrica publicado, relatório recorrente com janela fixa e uma fila de pedidos com critério. Sem SQL você não escapa do balcão; você só não entende o que está entregando.

Armadilha: publicar um número de receita vindo de uma junção sem grão declarado

A cena é a reunião mensal de receita. Você construiu o primeiro relatório decente que a empresa já teve, juntando oportunidades com itens de contrato para abrir a receita por módulo. O total aparece na tela. A CFO olha, compara com a planilha dela e diz que não bate. Você refaz, e continua não batendo, porque o erro não está numa linha: está no grão.

O custo não é o retrabalho. A Órbita já convive com quatro versões conflitantes do ARR: a planilha da CFO fecha em R$ 24,8 milhões, o painel do Looker mostra R$ 23,2 milhões porque foi construído sobre um export antigo, e há mais duas leituras diferentes no ERP e no CRM. Numa empresa assim, um quinto número não é recebido como descoberta. É recebido como prova de que o novo modelo de dados também não fecha, e a pessoa que você mais precisava ter do seu lado, a única da diretoria que quer um número em que possa assinar embaixo, volta para a planilha dela e não sai mais.

O que protege contra isso não é talento. É uma linha de comentário antes do JOIN dizendo qual é o grão da saída, e duas linhas de COUNT(*) depois dele. Custa menos de um minuto por consulta.

Um relatório que não bate custa mais caro que nenhum relatório, porque ele gasta a única moeda que RevOps tem no começo, que é crédito.

Recuperação

Responda antes de abrir. Tentar e errar consolida mais que reler.

  1. 1O que é o grão de uma consulta, e qual é o teste de duas linhas que detecta fan-out?
    Grão é o que uma linha da saída representa, dito em uma frase: uma oportunidade, um item de contrato, um segmento, um mês. O teste é rodar COUNT(*) na tabela de origem com o mesmo filtro, rodar COUNT(*) depois do JOIN e comparar. Se subiu e o grão não deveria ter mudado, há fan-out. A correção é agregar antes de juntar, ou somar a coluna do grão em que você efetivamente está.
  2. 2Uma tabela de 4.200 oportunidades é juntada a uma de 11.900 itens de contrato e a soma do valor da oportunidade sai em R$ 84 milhões contra R$ 31 milhões no relatório do financeiro. Qual o fator de inflação, e o que ele lhe diz sobre a tabela de itens?
    R$ 84 milhões ÷ R$ 31 milhões = 2,7 vezes. O fator é a média de itens por oportunidade que participaram da junção, o que sugere entre dois e três módulos por contrato fechado. Repare que o fator é informação útil sobre o produto, e é por isso que a conta vale a pena: você descobriu o erro e, de quebra, descobriu que o contrato médio tem quase três módulos. Publique o segundo e conserte o primeiro.
  3. 3O Módulo 5 especificou uma camada semântica em que cada métrica tem uma definição única. Por que ela não elimina a necessidade de você saber ler SQL?Módulo 5 · 5.6
    Porque a camada semântica é onde a definição vira código, e alguém precisa revisar esse código. A camada garante que todo mundo que consultar chegue ao mesmo número; ela não garante que o número esteja certo. Quem escreve a ficha de métrica precisa conseguir abrir a definição, ler o filtro, ver qual data ancora a janela e conferir se o JOIN declara o grão. Sem isso, a camada semântica apenas propaga a mesma definição errada com mais eficiência e mais autoridade.
  4. 4Escreva de memória as seis construções, na ordem de dor em que apareceram, e a linha de comentário obrigatória antes de todo JOIN.
    Um, SELECT com WHERE para separar negócio vivo de negócio morto. Dois, GROUP BY com agregação para o win rate por segmento. Três, JOIN para juntar duas tabelas, e é onde o total para de bater. Quatro, CTE para escrever a conta em etapas nomeadas. Cinco, LAG, SUM OVER e ROW_NUMBER para comparar com o anterior, acumular e numerar dentro do grupo. Seis, DATE_TRUNC com diferença de meses para a coorte. A linha obrigatória: o grão da saída desta consulta é uma linha por X. Se você não consegue completar a frase, não escreva o JOIN ainda.

A Órbita Software é uma empresa fictícia, construída para este curso. Por que um caso fictício.