SQL para análise de dados · Guias · 30 de outubro de 2026 · 13 min de leitura
ROW_NUMBER, RANK e DENSE_RANK no SQL: qual a diferença nos empates

ROW_NUMBER(), RANK() e DENSE_RANK() numeram as linhas na ordem que você define em OVER (ORDER BY ...). Elas dão o mesmo resultado até duas linhas empatarem. No empate, o ROW_NUMBER continua dando números únicos (1, 2, 3, 4), o RANK dá o mesmo número às linhas empatadas e depois pula (1, 2, 2, 4), e o DENSE_RANK dá o mesmo número sem pular (1, 2, 2, 3). Na prática: ROW_NUMBER para “exatamente N linhas” e para remover duplicados, RANK para rankings em que empatados dividem a posição, DENSE_RANK para “o N-ésimo maior valor”.
Todos os exemplos rodam no banco de dados da companhia aérea do curso de SQL da Nomto, com sintaxe do PostgreSQL sobre o DuckDB (o motor da sala de SQL da Nomto), e cada tabela de resultado é a saída real da consulta. Para o quadro geral de OVER, frames, total acumulado e LAG, veja o guia de funções de janela no SQL; esta página trata só das funções de ranking.
A diferença numa tabela só
Um ranking dos membros do programa de fidelidade pelo número de trechos voados. Vários membros têm a mesma contagem, e é exatamente aí que as três funções se separam:
WITH m AS (
SELECT tm.member_id, COUNT(*) AS flights
FROM ticket_members tm
JOIN ticket_flights tf ON tf.ticket_no = tm.ticket_no
GROUP BY tm.member_id
)
SELECT member_id, flights,
ROW_NUMBER() OVER (ORDER BY flights DESC, member_id) AS row_num,
RANK() OVER (ORDER BY flights DESC) AS rnk,
DENSE_RANK() OVER (ORDER BY flights DESC) AS dense_rnk
FROM m
ORDER BY row_num
LIMIT 11;
| member_id | flights | row_num | rnk | dense_rnk |
|---|---|---|---|---|
| 12589 | 33 | 1 | 1 | 1 |
| 2969 | 32 | 2 | 2 | 2 |
| 1659 | 31 | 3 | 3 | 3 |
| 8943 | 31 | 4 | 3 | 3 |
| 37704 | 31 | 5 | 3 | 3 |
| 39173 | 31 | 6 | 3 | 3 |
| 9128 | 30 | 7 | 7 | 4 |
| 15845 | 30 | 8 | 7 | 4 |
| 35285 | 30 | 9 | 7 | 4 |
| 38397 | 30 | 10 | 7 | 4 |
| 6702 | 29 | 11 | 11 | 5 |
Leia linha por linha:
- ROW_NUMBER conta de 1 a 11 ignorando empates. Os quatro membros com 31 voos recebem 3, 4, 5 e 6 só por causa do desempate
member_idnoORDER BY. - RANK dá o 3º lugar aos quatro e depois salta para 7: as posições 4, 5 e 6 foram “gastas” pelo empate. É a posição “quantas linhas estão na minha frente, mais um”.
- DENSE_RANK também dá 3 aos quatro e continua com 4. Ele numera os valores distintos: 33, 32, 31, 30 e 29 são da 1ª à 5ª contagem distinta de voos.
A mesma regra numa frase: o RANK conta linhas, o DENSE_RANK conta valores distintos, o ROW_NUMBER ignora empates.
Quantas linhas o “top 5” devolve com cada uma
Como cada função trata empates de um jeito, um filtro <= 5 devolve um número diferente de linhas:
WITH m AS (
SELECT tm.member_id, COUNT(*) AS flights
FROM ticket_members tm
JOIN ticket_flights tf ON tf.ticket_no = tm.ticket_no
GROUP BY tm.member_id
), r AS (
SELECT member_id, flights,
ROW_NUMBER() OVER (ORDER BY flights DESC, member_id) AS row_num,
RANK() OVER (ORDER BY flights DESC) AS rnk,
DENSE_RANK() OVER (ORDER BY flights DESC) AS dense_rnk
FROM m
)
SELECT COUNT(*) FILTER (WHERE row_num <= 5) AS by_row_number,
COUNT(*) FILTER (WHERE rnk <= 5) AS by_rank,
COUNT(*) FILTER (WHERE dense_rnk <= 5) AS by_dense_rank
FROM r;
| by_row_number | by_rank | by_dense_rank |
|---|---|---|
| 5 | 6 | 14 |
ROW_NUMBER <= 5sempre devolve exatamente 5 linhas e, sem avisar, deixa de fora um dos quatro membros empatados com 31.RANK <= 5devolve 6: todo mundo cuja posição é 5 ou melhor, então os quatro empatados em 3º entram.DENSE_RANK <= 5devolve 14: todo mundo com uma das cinco maiores contagens de voos (33, 32, 31, 30 ou 29).
Nenhuma está errada; elas respondem perguntas diferentes. Decida qual pergunta você está respondendo antes de escolher a função.
Top N por grupo com PARTITION BY
Acrescente PARTITION BY e a numeração recomeça em cada grupo. Os 3 maiores membros por aeroporto de origem, para dois aeroportos, com as três funções lado a lado:
WITH m AS (
SELECT lm.home_airport, tm.member_id, COUNT(*) AS flights
FROM ticket_members tm
JOIN ticket_flights tf ON tf.ticket_no = tm.ticket_no
JOIN loyalty_members lm ON lm.member_id = tm.member_id
WHERE lm.home_airport IN ('VKO', 'OVB')
GROUP BY lm.home_airport, tm.member_id
), r AS (
SELECT home_airport, member_id, flights,
ROW_NUMBER() OVER (PARTITION BY home_airport ORDER BY flights DESC, member_id) AS rn,
RANK() OVER (PARTITION BY home_airport ORDER BY flights DESC) AS rnk,
DENSE_RANK() OVER (PARTITION BY home_airport ORDER BY flights DESC) AS dense_rnk
FROM m
)
SELECT * FROM r
WHERE rnk <= 3
ORDER BY home_airport, rn;
| home_airport | member_id | flights | rn | rnk | dense_rnk |
|---|---|---|---|---|---|
| OVB | 39502 | 28 | 1 | 1 | 1 |
| OVB | 9835 | 25 | 2 | 2 | 2 |
| OVB | 4544 | 24 | 3 | 3 | 3 |
| OVB | 27427 | 24 | 4 | 3 | 3 |
| OVB | 38226 | 24 | 5 | 3 | 3 |
| OVB | 39050 | 24 | 6 | 3 | 3 |
| VKO | 39173 | 31 | 1 | 1 | 1 |
| VKO | 6702 | 29 | 2 | 2 | 2 |
| VKO | 17160 | 29 | 3 | 2 | 2 |
| VKO | 32434 | 29 | 4 | 2 | 2 |
O filtro rnk <= 3 devolve seis linhas para Novosibirsk (empate de quatro no 3º lugar) e quatro para Vnukovo (empate de três no 2º). Com rn <= 3 você teria exatamente três por aeroporto, e qual dos empatados entra dependeria só do desempate. Para uma lista de premiados, isso é injusto; para um widget “mostre três exemplos por aeroporto”, é exatamente o que você quer.
Duas regras valem para toda consulta de top N:
- Filtre por fora.
WHERE rnk <= 3não pode ficar no mesmoSELECTque calcularnk, porque oWHEREroda antes das funções de janela. Calcule o ranking numa CTE ou subconsulta e filtre na consulta externa. DuckDB, Snowflake, BigQuery e Databricks também aceitam a cláusulaQUALIFY rnk <= 3; PostgreSQL, MySQL e SQL Server não. - Particione pelo grupo, ordene pela métrica.
PARTITION BY home_airport ORDER BY flights DESCquer dizer “dentro de cada aeroporto, por voos”. Esquecer oPARTITION BYranqueia a tabela inteira.
Remover duplicados: uma linha por chave com ROW_NUMBER
Para remover duplicados, o empate é o ponto central: linhas duplicadas empatam em tudo, e você quer manter exatamente uma. Isso é trabalho para o ROW_NUMBER, nunca para o RANK, que numeraria as duas cópias com 1 e manteria as duas.
A tabela app_events recebe alguns eventos duas vezes: mesmo cliente, mesmo evento, mesmo event_time, received_at diferente. Numere as cópias, a que chegou primeiro antes:
SELECT event_id, event_name, event_time, received_at,
ROW_NUMBER() OVER (PARTITION BY client_id, event_name, event_time
ORDER BY received_at, event_id) AS rn
FROM app_events
WHERE client_id = '000034d98b23' AND event_time < '2026-07-05 02:00'
ORDER BY event_time, rn;
| event_id | event_name | event_time | received_at | rn |
|---|---|---|---|---|
| 131068 | app_open | 2026-07-05 01:02:07 | 2026-07-05 01:02:07 | 1 |
| 131081 | search | 2026-07-05 01:02:31 | 2026-07-05 01:02:31 | 1 |
| 131116 | search | 2026-07-05 01:02:31 | 2026-07-05 01:03:56 | 2 |
| 131085 | view_offer | 2026-07-05 01:02:41 | 2026-07-05 01:02:42 | 1 |
| 131096 | select_fare | 2026-07-05 01:03:32 | 2026-07-05 01:03:34 | 1 |
O evento search chegou duas vezes, com 85 segundos de diferença; a cópia recebe rn = 2. Mantenha rn = 1 na tabela inteira:
WITH ranked AS (
SELECT ROW_NUMBER() OVER (PARTITION BY client_id, event_name, event_time
ORDER BY received_at, event_id) AS rn
FROM app_events
)
SELECT COUNT(*) AS all_rows,
COUNT(*) FILTER (WHERE rn = 1) AS kept,
COUNT(*) FILTER (WHERE rn > 1) AS duplicates
FROM ranked;
| all_rows | kept | duplicates |
|---|---|---|
| 1494723 | 1484034 | 10689 |
As colunas do PARTITION BY definem o que é “a mesma linha”; o ORDER BY decide qual cópia sobrevive (aqui a primeira recebida; ORDER BY received_at DESC manteria a mais recente). Coloque uma coluna única como event_id no final para a escolha ser sempre a mesma. Outras formas de achar e apagar duplicados estão comparadas em como encontrar duplicados no SQL.
N-ésimo maior valor: DENSE_RANK
“Encontre o segundo maior preço” é uma pergunta clássica de entrevista, e é onde o RANK falha sem fazer barulho. Muitos bilhetes têm o mesmo preço:
WITH p AS (
SELECT amount,
RANK() OVER (ORDER BY amount DESC) AS rnk,
DENSE_RANK() OVER (ORDER BY amount DESC) AS dense_rnk
FROM ticket_flights
)
SELECT amount, MIN(rnk) AS rnk, MIN(dense_rnk) AS dense_rnk, COUNT(*) AS tickets
FROM p
WHERE dense_rnk <= 4
GROUP BY amount
ORDER BY amount DESC;
| amount | rnk | dense_rnk | tickets |
|---|---|---|---|
| 2033.00 | 1 | 1 | 427 |
| 2013.00 | 428 | 2 | 70 |
| 1998.00 | 498 | 3 | 691 |
| 1993.00 | 1189 | 4 | 856 |
São 427 bilhetes no preço máximo de US$ 2.033, então o RANK dá ao preço seguinte, US$ 2.013, a posição 428. Uma consulta com WHERE rnk = 2 devolve zero linhas; WHERE dense_rnk = 2 devolve US$ 2.013, a resposta certa. Já ROW_NUMBER() = 2 devolveria US$ 2.033 de novo, porque a segunda linha é só mais um bilhete no preço máximo.
WITH p AS (
SELECT amount, DENSE_RANK() OVER (ORDER BY amount DESC) AS dense_rnk
FROM ticket_flights
)
SELECT DISTINCT amount FROM p WHERE dense_rnk = 2;
| amount |
|---|
| 2013.00 |
Mais perguntas nesse estilo estão em perguntas de entrevista SQL.
Qual usar
| Tarefa | Função | Por quê |
|---|---|---|
| Exatamente N linhas, top N por grupo com tamanho fixo | ROW_NUMBER |
Números únicos; o desempate decide quem fica |
| Remover duplicados, última linha por chave | ROW_NUMBER |
As cópias empatam, e você precisa de exatamente uma com rn = 1 |
| Paginação por uma ordem estável | ROW_NUMBER |
Cada linha tem sua própria posição |
| Ranking, classificação de competição | RANK |
Empatados dividem a posição, a seguinte é pulada (1, 2, 2, 4) |
| Top N incluindo todos os empatados no corte | RANK |
rnk <= N mantém todas as linhas empatadas na posição N |
| N-ésimo maior ou menor valor | DENSE_RANK |
Conta valores distintos, sem buracos |
| Top N de valores distintos (os três maiores preços) | DENSE_RANK |
dense_rnk <= 3 devolve toda linha com um desses valores |
Mais uma regra: com ROW_NUMBER, sempre termine o ORDER BY com uma coluna única. Se a ordenação tiver empates, o banco pode numerar as linhas empatadas de outro jeito na próxima execução, e o seu “top 5” ou a sua tabela sem duplicados mudam sem nenhuma mudança nos dados. RANK e DENSE_RANK não precisam disso, porque linhas empatadas recebem o mesmo número de qualquer forma.
NTILE, PERCENT_RANK e CUME_DIST
Mais três funções de ranking que vale conhecer:
NTILE(n)divide as linhas ordenadas emngrupos de tamanho (quase) igual: quartis, decis.PERCENT_RANK()é(rank - 1) / (linhas - 1): 0 na primeira linha, 1 na última.CUME_DIST()é a fração de linhas com valor menor ou igual ao da linha atual.
Nos nove modelos de avião, ordenados por alcance:
SELECT model, range,
NTILE(4) OVER (ORDER BY range DESC) AS quartile,
ROUND(PERCENT_RANK() OVER (ORDER BY range), 3) AS pct_rank,
ROUND(CUME_DIST() OVER (ORDER BY range), 3) AS cume_dist
FROM aircrafts
ORDER BY range DESC;
| model | range | quartile | pct_rank | cume_dist |
|---|---|---|---|---|
| Boeing 777-300 | 11100 | 1 | 1 | 1 |
| Boeing 767-300 | 7900 | 1 | 0.875 | 0.889 |
| Airbus A319-100 | 6700 | 1 | 0.75 | 0.778 |
| Airbus A320-200 | 5700 | 2 | 0.625 | 0.667 |
| Airbus A321-200 | 5600 | 2 | 0.5 | 0.556 |
| Boeing 737-300 | 4200 | 3 | 0.375 | 0.444 |
| Sukhoi Superjet-100 | 3000 | 3 | 0.25 | 0.333 |
| Bombardier CRJ-200 | 2700 | 4 | 0.125 | 0.222 |
| Cessna 208 Caravan | 1200 | 4 | 0 | 0.111 |
Nove linhas não se dividem igualmente em quatro, então o primeiro grupo fica com a linha a mais (3, 2, 2, 2).
O NTILE tem o mesmo ponto cego do ROW_NUMBER: ignora empates. Divida todos os membros do programa de fidelidade em decis por voos:
WITH m AS (
SELECT tm.member_id, COUNT(*) AS flights
FROM ticket_members tm
JOIN ticket_flights tf ON tf.ticket_no = tm.ticket_no
GROUP BY tm.member_id
), t AS (
SELECT member_id, flights,
NTILE(10) OVER (ORDER BY flights DESC, member_id) AS decile
FROM m
)
SELECT decile, COUNT(*) AS members, MIN(flights) AS min_flights, MAX(flights) AS max_flights
FROM t
GROUP BY decile
ORDER BY decile;
| decile | members | min_flights | max_flights |
|---|---|---|---|
| 1 | 4158 | 8 | 33 |
| 2 | 4158 | 6 | 8 |
| 3 | 4158 | 6 | 6 |
| 4 | 4158 | 4 | 6 |
| 5 | 4157 | 4 | 4 |
| 6 | 4157 | 4 | 4 |
| 7 | 4157 | 3 | 4 |
| 8 | 4157 | 2 | 3 |
| 9 | 4157 | 2 | 2 |
| 10 | 4157 | 1 | 2 |
Os grupos têm o mesmo tamanho, mas membros com 8 voos caem tanto no decil 1 quanto no 2, e os com 4 voos se espalham por quatro decis. Se o “top 10%” não pode separar valores iguais, use PERCENT_RANK() <= 0.1 ou CUME_DIST(), que dão o mesmo valor a linhas empatadas.
Diferenças entre bancos
ROW_NUMBER, RANK, DENSE_RANK, NTILE, PERCENT_RANK e CUME_DIST são SQL padrão e funcionam igual no PostgreSQL, DuckDB, SQL Server (2012 em diante para as duas últimas), Oracle, BigQuery e Snowflake. O MySQL tem todas a partir da versão 8.0; o MySQL 5.7, ainda comum em sistemas legados, não tem nenhuma função de janela. O SQL Server exige ORDER BY dentro do OVER em todas elas, inclusive no ROW_NUMBER; PostgreSQL e DuckDB aceitam ROW_NUMBER() OVER (), mas aí a numeração é arbitrária.
Perguntas frequentes
Qual a diferença entre ROW_NUMBER, RANK e DENSE_RANK?
Elas só diferem nos empates. O ROW_NUMBER dá um número único a cada linha (1, 2, 3, 4). O RANK dá o mesmo número às linhas empatadas e pula os seguintes (1, 2, 2, 4). O DENSE_RANK dá o mesmo número às empatadas sem pular (1, 2, 2, 3).
Qual usar para top N por grupo?
ROW_NUMBER se cada grupo precisa devolver exatamente N linhas. RANK se todos os empatados na N-ésima posição devem entrar, o que pode devolver mais de N linhas por grupo. Nos dois casos, calcule o número numa CTE e filtre na consulta externa.
Por que o RANK pula números?
Porque o RANK é o número de linhas à frente da linha atual mais um. Se quatro linhas empatam em 3º, a próxima tem seis linhas à frente, então fica em 7º. O DENSE_RANK conta valores distintos, então a próxima fica em 4º.
Como pegar o segundo maior valor no SQL?
Use DENSE_RANK() OVER (ORDER BY valor DESC) e mantenha dense_rnk = 2. O RANK pode não devolver nada quando o maior valor é dividido por várias linhas, e o ROW_NUMBER pode devolver o maior valor de novo.
O ROW_NUMBER é determinístico?
Só se o ORDER BY dentro do OVER for único. Com empates na ordenação, o banco pode numerar as linhas empatadas de outro jeito entre execuções. Coloque uma coluna única, como um id, como última chave de ordenação.
Outros idiomas: English, Español, Tiếng Việt, 简体中文