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.
ROW_NUMBER(): numerar as linhas 1, 2, 3…RANK()eDENSE_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
LIMITnão basta. NTILE(n): dividir em faixas, e a paginação comROW_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 oDENSE_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 BYfinal (mas não noWHERE; 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
JOINnã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:
| Unidades | Quem | ROW_NUMBER | RANK | DENSE_RANK |
|---|---|---|---|---|
| 14 | Cabo HDMI (sozinho) | 1 | 1 | 1 |
| 12 | 3 produtos empatados | 2, 3, 4 | 2, 2, 2 | 2, 2, 2 |
| 11 | 2 produtos empatados | 5, 6 | 5, 5 | 3, 3 |
| 10 | 8 produtos empatados | 7 a 14 | 7, … | 4, … |
| 9 | os seguintes | 15, 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, onome, 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ção | Empates | Exemplo com 14, 12, 12, 11 | Use quando… |
|---|---|---|---|
ROW_NUMBER() | Números diferentes; o desempate decide | 1, 2, 3, 4 | Você precisa de um número único por linha: numerar, paginar, escolher uma linha |
RANK() | Mesma posição; depois pula | 1, 2, 2, 4 | Premiação, classificação de campeonato |
DENSE_RANK() | Mesma posição; não pula | 1, 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 resultados | LIMIT … OFFSET … (aula LIMIT e OFFSET): mais curto |
| A página e o número de cada linha | ROW_NUMBER() numa CTE + WHERE n BETWEEN … |
| Os N primeiros, sem cortar empatados | RANK() numa CTE + WHERE posicao <= N |
| Os N maiores valores diferentes | DENSE_RANK() numa CTE + WHERE posicao <= N |
| Faixas do mesmo tamanho | NTILE(n) |
| A posição num SQLite antigo, sem janelas | Subconsulta: (SELECT COUNT(*) … WHERE v2.x > v.x) + 1 |
"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
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.)
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 …).
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.
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).
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.
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.
ROW_NUMBER,RANK,DENSE_RANKeNTILEsã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 BYdentro doOVERé obrigatório noROW_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
| Escrita | O 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 BY | Ranking dos grupos, no mesmo SELECT |
WITH r AS (SELECT …, RANK() OVER (…) AS pos FROM …) SELECT … FROM r WHERE pos <= 3 | Top 3 com empatados |
| Mensagem | Quer dizer |
|---|---|
misuse of aliased window function posicao | O 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:
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:
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()eDENSE_RANK()são funções de janela: sempre comOVER (ORDER BY …), e oORDER BYde dentro diz quem vem primeiro.ROW_NUMBERnunca repete (1, 2, 3, 4); termine oORDER BYcom uma coluna única para o resultado não mudar.RANKrepete nos empates e pula depois (1, 2, 2, 4);DENSE_RANKrepete 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; umLIMITcorta empatados por acaso. NTILE(n)divide em n faixas;ROW_NUMBERtambém numera páginas.- A janela pode ordenar por uma agregação (
OVER (ORDER BY SUM(…))) no mesmoSELECTdoGROUP BY. - Próxima aula: espiar a linha anterior e a seguinte com LAG e LEAD.