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

Ilustração plana de passageiros num pódio de três degraus com crachás numerados, dois deles dividindo o mesmo degrau, e um avião no céu

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:

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

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:

  1. Filtre por fora. WHERE rnk <= 3 não pode ficar no mesmo SELECT que calcula rnk, porque o WHERE roda 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áusula QUALIFY rnk <= 3; PostgreSQL, MySQL e SQL Server não.
  2. Particione pelo grupo, ordene pela métrica. PARTITION BY home_airport ORDER BY flights DESC quer dizer “dentro de cada aeroporto, por voos”. Esquecer o PARTITION BY ranqueia 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:

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, 简体中文