Módulo 7: Window functions
OVER, PARTITION BY, ROW_NUMBER/RANK/DENSE_RANK, running SUM, LAG/LEAD
Antes de começar
GROUP BY colapsa o grupo numa linha. Window function faz uma
conta olhando um conjunto de linhas (a "janela") mas mantém cada linha no
resultado. É como ter o total do grupo ao lado de cada item.
função() OVER (PARTITION BY x ORDER BY y): a janela.PARTITION BYdivide em grupos;ORDER BYordena dentro de cada grupo (importa pra ranking e acumulado). Os dois são opcionais.- Ranking:
ROW_NUMBER()(1,2,3… sem empate),RANK()(empate ocupa a posição e pula: 1,1,3),DENSE_RANK()(1,1,2). - Acumulado:
SUM(x) OVER (ORDER BY y): soma correndo linha a linha. - Vizinhos:
LAG(x)/LEAD(x): valor da linha anterior / seguinte (dentro da partição). - Top-N por grupo: numere com
ROW_NUMBER() OVER (PARTITION BY g ORDER BY v DESC)numa subconsulta e filtreWHERE rn <= 3por fora (não dá pra filtrar window function noWHEREdireto).
Dois exemplos
Teoria
COUNT(*) OVER (): total geral em toda linha.AVG(x) OVER (PARTITION BY g): média do grupo ao lado de cada linha.ROW_NUMBER() OVER (ORDER BY x DESC): posição no ranking.SUM(x) OVER (ORDER BY d ROWS UNBOUNDED PRECEDING): acumulado (oROWS…é o default quando há ORDER BY).LAG(x, 1) OVER (ORDER BY d): valor anterior;x - LAG(x) OVER (…)= variação.- Window function não pode ir no
WHERE/HAVING: embrulhe numa subconsulta/CTE e filtre lá fora.
Exemplos de aplicação
Um exemplo montado pra cada item da teoria acima. Não precisa rodar, é pra ver a cara do recurso e entender cada pedaço. Os valores são ilustrativos.
OVER () e OVER (PARTITION BY ...)
SELECT
id,
valor_total,
ROUND(AVG(valor_total) OVER (), 2) AS media_geral,
ROUND(AVG(valor_total) OVER (PARTITION BY status), 2) AS media_do_status
FROM pedidos;
Window function calcula um agregado sem colapsar as linhas: cada pedido continua aparecendo, com a média ao lado. OVER () = sobre a tabela toda; OVER (PARTITION BY status) = sobre os pedidos do mesmo status. Dá pra comparar cada linha com a média dela.
ROW_NUMBER() OVER (ORDER BY ...)
SELECT
id,
valor_total,
ROW_NUMBER() OVER (ORDER BY valor_total DESC) AS posicao
FROM pedidos;
Numera as linhas na ordem que você pedir: aqui, 1 pro pedido mais caro, 2 pro segundo, e assim por diante. Com PARTITION BY cliente_id junto, o ranking reinicia a cada cliente ("o maior pedido de cada um").
Acumulado: SUM(...) OVER (ORDER BY ...)
SELECT
data_pedido,
valor_total,
SUM(valor_total) OVER (ORDER BY data_pedido) AS acumulado
FROM pedidos
WHERE status <> 'cancelado';
Com ORDER BY dentro do OVER, a soma vira acumulada: cada linha traz o total dela mais o de todas as anteriores na ordem de data. É assim que se faz "faturamento acumulado no tempo".
LAG(...): comparar com a linha anterior
SELECT
mes,
total,
total - LAG(total) OVER (ORDER BY mes) AS variacao
FROM (
SELECT DATE_TRUNC('month', data_pedido)::date AS mes,
SUM(valor_total) AS total
FROM pedidos
GROUP BY 1
) m;
LAG(total) OVER (ORDER BY mes) traz o total do mês anterior pra mesma linha; a subtração dá a variação mês a mês. No primeiro mês não há anterior, então LAG vem NULL e a variação também.
Window function não vai no WHERE
SELECT *
FROM (
SELECT id, valor_total,
ROW_NUMBER() OVER (ORDER BY valor_total DESC) AS pos
FROM pedidos
) ranqueado
WHERE pos <= 5;
O WHERE/HAVING roda antes das window functions, então WHERE pos <= 5 na mesma query daria erro. A saída é embrulhar numa subconsulta (ou CTE) e filtrar do lado de fora: aqui, "os 5 maiores pedidos".
Pegadinhas deste módulo
Os erros que quase todo mundo comete aqui. Ler agora poupa meia hora de confusão depois, e alguns deles são a causa de relatório errado em projeto real.
UPDATEouDELETEsemWHEREatinge a tabela inteira. E não pede confirmação. Hábito que salva: escreva primeiro comoSELECTcom o mesmoWHERE, confira quais linhas voltam, e só então troque o começo porUPDATEouDELETE.- Sem
BEGIN, cada comando já está confirmado. Na maioria das ferramentas não existe "desfazer". Em dado que importa, abra a transação antes, confira o resultado, e só entãoCOMMITouROLLBACK. - Conte antes de escrever.
SELECT COUNT(*)com o mesmoWHEREdiz quantas linhas você está prestes a mudar. Se o número surpreender, pare. INSERTsem nomear as colunas quebra quando a tabela muda.INSERT INTO t VALUES (...)depende da ordem das colunas. Nomeie sempre:INSERT INTO t (a, b) VALUES (...).- Massa de teste criada por você também precisa ser limpa. Dado de teste esquecido em ambiente compartilhado vira o "bug" que outra pessoa vai investigar na semana que vem.
Prática
Escreva a query e clique Rodar (ou Ctrl+Enter).
carregando banco…
Query livre
Olhar de QA
Window functions respondem perguntas que agregação simples não alcança: "qual a posição deste pedido no histórico do cliente?", "este defeito demorou mais que os anteriores do mesmo módulo?", "quanto tempo passou entre uma execução e a seguinte do mesmo caso de teste?". É a ferramenta de quem analisa tendência, e não só o retrato de hoje, e tendência é o que se leva para uma reunião de qualidade.