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_id | conta_id | segmento | origem | data_criacao | data_fechamento_real | acv | resultado |
|---|---|---|---|---|---|---|---|
| OP-1001 | C-0412 | mid | inbound | 2025-01-09 | 2025-04-15 | R$ 60 mil | ganha |
| OP-1002 | C-0418 | mid | outbound | 2025-01-22 | 2025-05-30 | R$ 74 mil | perdida |
| OP-1003 | C-0431 | smb | inbound | 2025-02-03 | 2025-03-14 | R$ 19,2 mil | ganha |
| OP-1004 | C-0447 | mid | evento | 2025-02-17 | 2025-07-21 | R$ 96 mil | perdida |
| OP-1005 | C-0452 | smb | mídia paga | 2025-03-02 | 2025-04-02 | R$ 16,8 mil | perdida |
| OP-1006 | C-0460 | mid | inbound | 2025-03-19 | 2025-08-08 | R$ 51 mil | perdida |
| OP-1007 | C-0473 | mid | indicação | 2025-04-07 | 2025-06-25 | R$ 66 mil | ganha |
| OP-1008 | C-0486 | smb | mídia paga | 2025-04-28 | 2025-05-19 | R$ 21 mil | perdida |
| OP-1009 | C-0418 | mid | outbound | 2025-05-12 | 2025-10-09 | R$ 83 mil | perdida |
| OP-1010 | C-0412 | mid | inbound | 2025-06-04 | 2025-09-16 | R$ 47 mil | perdida |
| OP-1011 | C-0517 | smb | inbound | 2025-06-23 | (vazio) | R$ 18 mil | aberta |
| OP-1012 | C-0524 | mid | evento | 2025-07-01 | (vazio) | R$ 112 mil | aberta |
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_id | item | valor_item | Por que esta linha existe |
|---|---|---|---|
| OP-1001 | Módulo base | R$ 32 mil | Primeiro módulo do contrato. Sozinho, não é o valor do negócio. |
| OP-1001 | Roteirização | R$ 18 mil | Segundo módulo do mesmo negócio. Aqui nasce a duplicata. |
| OP-1001 | Auditoria de frete | R$ 10 mil | Terceiro módulo. O negócio OP-1001 agora tem três linhas. |
| OP-1007 | Módulo base | R$ 42 mil | Outro negócio, dois módulos. |
| OP-1007 | Roteirização | R$ 24 mil | A segunda linha do OP-1007. |
| OP-1003 | Módulo base | R$ 19,2 mil | Negó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.
Arraste o desenho para o lado para ver inteiro
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;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;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'; -- 6Arraste o desenho para o lado para ver inteiro
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.
Arraste o desenho para o lado para ver inteiro
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;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| mes | acv_novo (R$ mil) | variacao (R$ mil) | variacao_pct | acumulado (R$ mil) | Leitura |
|---|---|---|---|---|---|
| jan/25 | 380 | 380 | Base do ano. Não há mês anterior para comparar. | ||
| fev/25 | 440 | 60 | 15,8% | 820 | Retomada normal depois de janeiro. |
| mar/25 | 480 | 40 | 9,1% | 1.300 | Fechamento de trimestre. Parte disso é calendário, não demanda. |
| abr/25 | 520 | 40 | 8,3% | 1.820 | Crescimento limpo, sem efeito de fim de trimestre. |
| mai/25 | 560 | 40 | 7,7% | 2.380 | Crescimento limpo. |
| jun/25 | 600 | 40 | 7,1% | 2.980 | Fechamento de trimestre outra vez. |
| jul/25 | 540 | -60 | -10,0% | 3.520 | Única queda relevante do ano. Investigue antes de explicar. |
| ago/25 | 580 | 40 | 7,4% | 4.100 | Recuperação parcial. Não devolve o que julho tirou. |
| set/25 | 660 | 80 | 13,8% | 4.760 | Fechamento de trimestre e melhor mês antes de dezembro. |
| out/25 | 620 | -40 | -6,1% | 5.380 | Queda pequena, no padrão de início de trimestre. |
| nov/25 | 640 | 20 | 3,2% | 6.020 | Praticamente estável. |
| dez/25 | 880 | 240 | 37,5% | 6.900 | Fim 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).| safra | criadas | resolvidas | abertas | ganhas | conversão | O que a linha permite dizer |
|---|---|---|---|---|---|---|
| jan/25 | 2 | 2 | 0 | 1 | 50,0% | Denominador de 2. Serve para conferir a consulta, não para decidir. |
| fev/25 | 2 | 2 | 0 | 1 | 50,0% | Mesmo caso: duas linhas não sustentam porcentagem. |
| mar/25 | 2 | 2 | 0 | 0 | 0,0% | Zero de verdade: as duas foram resolvidas e as duas se perderam. |
| abr/25 | 2 | 2 | 0 | 1 | 50,0% | Safra madura pelos dois ciclos medianos de cada segmento. |
| mai/25 | 1 | 1 | 0 | 0 | 0,0% | Uma linha. Sem o (0 de 1) ao lado, publicar 0,0% é desonesto. |
| jun/25 | 2 | 1 | 1 | 0 | 0,0% | A aberta é o negócio parado da construção 1, fora do denominador. |
| jul/25 | 1 | 0 | 1 | 0 | Sem 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ção | O que faz | Equivalente na planilha | Onde a planilha quebra |
|---|---|---|---|
| SELECT com WHERE | Escolhe colunas e filtra linhas | Filtro da barra de ferramentas | Quando o critério muda por linha, vira coluna auxiliar e ninguém sabe quantas existem |
| GROUP BY com agregação | Agrupa e conta, soma ou tira média | Tabela dinâmica | A dinâmica não guarda a definição: quem abrir amanhã remonta com outros filtros |
| JOIN | Junta tabelas por uma chave | PROCV ou PROCX | PROCV devolve a primeira ocorrência e esconde o um para muitos; o JOIN mostra |
| CTE | Nomeia cada etapa do raciocínio | Abas intermediárias | Aba intermediária não é versionável e ninguém sabe qual está atualizada |
| LAG, SUM OVER, ROW_NUMBER | Compara com a linha anterior, acumula, numera dentro do grupo | Fórmula que aponta para a célula de cima | Quebra em silêncio quando alguém reordena ou filtra a tabela |
| DATE_TRUNC com diferença de meses | Monta safra e mês de vida, e com isso a tabela de coortes | Dinâmica com coluna de mês de vida calculada à mão | O 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.
- 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.
- 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(*)?
- 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ê.
- 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.
- 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 respostasEsconder respostas
- 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. - 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.
- 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.
- 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ê.
- 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.
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á.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.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.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.