SQL para análise de dados · Guias · 30 de outubro de 2026 · 12 min de leitura

Funções de janela SQL: ROW_NUMBER, RANK, LAG, PARTITION BY e total acumulado

Fileiras de pessoas com crachás de posição vistas através de uma moldura de janela, e um avião no céu

Uma função de janela calcula, para cada linha, um valor a partir de um conjunto de linhas relacionadas (a “janela”) sem juntar essas linhas numa só. O GROUP BY transforma 33.121 voos em seis linhas de status; COUNT(*) OVER (PARTITION BY status) mantém os 33.121 voos e escreve ao lado de cada um a contagem do seu status. Essa ideia simples dá rankings, top N por grupo, totais acumulados, médias móveis, fatias de um total e comparações com a linha anterior.

A sintaxe é sempre a mesma:

função(...) OVER (
  PARTITION BY ...   -- opcional: reinicia o cálculo para cada grupo
  ORDER BY ...       -- opcional: ordena as linhas dentro da janela
  ROWS/RANGE ...     -- opcional: quais linhas em volta da atual entram no cálculo
)

Todos os exemplos rodam no banco de dados de uma companhia aérea do curso de SQL da Nomto (104 aeroportos, 9 modelos de avião, 33.121 voos entre julho e setembro de 2026, 262.788 reservas; valores em dólares americanos, textos dos dados em inglês), na sintaxe do PostgreSQL. Cada saída abaixo é real, e você pode executar qualquer consulta na sala de SQL da Nomto para conferir.

OVER (): uma agregação ao lado de cada linha

A janela mais simples é o resultado inteiro. Compare o alcance de cada modelo com a média da frota:

SELECT model, range,
       ROUND(AVG(range) OVER ()) AS fleet_avg,
       range - ROUND(AVG(range) OVER ()) AS diff
FROM aircrafts
ORDER BY range DESC
LIMIT 5;
model range fleet_avg diff
Boeing 777-300 11100 5344 5756
Boeing 767-300 7900 5344 2556
Airbus A319-100 6700 5344 1356
Airbus A320-200 5700 5344 356
Airbus A321-200 5600 5344 256

Sem janela, você precisaria de uma subconsulta ou de um JOIN com uma tabela de uma linha só para colocar fleet_avg em cada linha.

Fatia do total

As funções de janela são calculadas depois do GROUP BY, então dá para envolver uma agregação numa janela. SUM(COUNT(*)) OVER () é a soma de todas as contagens dos grupos:

SELECT status, COUNT(*) AS flights,
       ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 1) AS pct
FROM flights
GROUP BY status
ORDER BY flights DESC;
status flights pct
Arrived 16707 50.4
Scheduled 15383 46.4
On Time 518 1.6
Cancelled 414 1.2
Departed 58 0.2
Delayed 41 0.1

O 100.0 faz diferença: com inteiros, o PostgreSQL faria divisão inteira e a maioria das fatias sairia como 0.

PARTITION BY: uma janela separada para cada grupo

PARTITION BY divide as linhas em grupos e calcula a janela dentro de cada um, como o GROUP BY, mas sem juntar as linhas. Fatia da receita por classe dentro de cada modelo de avião:

SELECT a.model, tf.fare_conditions,
       SUM(tf.amount) AS revenue,
       ROUND(100.0 * SUM(tf.amount) / SUM(SUM(tf.amount)) OVER (PARTITION BY a.model), 1) AS pct_of_model
FROM ticket_flights tf
JOIN flights f ON f.flight_id = tf.flight_id
JOIN aircrafts a ON a.aircraft_code = f.aircraft_code
WHERE a.aircraft_code IN ('773', 'SU9')
GROUP BY a.model, tf.fare_conditions
ORDER BY a.model, revenue DESC;
model fare_conditions revenue pct_of_model
Boeing 777-300 Economy 22398522.00 65.3
Boeing 777-300 Business 6252364.00 18.2
Boeing 777-300 Comfort 5661169.00 16.5
Sukhoi Superjet-100 Economy 35936340.00 70.3
Sukhoi Superjet-100 Business 15208507.00 29.7

As porcentagens somam 100 dentro de cada modelo, não na tabela inteira.

ROW_NUMBER, RANK e DENSE_RANK

As três numeram as linhas na ordem dada pelo ORDER BY dentro do OVER. A diferença aparece só nos empates:

Vamos ordenar os aeroportos pelo número de destinos distintos. A parte interessante fica perto do décimo lugar, onde cinco aeroportos empatam com 9 destinos:

WITH d AS (
  SELECT departure_airport, COUNT(DISTINCT arrival_airport) AS destinations
  FROM flights
  GROUP BY departure_airport
), r AS (
  SELECT departure_airport, destinations,
         ROW_NUMBER() OVER (ORDER BY destinations DESC, departure_airport) AS row_num,
         RANK()       OVER (ORDER BY destinations DESC) AS rnk,
         DENSE_RANK() OVER (ORDER BY destinations DESC) AS dense_rnk
  FROM d
)
SELECT * FROM r
WHERE row_num BETWEEN 8 AND 15
ORDER BY row_num;
departure_airport destinations row_num rnk dense_rnk
ROV 11 8 8 8
KJA 10 9 9 9
IKT 9 10 10 10
KZN 9 11 10 10
PEE 9 12 10 10
SGC 9 13 10 10
TJM 9 14 10 10
ARH 8 15 15 11

Depois do empate de cinco, o RANK salta para 15 (quatro posições são “gastas”), enquanto o DENSE_RANK continua com 11. O desempate por departure_airport no ROW_NUMBER deixa o resultado reproduzível; sem ele, IKT, KZN, PEE, SGC e TJM poderiam sair em qualquer ordem em outra execução.

Qual usar:

Top N por grupo

O uso clássico de ROW_NUMBER com PARTITION BY: os dois destinos mais frequentes a partir de cada aeroporto de Moscou.

WITH r AS (
  SELECT departure_airport, arrival_airport, COUNT(*) AS flights,
         ROW_NUMBER() OVER (PARTITION BY departure_airport
                            ORDER BY COUNT(*) DESC, arrival_airport) AS rn
  FROM flights
  WHERE departure_airport IN ('SVO', 'DME', 'VKO')
  GROUP BY departure_airport, arrival_airport
)
SELECT departure_airport, arrival_airport, flights, rn
FROM r
WHERE rn <= 2
ORDER BY departure_airport, rn;
departure_airport arrival_airport flights rn
DME LED 244 1
DME BZK 183 2
SVO LED 305 1
SVO BZK 183 2
VKO BZK 183 1
VKO LED 183 2

Duas coisas para notar:

  1. Não dá para filtrar uma função de janela no WHERE. O WHERE roda antes de as janelas serem calculadas, então WHERE ROW_NUMBER() OVER (...) <= 2 dá erro. Calcule o número numa CTE ou subconsulta e filtre por fora. (Alguns bancos oferecem a cláusula QUALIFY para isso; o PostgreSQL não.)
  2. Verifique os empates. A partir de Vnukovo, BZK e LED têm 183 voos cada; o ROW_NUMBER colocou BZK em primeiro só por causa do desempate alfabético. Se as duas linhas empatadas devem aparecer, use RANK() ... <= 2; isso pode devolver mais de duas linhas por grupo.

Deduplicação: a linha mais recente por chave

O mesmo padrão mantém uma linha por chave. A última partida concluída de cada número de voo saindo de Kazan:

WITH ranked AS (
  SELECT flight_no, arrival_airport, scheduled_departure,
         ROW_NUMBER() OVER (PARTITION BY flight_no ORDER BY scheduled_departure DESC) AS rn
  FROM flights
  WHERE departure_airport = 'KZN' AND status = 'Arrived'
)
SELECT flight_no, arrival_airport, scheduled_departure
FROM ranked
WHERE rn = 1
ORDER BY flight_no
LIMIT 5;
flight_no arrival_airport scheduled_departure
PG0039 KVX 2026-08-11 17:25:00
PG0203 DME 2026-08-10 17:45:00
PG0446 MRV 2026-08-11 11:25:00
PG0498 IKT 2026-08-11 10:15:00
PG0508 LED 2026-08-11 09:45:00

Total acumulado

Coloque ORDER BY dentro do OVER de uma agregação e ela vira cumulativa. Reservas diárias na primeira semana de agosto, com total acumulado:

WITH daily AS (
  SELECT CAST(book_date AS date) AS day, COUNT(*) AS bookings
  FROM bookings
  WHERE book_date >= '2026-08-01' AND book_date < '2026-08-08'
  GROUP BY CAST(book_date AS date)
)
SELECT day, bookings,
       SUM(bookings) OVER (ORDER BY day) AS running_total,
       LAG(bookings) OVER (ORDER BY day) AS prev_day,
       bookings - LAG(bookings) OVER (ORDER BY day) AS change
FROM daily
ORDER BY day;
day bookings running_total prev_day change
2026-08-01 5746 5746 NULL NULL
2026-08-02 5761 11507 5746 15
2026-08-03 5665 17172 5761 -96
2026-08-04 5813 22985 5665 148
2026-08-05 5937 28922 5813 124
2026-08-06 6081 35003 5937 144
2026-08-07 6184 41187 6081 103

Acrescente PARTITION BY para reiniciar o total acumulado por grupo, por exemplo por aeroporto ou por mês.

LAG e LEAD: a linha anterior e a seguinte

LAG(x) devolve o x da linha anterior na ordem da janela, LEAD(x) o da linha seguinte. A tabela acima usa LAG para a variação dia a dia. Na primeira linha não existe linha anterior, então o resultado é NULL; LAG(bookings, 1, 0) devolveria 0 no lugar, e LAG(bookings, 7) olharia uma semana para trás.

LEAD responde “o que vem depois”. Com que frequência voa o PG0579, e quando é a sua última partida?

SELECT flight_no,
       CAST(scheduled_departure AS date) AS day,
       LEAD(CAST(scheduled_departure AS date)) OVER (ORDER BY scheduled_departure)
         - CAST(scheduled_departure AS date) AS days_to_next
FROM flights
WHERE flight_no = 'PG0579'
ORDER BY scheduled_departure DESC
LIMIT 3;
flight_no day days_to_next
PG0579 2026-09-09 NULL
PG0579 2026-09-02 7
PG0579 2026-08-26 7

É um voo semanal; a última partida da programação não tem uma seguinte. Repare que o ORDER BY da janela (crescente) é independente do ORDER BY da consulta (decrescente): primeiro a janela é calculada, depois a saída é ordenada.

Quando você compara linhas de várias entidades de uma vez, sempre particione: LEAD(...) OVER (PARTITION BY flight_no ORDER BY scheduled_departure). Sem isso, a última partida de um voo seria comparada com a primeira partida do número de voo seguinte.

Frames da janela: ROWS ou RANGE

O frame define quais linhas em volta da atual a função enxerga. Duas coisas para guardar.

O frame padrão. Com ORDER BY e sem frame explícito, o frame é RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. O RANGE trata todas as linhas com o mesmo valor de ORDER BY como um único passo, então linhas empatadas recebem o mesmo total acumulado:

WITH d AS (
  SELECT departure_airport, COUNT(DISTINCT arrival_airport) AS destinations
  FROM flights
  GROUP BY departure_airport
), w AS (
  SELECT departure_airport, destinations,
         SUM(destinations) OVER (ORDER BY destinations DESC) AS range_total,
         SUM(destinations) OVER (ORDER BY destinations DESC, departure_airport
                                 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS rows_total
  FROM d
)
SELECT * FROM w
WHERE destinations BETWEEN 9 AND 10
ORDER BY destinations DESC, departure_airport;
departure_airport destinations range_total rows_total
KJA 10 216 216
IKT 9 261 225
KZN 9 261 234
PEE 9 261 243
SGC 9 261 252
TJM 9 261 261

Com RANGE, os cinco aeroportos empatados pulam direto para 261. Com ROWS, o total cresce uma linha por vez. Se o seu total acumulado “dá degraus” em timestamps iguais, o motivo é este. Para um total acumulado linha a linha, use ROWS e uma ordenação única.

Médias móveis. Um frame como ROWS BETWEEN 2 PRECEDING AND CURRENT ROW é uma janela móvel de 3 linhas:

WITH daily AS (
  SELECT CAST(book_date AS date) AS day, COUNT(*) AS bookings
  FROM bookings
  WHERE book_date >= '2026-08-01' AND book_date < '2026-08-08'
  GROUP BY CAST(book_date AS date)
)
SELECT day, bookings,
       ROUND(AVG(bookings) OVER (ORDER BY day ROWS BETWEEN 2 PRECEDING AND CURRENT ROW), 1) AS avg_3d
FROM daily
ORDER BY day;
day bookings avg_3d
2026-08-01 5746 5746
2026-08-02 5761 5753.5
2026-08-03 5665 5724
2026-08-04 5813 5746.3
2026-08-05 5937 5805
2026-08-06 6081 5943.7
2026-08-07 6184 6067.3

As duas primeiras linhas têm média de menos de três dias, porque nesta janela não há nada antes de 1º de agosto. ROWS conta linhas, não dias: se faltasse um dia em daily, a média “de 3 dias” cobriria em silêncio quatro dias do calendário. Preencha as lacunas antes com uma série de datas, ou use RANGE BETWEEN INTERVAL '2 days' PRECEDING AND CURRENT ROW sobre uma coluna de data.

Onde as funções de janela entram na ordem da consulta

Logicamente, as janelas são calculadas depois de FROM, WHERE, GROUP BY e HAVING, e antes de ORDER BY e LIMIT. Isso explica três regras:

Para entender como WHERE, GROUP BY e HAVING interagem, veja GROUP BY e HAVING. Ranking e “top N por grupo” também estão entre as perguntas de entrevista SQL mais comuns.

Perguntas frequentes

Qual a diferença entre função de janela e GROUP BY?

O GROUP BY devolve uma linha por grupo. Uma função de janela devolve todas as linhas de entrada e acrescenta um valor calculado sobre um grupo de linhas. Dá para usar os dois na mesma consulta: a janela é aplicada sobre o resultado agrupado.

Qual a diferença entre RANK e DENSE_RANK?

Os dois dão o mesmo número às linhas empatadas. Depois de um empate, o RANK pula números (1, 2, 2, 4) e o DENSE_RANK não (1, 2, 2, 3). No exemplo dos aeroportos, cinco empatam no 10º lugar; o seguinte fica em 15º pelo RANK e em 11º pelo DENSE_RANK.

Por que o meu total acumulado se repete em várias linhas?

Porque o frame padrão é RANGE, que trata as linhas com valores iguais no ORDER BY como pares e inclui todas de uma vez. Especifique ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW e deixe a ordenação única.

Posso usar uma função de janela no WHERE?

Não. Calcule-a numa CTE ou subconsulta e filtre na consulta externa.

O ROW_NUMBER precisa de ORDER BY?

O PostgreSQL aceita ROW_NUMBER() OVER (), mas aí a numeração é arbitrária. Para qualquer resultado que você queira reproduzir, dê a ele um ORDER BY com um desempate único.

Outros idiomas: Tiếng Việt, English, Español, 简体中文