Três funções do PostgreSQL parecem intercambiáveis — ROW_NUMBER(), RANK() e DENSE_RANK() — e produzem o mesmo resultado, até duas linhas empatarem. Escolher a errada nesse ponto é uma fonte clássica de rankings e relatórios sutilmente errados. Este post fixa a diferença de uma vez por todas.
Três funções de ranking, uma diferença crucial
As três são window functions: numeram linhas conforme o ORDER BY dentro do OVER(), opcionalmente reiniciando por grupo do PARTITION BY. Comportam-se de forma idêntica até duas linhas empatarem no valor de ordenação — e é nesse empate que os comportamentos se separam:
- ROW_NUMBER() — sempre único. Empates são resolvidos de forma arbitrária. Produz 1, 2, 3, 4.
- RANK() — linhas empatadas compartilham a posição e a próxima pula as posições consumidas. Produz 1, 2, 2, 4.
- DENSE_RANK() — linhas empatadas compartilham a posição, sem lacuna depois. Produz 1, 2, 2, 3.
Sintaxe
RANK() OVER (
[PARTITION BY coluna_ou_expr]
ORDER BY coluna_ou_expr [ASC | DESC]
)
O ORDER BY define a regra do ranking; o PARTITION BY, quando presente, reinicia o ranking para cada grupo. O DENSE_RANK() e o ROW_NUMBER() têm exatamente o mesmo formato — só o comportamento no empate muda.
Veja lado a lado
SELECT
name,
score,
ROW_NUMBER() OVER (ORDER BY score DESC) AS row_number,
RANK() OVER (ORDER BY score DESC) AS rank,
DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank
FROM players;
| name | score | row_number | rank | dense_rank |
|---|---|---|---|---|
| Ana | 100 | 1 | 1 | 1 |
| Bruno | 90 | 2 | 2 | 2 |
| Carla | 90 | 3 | 2 | 2 |
| Diego | 80 | 4 | 4 | 3 |
Repare no Diego, a linha logo após o empate. Bruno e Carla fizeram 90 e dividem o rank 2. O RANK() então joga o Diego para 4 — as posições 2 e 3 foram "consumidas" pelo empate. O DENSE_RANK() continua contando densamente e dá 3 ao Diego. O ROW_NUMBER() ignorou o empate e numerou direto.
Macete: RANK deixa lacunas, DENSE_RANK é denso (sem lacunas), ROW_NUMBER nunca empata.
Qual deles você realmente quer?
- Ranking com medalhas estilo olímpico →
RANK(). Dois ouros significam nenhuma prata — exatamente o comportamento de pular. - "Quantas faixas de preço distintas existem abaixo deste produto?" →
DENSE_RANK(), porque importam os níveis distintos, não as posições. - Top-N estrito, exatamente N linhas, empates resolvidos de forma determinística →
ROW_NUMBER()com uma coluna de desempate noORDER BY(ex.:ORDER BY score DESC, id).
Top-3 por categoria
SELECT *
FROM (
SELECT
product_id, category, revenue,
DENSE_RANK() OVER (
PARTITION BY category
ORDER BY revenue DESC
) AS tier
FROM product_sales
) t
WHERE tier <= 3;
Usar DENSE_RANK() aqui faz com que um empate triplo no primeiro lugar ainda revele as próximas faixas de receita realmente diferentes, em vez de cortar em exatamente três linhas como o ROW_NUMBER() faria. Escolha de propósito: você quer "os 3 produtos do topo" (ROW_NUMBER()) ou "produtos nas 3 maiores faixas de receita" (DENSE_RANK())? Eles diferem exatamente quando há empates.
As outras funções de ranking
A mesma família inclui mais três que vale conhecer:
- NTILE(n) — divide as linhas ordenadas em n baldes de tamanho parecido.
NTILE(4)dá quartis,NTILE(100)percentis. - PERCENT_RANK() — rank relativo de 0 a 1: "esta linha é melhor que 80% das demais".
- CUME_DIST() — distribuição acumulada, a fração de linhas no nível atual ou abaixo.
Mantendo a performance em produção
Todas compartilham um custo: uma ordenação sobre a partição. Conforme os dados crescem, uma consulta de ranking sem índice passa de um sort em memória para um external merge em disco e a latência se multiplica — silenciosamente. Um índice composto que casa com (coluna_particao, coluna_ordem) deixa a janela ler já ordenado e pular o sort. O PG Monitoring sinaliza exatamente essas regressões — o mesmo SQL, de repente 5× mais lento — e aponta o índice faltante nas colunas de partição/ordenação.