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.
- 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)
)
| Parte | O que faz | Sem ela | Aula |
|---|---|---|---|
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 comum | Funções de Janela |
PARTITION BY | Divide 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 linhas | PARTITION BY |
ORDER BY | A 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 conta | ROW_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ção | O que faz | Com empate | Aula |
|---|---|---|---|
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ção | O que devolve | Quando não existe | Aula |
|---|---|---|---|
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 antes | NULL | |
lag(x, n, padrão) | o mesmo, com um valor padrão | o 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 moldura | NULL (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.
| Escrita | O 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.
| Escrita | O que quer dizer |
|---|---|
ROWS | conta em linhas: "2 linhas antes" são 2 linhas, mesmo que tenham valores iguais |
RANGE | conta 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ó) |
GROUPS | conta em grupos de empatados: "1 grupo antes" é o conjunto de linhas com o valor anterior |
UNBOUNDED PRECEDING | desde a primeira linha da partição |
n PRECEDING | n antes da atual |
CURRENT ROW | a linha atual |
n FOLLOWING | n depois da atual |
UNBOUNDED FOLLOWING | até a última linha da partição |
ROWS UNBOUNDED PRECEDING | forma curta de ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW |
EXCLUDE NO OTHERS | (o padrão) não tira ninguém da moldura |
EXCLUDE CURRENT ROW | tira a linha atual |
EXCLUDE GROUP | tira a linha atual e as empatadas com ela |
EXCLUDE TIES | tira 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ês | Causa | Como 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 OVER | escreva o OVER; para filtrar, use uma subconsulta (acima) |
misuse of aliased window function r | uso errado da função de janela apelidada r | o apelido de uma função de janela usado no WHERE (… AS r … WHERE r = 1) | subconsulta |
argument of ntile must be a positive integer | o argumento do ntile deve ser um inteiro positivo | ntile(0) | use 1 ou mais |
second argument to nth_value must be a positive integer | o segundo argumento do nth_value deve ser um inteiro positivo | nth_value(nome, 0): a contagem começa em 1 | use 1 ou mais |
frame starting offset must be a non-negative integer | o deslocamento do começo da moldura deve ser um inteiro não negativo | ROWS 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 expression | RANGE com deslocamento exige uma expressão no ORDER BY | RANGE 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
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.
| Recurso | SQLite | MySQL 8.4 | PostgreSQL | SQL Server |
|---|---|---|---|---|
ROWS, RANGE | sim | sim | sim | sim; mas RANGE não aceita números (2 PRECEDING), só UNBOUNDED e CURRENT ROW |
GROUPS | sim | não | sim | não |
EXCLUDE … | sim | não | sim | não |
FILTER (WHERE …) numa agregada de janela | sim | não (use CASE) | sim | não (use CASE) |
moldura padrão com ORDER BY | RANGE 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), comEXCLUDEopcional. - Não vão no
WHERE: filtre numa subconsulta.