SQL Funções de Janela

Uma função de janela faz uma conta olhando para outras linhas do resultado, sem juntar as linhas numa só como o GROUP BY faz: numerar os pedidos de cada cliente, dar a posição de cada produto no ranking de preços, pegar o frete do pedido anterior, somar o frete acumulado. Toda função de janela tem um OVER (…) depois dela. Esta página reúne todas as do SQLite 3.50; as explicações passo a passo estão em Funções de Janela (OVER), ROW_NUMBER, RANK e DENSE_RANK, LAG e LEAD e PARTITION BY.

Como ler esta página
  • Cada função de janela devolve um valor para cada linha. Por isso, os resultados estão nos quadros "Resultado", que rodam o SQL de verdade no seu navegador (SQLite 3.50.4, banco da Loja Aurora).
  • As tabelas resumem o que cada função faz e citam os valores que aparecem nesses quadros.

A anatomia do OVER

função(…) OVER (
    PARTITION BY coluna      -- divide as linhas em grupos (opcional)
    ORDER BY coluna          -- a ordem dentro de cada grupo (opcional)
    ROWS BETWEEN … AND …     -- a "moldura": quais linhas entram na conta (opcional)
)
ParteO que fazSem elaAula
OVER ()Diz que a função é de janela. Vazio: a janela é o resultado inteiro.sem OVER, row_number() dá erro e sum(x) vira agregada comumFunções de Janela
PARTITION BYDivide as linhas em partições ("pedaços"); a conta recomeça em cada uma. Parecido com o GROUP BY, mas sem juntar as linhas.uma partição só, com todas as linhasPARTITION BY
ORDER BYA ordem das linhas dentro da partição: decide quem é o 1º, quem é o "anterior".ordem não garantida; nas agregadas, todas as linhas entram na contaROW_NUMBER
moldura (ROWS, RANGE, GROUPS)Quais linhas, em volta da atual, entram na conta das agregadas e de first_value/last_value/nth_value.com ORDER BY: do começo até a linha atual (e as empatadas com ela)Moldura

Numerar e classificar

FunçãoO que fazCom empateAula
row_number()Numera as linhas: 1, 2, 3…números diferentes para os empatados (a ordem entre eles não é garantida)ROW_NUMBER
rank()Posição no ranking, como no esporte.empatados recebem a mesma posição, e o próximo pula (1, 1, 1, 4…)RANK
dense_rank()Posição no ranking, sem buracos.empatados recebem a mesma posição, e o próximo não pula (1, 1, 1, 2…)DENSE_RANK
percent_rank()Posição relativa de 0 a 1: (rank - 1) / (linhas - 1).igual para os empatados
cume_dist()Distribuição acumulada: a fração das linhas que vêm antes ou empatam com esta (de 0 a 1).igual para os empatados
ntile(n)Divide as linhas em n grupos do mesmo tamanho (o quanto der) e diz o número do grupo: 1, 2… n.—

Os três números lado a lado, nos fretes de seis pedidos. Os pedidos 4, 12 e 20 tiveram frete grátis (empate em 0.0):

SELECT id, frete,
       row_number() OVER w AS row_number,
       rank()       OVER w AS rank,
       dense_rank() OVER w AS dense_rank
FROM pedidos
WHERE id IN (1, 3, 4, 12, 20, 5)
WINDOW w AS (ORDER BY frete);

O row_number deu 1, 2, 3 para os três empatados; o rank deu 1, 1, 1 e pulou para 4; o dense_rank deu 1, 1, 1 e seguiu com 2. O WINDOW w AS (…) no fim dá um nome à janela, para não repetir o OVER (ORDER BY frete) três vezes.

E as outras três, nos 6 produtos de papelaria (categoria 1), do mais caro para o mais barato:

SELECT nome, preco,
       round(percent_rank() OVER w, 3) AS percent_rank,
       round(cume_dist() OVER w, 3)    AS cume_dist,
       ntile(3) OVER w                 AS terco
FROM produtos
WHERE categoria_id = 1
WINDOW w AS (ORDER BY preco DESC);

Pegar o valor de outra linha

FunçãoO que devolveQuando não existeAula
lag(x)o valor de x na linha anterior (lag = "atraso")NULL (na primeira linha)LAG e LEAD
lag(x, n)o valor n linhas antesNULL
lag(x, n, padrão)o mesmo, com um valor padrãoo padrão
lead(x), lead(x, n), lead(x, n, padrão)o valor na linha seguinte (lead = "dianteira")NULL ou o padrão (na última linha)LAG e LEAD
first_value(x)o valor na primeira linha da moldura—
last_value(x)o valor na última linha da moldura. Cuidado: com a moldura padrão, a última linha é a atual (veja o exemplo).—
nth_value(x, n)o valor na n-ésima linha da molduraNULL (se a moldura ainda não tem n linhas)
SELECT id, data, frete,
       lag(frete)        OVER (ORDER BY id) AS anterior,
       lead(frete)       OVER (ORDER BY id) AS proximo,
       lag(frete, 2, 0)  OVER (ORDER BY id) AS dois_antes
FROM pedidos
WHERE id <= 5;

A armadilha do last_value: com só ORDER BY, a moldura vai do começo até a linha atual, então o "último" é sempre a própria linha (coluna ultimo_padrao). Para o último da partição inteira, estique a moldura até o fim (UNBOUNDED FOLLOWING):

SELECT nome, preco,
       first_value(nome) OVER w AS mais_barato,
       last_value(nome)  OVER w AS ultimo_padrao,
       last_value(nome)  OVER (ORDER BY preco
                               ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS mais_caro,
       nth_value(nome, 2) OVER w AS segundo
FROM produtos
WHERE categoria_id = 1
WINDOW w AS (ORDER BY preco);

Agregadas como janela

Qualquer função agregada (sum, avg, count, min, max, total, group_concat…) vira função de janela quando ganha um OVER. Ela continua calculando sobre várias linhas, mas devolve o resultado em cada linha.

EscritaO que dá
sum(frete) OVER ()o total geral, repetido em todas as linhas
sum(frete) OVER (ORDER BY id)a soma acumulada (do primeiro pedido até este)
avg(frete) OVER (ORDER BY id ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)a média móvel das 3 últimas linhas
count(*) OVER (PARTITION BY cliente_id)quantos pedidos o cliente desta linha tem
salario * 100.0 / sum(salario) OVER ()a porcentagem de cada um no total
count(*) FILTER (WHERE frete = 0) OVER (ORDER BY id)quantos fretes grátis houve até esta linha
SELECT id, frete,
       sum(frete) OVER (ORDER BY id) AS acumulado,
       round(avg(frete) OVER (ORDER BY id ROWS BETWEEN 2 PRECEDING AND CURRENT ROW), 2) AS media_3,
       count(*) OVER () AS total
FROM pedidos
WHERE id <= 5;

A porcentagem de cada salário na folha da loja:

SELECT nome, salario,
       round(salario * 100.0 / sum(salario) OVER (), 1) AS pct
FROM funcionarios;

A moldura (frame)

A moldura diz quais linhas, em volta da atual, entram na conta. Ela vale para as agregadas e para first_value, last_value e nth_value; as funções de ranking e o lag/lead a ignoram.

EscritaO que quer dizer
ROWSconta em linhas: "2 linhas antes" são 2 linhas, mesmo que tenham valores iguais
RANGEconta em valores do ORDER BY: as linhas empatadas entram juntas; RANGE BETWEEN 5 PRECEDING … quer dizer "valores até 5 a menos" (exige um ORDER BY de uma coluna só)
GROUPSconta em grupos de empatados: "1 grupo antes" é o conjunto de linhas com o valor anterior
UNBOUNDED PRECEDINGdesde a primeira linha da partição
n PRECEDINGn antes da atual
CURRENT ROWa linha atual
n FOLLOWINGn depois da atual
UNBOUNDED FOLLOWINGaté a última linha da partição
ROWS UNBOUNDED PRECEDINGforma curta de ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
EXCLUDE NO OTHERS(o padrão) não tira ninguém da moldura
EXCLUDE CURRENT ROWtira a linha atual
EXCLUDE GROUPtira a linha atual e as empatadas com ela
EXCLUDE TIEStira só as empatadas, deixando a atual
(sem moldura, com ORDER BY)RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW: do começo até a atual e as empatadas com ela
(sem moldura e sem ORDER BY)a partição inteira

A diferença entre RANGE (o padrão) e ROWS só aparece nos empates. Os pedidos 7 e 15 têm o mesmo frete (7.25), e os pedidos 1 e 9 também (9.9). Com RANGE, os empatados entram juntos no acumulado (os dois de 7,25 já mostram 14.5); com ROWS, entram um de cada vez (7.25 e depois 14.5). A coluna grupos mostra, com GROUPS, quais pedidos entram na moldura "o grupo de empatados anterior e o atual":

SELECT id, frete,
       sum(frete) OVER (ORDER BY frete) AS range_padrao,
       sum(frete) OVER (ORDER BY frete ROWS UNBOUNDED PRECEDING) AS rows_ac,
       group_concat(id) OVER (ORDER BY frete GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW) AS grupos
FROM pedidos
WHERE id IN (4, 7, 15, 1, 9);

O padrão "o primeiro de cada grupo"

Funções de janela não podem ir no WHERE (elas são calculadas depois dele). Para filtrar pelo número, calcule numa subconsulta e filtre por fora. O produto mais caro de cada categoria:

SELECT nome, categoria_id, preco
FROM (
    SELECT nome, categoria_id, preco,
           row_number() OVER (PARTITION BY categoria_id ORDER BY preco DESC) AS n
    FROM produtos
)
WHERE n = 1;

Troque n = 1 por n <= 3 para os 3 mais caros de cada categoria. O mesmo dá para escrever com uma CTE (WITH).


Quando dá errado

Mensagem (como sai)Em portuguêsCausaComo resolver
misuse of window function rank()uso errado da função de janela rank()função de janela no WHERE, no GROUP BY ou no HAVING, ou sem OVERescreva o OVER; para filtrar, use uma subconsulta (acima)
misuse of aliased window function ruso errado da função de janela apelidada ro apelido de uma função de janela usado no WHERE (… AS r … WHERE r = 1)subconsulta
argument of ntile must be a positive integero argumento do ntile deve ser um inteiro positivontile(0)use 1 ou mais
second argument to nth_value must be a positive integero segundo argumento do nth_value deve ser um inteiro positivonth_value(nome, 0): a contagem começa em 1use 1 ou mais
frame starting offset must be a non-negative integero deslocamento do começo da moldura deve ser um inteiro não negativoROWS BETWEEN -1 PRECEDING …o sentido vem de PRECEDING/FOLLOWING; o número é sempre 0 ou mais
RANGE with offset PRECEDING/FOLLOWING requires one ORDER BY expressionRANGE com deslocamento exige uma expressão no ORDER BYRANGE BETWEEN 1 PRECEDING … sem ORDER BY (ou com mais de uma coluna)ponha um ORDER BY de uma coluna, ou use ROWS
SELECT nome
FROM produtos
WHERE rank() OVER (ORDER BY preco) = 1;

Nos outros bancos

O básico é igual; as molduras mudam

As funções desta página são do padrão SQL e têm os mesmos nomes no MySQL (8.0 em diante), no PostgreSQL e no SQL Server. As diferenças estão nas molduras e nos extras. Conferido nas documentações oficiais; trechos só para comparar.

RecursoSQLiteMySQL 8.4PostgreSQLSQL Server
ROWS, RANGEsimsimsimsim; mas RANGE não aceita números (2 PRECEDING), só UNBOUNDED e CURRENT ROW
GROUPSsimnãosimnão
EXCLUDE …simnãosimnão
FILTER (WHERE …) numa agregada de janelasimnão (use CASE)simnão (use CASE)
moldura padrão com ORDER BYRANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW nos quatro

Experimente

Missão: numere os pedidos de cada cliente em ordem de data: o primeiro pedido de cada cliente recebe 1, o segundo 2, e assim por diante. Mostre cliente_id, id, data e o número numa coluna n. Dica: row_number() com PARTITION BY e ORDER BY.

SELECT cliente_id, id, data
FROM pedidos
ORDER BY cliente_id, data;
SELECT cliente_id, id, data,
       row_number() OVER (PARTITION BY cliente_id ORDER BY data) AS n
FROM pedidos;

Resumo

  • Função de janela = função + OVER (PARTITION BY … ORDER BY … moldura). Ela calcula olhando outras linhas, mas não junta as linhas.
  • Numerar e classificar: row_number, rank (pula depois do empate), dense_rank (não pula), percent_rank, cume_dist, ntile(n).
  • Outra linha: lag, lead, first_value, last_value (cuidado com a moldura padrão), nth_value.
  • Qualquer agregada com OVER: total acumulado, média móvel, porcentagem do total.
  • Moldura: ROWS (linhas), RANGE (valores), GROUPS (grupos de empatados), com EXCLUDE opcional.
  • Não vão no WHERE: filtre numa subconsulta.