SQL ROW_NUMBER, RANK e DENSE_RANK

Todo fim de ano a Loja Aurora premia os produtos campeões de venda e os melhores clientes. A Ana precisa de um ranking: 1º lugar, 2º lugar, 3º lugar… Ordenar com ORDER BY ela já sabe; o que falta é a coluna com a posição. E aí aparece a pergunta que todo campeonato tem de responder: e quando dois empatam? Nesta aula você conhece três funções de janela que numeram as linhas, cada uma com uma regra diferente para os empates: ROW_NUMBER, RANK e DENSE_RANK.

Você vai aprender
  • ROW_NUMBER(): numerar as linhas 1, 2, 3…
  • RANK() e DENSE_RANK(): posições com empate, como num pódio.
  • A diferença entre as três, lado a lado, com os empates reais da Aurora.
  • Como pegar o "top N" sem cortar quem empatou, e por que um LIMIT não basta.
  • NTILE(n): dividir em faixas, e a paginação com ROW_NUMBER.
  • Os erros mais comuns, com a mensagem real do SQLite.

Esta aula continua a de Funções de Janela (OVER). Todos os exemplos rodam de verdade no seu navegador, no banco da Loja Aurora.


A ideia: a fila da senha e o pódio

Pense em duas situações do dia a dia:

  • A senha do banco: cada pessoa pega um número, 1, 2, 3, 4… Dois clientes nunca têm a mesma senha, mesmo que tenham chegado juntos. Isso é o ROW_NUMBER.
  • O pódio de uma corrida: se dois atletas cruzam a linha no mesmo instante, os dois ficam em 2º lugar. Isso é o RANK (e o DENSE_RANK, com uma diferença que você vai ver já já).

As três funções são funções de janela: escrevem-se com OVER (ORDER BY …), e o ORDER BY de dentro diz quem vem primeiro. Ao contrário de SUM e AVG, elas só existem como janela: não há ROW_NUMBER() sem OVER.


1. ROW_NUMBER: numerar as linhas

Os 5 produtos mais caros da loja, numerados:

SELECT ROW_NUMBER() OVER (ORDER BY preco DESC) AS posicao,
       nome,
       preco
FROM produtos
ORDER BY posicao
LIMIT 5;
ROW_NUMBER()
"Número da linha". Não recebe nada entre os parênteses: o que ela numera vem do OVER.
OVER (ORDER BY preco DESC)
A janela é a tabela inteira, em ordem de preço do maior para o menor. O mais caro ganha 1, o seguinte 2, e assim por diante.
AS posicao
O apelido da coluna. Ele pode ser usado no ORDER BY final (mas não no WHERE; veja Quando dá errado).
ORDER BY posicao LIMIT 5
Mostra o resultado em ordem de posição e só os 5 primeiros.

O ranking dos produtos por valor vendido

Agora o ranking de verdade: quanto cada produto rendeu em vendas. A janela pode ordenar por uma agregação, no mesmo SELECT do GROUP BY. É que o SQLite primeiro agrupa e só depois calcula a janela (aula anterior, seção 6):

SELECT ROW_NUMBER() OVER (ORDER BY SUM(i.quantidade * i.preco_unitario) DESC) AS posicao,
       pr.nome,
       ROUND(SUM(i.quantidade * i.preco_unitario), 2) AS vendido
FROM itens_pedido AS i
JOIN produtos AS pr ON pr.id = i.produto_id
GROUP BY pr.id
ORDER BY posicao
LIMIT 5;
GROUP BY pr.id
Uma linha por produto vendido (42 produtos; os 3 de jardinagem nunca vendidos ficam de fora, porque o JOIN não acha itens deles).
OVER (ORDER BY SUM(…) DESC)
Ordena os grupos pelo valor vendido, do maior para o menor, e numera.

O Jogo de Panelas 5 peças é o campeão, com R$ 1.999,20. Aqui não há empate de valor, então qualquer uma das três funções daria 1, 2, 3, 4, 5. A diferença aparece quando há empate.

Missão: agora os clientes. Monte o ranking dos 5 clientes que mais gastaram em produtos, com as colunas posicao, nome e gasto. O JOIN e o GROUP BY já estão prontos; falta a coluna da posição e o limite de 5.

SELECT c.nome,
       ROUND(SUM(i.quantidade * i.preco_unitario), 2) AS gasto
FROM clientes AS c
JOIN pedidos AS p ON p.cliente_id = c.id
JOIN itens_pedido AS i ON i.pedido_id = p.id
GROUP BY c.id
ORDER BY gasto DESC;
SELECT ROW_NUMBER() OVER (ORDER BY SUM(i.quantidade * i.preco_unitario) DESC) AS posicao,
       c.nome,
       ROUND(SUM(i.quantidade * i.preco_unitario), 2) AS gasto
FROM clientes AS c
JOIN pedidos AS p ON p.cliente_id = c.id
JOIN itens_pedido AS i ON i.pedido_id = p.id
GROUP BY c.id
ORDER BY posicao
LIMIT 5;

2. RANK e DENSE_RANK: o pódio com empates

Para ver empates, o ranking agora é por unidades vendidas: aí muitos produtos empatam (vários venderam 10 unidades, por exemplo). As três funções lado a lado:

WITH vendidos AS (
  SELECT pr.nome, SUM(i.quantidade) AS unidades
  FROM itens_pedido AS i
  JOIN produtos AS pr ON pr.id = i.produto_id
  GROUP BY pr.id
)
SELECT nome,
       unidades,
       ROW_NUMBER() OVER (ORDER BY unidades DESC, nome) AS row_number,
       RANK()       OVER (ORDER BY unidades DESC) AS rank,
       DENSE_RANK() OVER (ORDER BY unidades DESC) AS dense_rank
FROM vendidos
ORDER BY unidades DESC, nome
LIMIT 16;

Siga a tabela de cima para baixo:

UnidadesQuemROW_NUMBERRANKDENSE_RANK
14Cabo HDMI (sozinho)111
123 produtos empatados2, 3, 42, 2, 22, 2, 2
112 produtos empatados5, 65, 53, 3
108 produtos empatados7 a 147, …4, …
9os seguintes15, 16…15, …5, …
ROW_NUMBER()
Nunca repete: cada linha ganha um número diferente, como a senha. Entre empatados, quem decide a ordem é o desempate do ORDER BY (aqui, o nome, em ordem alfabética).
RANK()
Empatados ganham a mesma posição, e as posições seguintes pulam: depois de três em 2º, o próximo é o 5º, porque já passaram 4 produtos na frente dele. É o jeito das competições esportivas.
DENSE_RANK()
"Rank denso". Empatados ganham a mesma posição, mas sem pular: depois do 2º vem o 3º. A posição conta quantos valores diferentes vieram antes, e não quantas linhas.

Uma conta para conferir: no RANK, a posição é "1 + quantos produtos venderam mais do que eu". Os produtos de 10 unidades têm 6 na frente (1 de 14, 3 de 12, 2 de 11), então ficam em 7º. No DENSE_RANK, a posição é "1 + quantos valores maiores existem": 14, 12 e 11 são três valores, então ficam em 4º.

Qual escolher

FunçãoEmpatesExemplo com 14, 12, 12, 11Use quando…
ROW_NUMBER()Números diferentes; o desempate decide1, 2, 3, 4Você precisa de um número único por linha: numerar, paginar, escolher uma linha
RANK()Mesma posição; depois pula1, 2, 2, 4Premiação, classificação de campeonato
DENSE_RANK()Mesma posição; não pula1, 2, 2, 3"Os 3 maiores valores", faixas de preço, "o 2º maior salário"

3. O top 3 que não corta empatados

A Ana quer premiar os 3 produtos que venderam mais unidades. A primeira ideia é ORDER BY … LIMIT 3:

WITH vendidos AS (
  SELECT pr.nome, SUM(i.quantidade) AS unidades
  FROM itens_pedido AS i
  JOIN produtos AS pr ON pr.id = i.produto_id
  GROUP BY pr.id
)
SELECT nome, unidades
FROM vendidos
ORDER BY unidades DESC
LIMIT 3;

Vieram 3 linhas, mas a Garrafa Térmica 1L, que também vendeu 12, ficou de fora. E por que ela, e não o Lápis? Por acaso: entre empatados, o SQLite entrega na ordem que quiser, e o LIMIT corta onde o número mandar. Injusto com a garrafa.

Com RANK, o corte é pela posição, não pela quantidade de linhas. Como uma janela não pode ir no WHERE, a posição é calculada numa CTE (aula CTE (WITH)) e filtrada fora:

WITH vendidos AS (
  SELECT pr.nome, SUM(i.quantidade) AS unidades
  FROM itens_pedido AS i
  JOIN produtos AS pr ON pr.id = i.produto_id
  GROUP BY pr.id
),
ranking AS (
  SELECT nome, unidades,
         RANK() OVER (ORDER BY unidades DESC) AS posicao
  FROM vendidos
)
SELECT posicao, nome, unidades
FROM ranking
WHERE posicao <= 3
ORDER BY posicao, nome;
ranking
A segunda CTE dá a posição de cada produto. Dentro dela, posicao é uma coluna comum para quem lê de fora.
WHERE posicao <= 3
"Do 1º ao 3º lugar". Como os três produtos de 12 unidades estão todos em 2º, os quatro entram. O próximo é o 5º, e fica de fora.

Quatro premiados, e ninguém foi cortado por sorte. Se a regra fosse "os 3 maiores números de unidades" (14, 12 e 11), a função seria DENSE_RANK, e entrariam 6 produtos.


4. NTILE: dividir em faixas

Uma quarta função da família: NTILE(n) divide as linhas, em ordem, em n grupos do mesmo tamanho (ou quase). Os produtos de papelaria e eletrônicos em 4 faixas de preço:

SELECT nome,
       preco,
       NTILE(4) OVER (ORDER BY preco DESC) AS faixa
FROM produtos
WHERE categoria_id IN (1, 2)
ORDER BY preco DESC;
NTILE(4)
"N-ésimo pedaço", em 4 pedaços. São 13 produtos: 13 ÷ 4 dá 3 e sobra 1, então a primeira faixa fica com 4 e as outras com 3.
faixa 1
O quarto mais caro; faixa 4, o quarto mais barato.

5. Formas diferentes de fazer a mesma coisa

A posição com subconsulta

Sem funções de janela, o RANK se calcula com uma subconsulta correlacionada: "1 + quantos venderam mais do que eu".

WITH vendidos AS (
  SELECT pr.nome, SUM(i.quantidade) AS unidades
  FROM itens_pedido AS i
  JOIN produtos AS pr ON pr.id = i.produto_id
  GROUP BY pr.id
)
SELECT v.nome,
       v.unidades,
       (SELECT COUNT(*) FROM vendidos AS v2
        WHERE v2.unidades > v.unidades) + 1 AS posicao
FROM vendidos AS v
ORDER BY posicao, v.nome
LIMIT 6;

Mesmo resultado do RANK (1, 2, 2, 2, 5, 5), mas a subconsulta roda uma vez para cada produto, e a ideia fica escondida na conta.

Paginação: ROW_NUMBER × LIMIT/OFFSET

Para mostrar o catálogo em páginas de 5 produtos, a 3ª página (do 11º ao 15º mais caro) sai de dois jeitos:

WITH numerados AS (
  SELECT nome, preco,
         ROW_NUMBER() OVER (ORDER BY preco DESC, nome) AS n
  FROM produtos
)
SELECT n, nome, preco
FROM numerados
WHERE n BETWEEN 11 AND 15;
SELECT nome, preco
FROM produtos
ORDER BY preco DESC, nome
LIMIT 5 OFFSET 10;
Você quer…Use
Uma página de resultadosLIMIT … OFFSET … (aula LIMIT e OFFSET): mais curto
A página e o número de cada linhaROW_NUMBER() numa CTE + WHERE n BETWEEN …
Os N primeiros, sem cortar empatadosRANK() numa CTE + WHERE posicao <= N
Os N maiores valores diferentesDENSE_RANK() numa CTE + WHERE posicao <= N
Faixas do mesmo tamanhoNTILE(n)
A posição num SQLite antigo, sem janelasSubconsulta: (SELECT COUNT(*) … WHERE v2.x > v.x) + 1
E o "melhor de cada grupo"?

"O produto mais vendido de cada categoria" e "o último pedido de cada cliente" são os usos mais famosos do ROW_NUMBER. Para isso a numeração precisa recomeçar em cada grupo, e é esse o assunto de PARTITION BY.


Quando dá errado

Filtrar pela posição no WHERE
SELECT nome,
       ROW_NUMBER() OVER (ORDER BY preco DESC) AS posicao
FROM produtos
WHERE posicao <= 3;

A mensagem: misuse of aliased window function posicao, "uso indevido da função de janela com apelido posicao". O WHERE roda antes da janela: na hora de filtrar, a posição ainda não foi calculada.

Como consertar: calcule a posição numa CTE e filtre fora dela, como na seção 3. (Para "os 3 primeiros" sem empate, ORDER BY posicao LIMIT 3 também funciona, porque o ORDER BY roda depois da janela.)

Esqueceu o OVER
SELECT nome, ROW_NUMBER() AS posicao
FROM produtos;

A mensagem: misuse of window function ROW_NUMBER(). Ao contrário de SUM e AVG, que funcionam sozinhas como agregação, ROW_NUMBER, RANK e DENSE_RANK só existem como janela.

Como consertar: ROW_NUMBER() OVER (ORDER BY …).

Coluna dentro dos parênteses da função
SELECT nome, RANK(preco) OVER (ORDER BY preco DESC) AS posicao
FROM produtos;

A mensagem: wrong number of arguments to function RANK(), "número errado de argumentos para a função RANK()". A coluna que decide o ranking vai no ORDER BY do OVER, não dentro de RANK().

Como consertar: RANK() OVER (ORDER BY preco DESC), com os parênteses da função vazios.

Sem erro, mas todo mundo em 1º: faltou o ORDER BY da janela
SELECT nome, preco, RANK() OVER () AS posicao
FROM produtos
LIMIT 5;

O que acontece: todos ficaram em 1º lugar. Sem ORDER BY dentro do OVER, ninguém vem antes de ninguém: todas as linhas são pares (empatadas), e o RANK dá 1 para todas. Com ROW_NUMBER() OVER () seria pior: números 1, 2, 3… numa ordem qualquer, que parece certa mas não quer dizer nada.

Como consertar: diga quem vem primeiro: RANK() OVER (ORDER BY preco DESC).

Sem erro, mas instável: ROW_NUMBER com empate e sem desempate

Com ROW_NUMBER() OVER (ORDER BY unidades DESC), os três produtos de 12 unidades recebem 2, 3 e 4, mas qual recebe o 2 o SQLite decide. Numa versão nova do banco, ou com um índice novo, a ordem pode mudar, e o relatório de hoje não bate com o de amanhã.

Como consertar: sempre que usar ROW_NUMBER, termine o ORDER BY da janela com uma coluna única, como o id ou o nome: OVER (ORDER BY unidades DESC, nome). Se o empate deve ser empate, use RANK ou DENSE_RANK.

RANK = 3 que não acha ninguém

Veja a missão final logo abaixo: com RANK, depois de três produtos em 2º, ninguém fica em 3º nem em 4º, e um WHERE posicao = 3 devolve zero linhas. Não é erro; é a regra do RANK (as posições pulam). Para "o 3º maior valor", use DENSE_RANK.

Nos outros bancos
  • ROW_NUMBER, RANK, DENSE_RANK e NTILE são do padrão SQL e se escrevem igual no PostgreSQL, no SQL Server e no MySQL (a partir da 8.0).
  • SQL Server: o ORDER BY dentro do OVER é obrigatório no ROW_NUMBER, e a documentação avisa que, sem colunas de ordem únicas, a numeração pode mudar de uma execução para outra, a mesma instabilidade de que falamos acima.

Referência rápida

EscritaO que faz
ROW_NUMBER() OVER (ORDER BY x DESC, id)1, 2, 3… sem repetir; termine com uma coluna única
RANK() OVER (ORDER BY x DESC)Empate = mesma posição; depois pula (1, 2, 2, 4)
DENSE_RANK() OVER (ORDER BY x DESC)Empate = mesma posição; não pula (1, 2, 2, 3)
NTILE(n) OVER (ORDER BY x)Divide em n faixas de tamanho parecido
… OVER (ORDER BY SUM(x) DESC) com GROUP BYRanking dos grupos, no mesmo SELECT
WITH r AS (SELECT …, RANK() OVER (…) AS pos FROM …) SELECT … FROM r WHERE pos <= 3Top 3 com empatados
MensagemQuer dizer
misuse of aliased window function posicaoO apelido da janela no WHERE
misuse of window function ROW_NUMBER()Faltou o OVER (…)
wrong number of arguments to function RANK()Coluna dentro de RANK(); ela vai no ORDER BY do OVER

Exercícios

1. Numerar

Complete a função que numera as linhas sem repetir número:

SELECT () OVER (ORDER BY nome) AS n, nome FROM clientes;

2. O pódio sem pulos

Complete a função que dá a mesma posição aos empatados e não pula a posição seguinte:

SELECT nome, salario, () OVER (ORDER BY salario DESC) AS faixa FROM funcionarios;

3. Os valores, já em ordem, são 50, 40, 40, 30. Que posições o RANK() dá?

4. Por que ORDER BY unidades DESC LIMIT 3 pode ser injusto num ranking?

5. A Ana escreveu WHERE posicao <= 3 no mesmo SELECT em que criou posicao com RANK() OVER (…). O que acontece?

Missão final: o 3º lugar

Missão: a Ana quer os produtos do 3º maior número de unidades vendidas. Rode o SQL como está: com RANK, ninguém fica em 3º (as posições pulam do 2 para o 5) e volta vazio. Troque a função para que o 3º lugar seja o 3º valor diferente, e apareçam os 2 produtos de 11 unidades.

WITH vendidos AS (
  SELECT pr.nome, SUM(i.quantidade) AS unidades
  FROM itens_pedido AS i
  JOIN produtos AS pr ON pr.id = i.produto_id
  GROUP BY pr.id
),
ranking AS (
  SELECT nome, unidades,
         RANK() OVER (ORDER BY unidades DESC) AS posicao
  FROM vendidos
)
SELECT posicao, nome, unidades
FROM ranking
WHERE posicao = 3;
WITH vendidos AS (
  SELECT pr.nome, SUM(i.quantidade) AS unidades
  FROM itens_pedido AS i
  JOIN produtos AS pr ON pr.id = i.produto_id
  GROUP BY pr.id
),
ranking AS (
  SELECT nome, unidades,
         DENSE_RANK() OVER (ORDER BY unidades DESC) AS posicao
  FROM vendidos
)
SELECT posicao, nome, unidades
FROM ranking
WHERE posicao = 3;

O Estojo Duplo e o Conjunto de Copos 6 unidades, com 11 unidades cada, dividem o 3º lugar no DENSE_RANK. No RANK, eles estavam em 5º.


Resumo

  • ROW_NUMBER(), RANK() e DENSE_RANK() são funções de janela: sempre com OVER (ORDER BY …), e o ORDER BY de dentro diz quem vem primeiro.
  • ROW_NUMBER nunca repete (1, 2, 3, 4); termine o ORDER BY com uma coluna única para o resultado não mudar.
  • RANK repete nos empates e pula depois (1, 2, 2, 4); DENSE_RANK repete e não pula (1, 2, 2, 3).
  • Para o top N com empatados, calcule a posição numa CTE e filtre com WHERE posicao <= N; um LIMIT corta empatados por acaso.
  • NTILE(n) divide em n faixas; ROW_NUMBER também numera páginas.
  • A janela pode ordenar por uma agregação (OVER (ORDER BY SUM(…))) no mesmo SELECT do GROUP BY.
  • Próxima aula: espiar a linha anterior e a seguinte com LAG e LEAD.