
Imagine que você trabalha em um e-commerce.
Alguém faz uma pergunta:
“Qual foi a última compra de cada cliente?”
A princípio, parece simples.
Temos uma tabela pedido:
pedido
----------------------
ID_PEDIDO
ID_CLIENTE
DATA_PEDIDO
VALOR_TOTALE precisamos encontrar a compra mais recente.
Talvez sua primeira ideia seja utilizar:
MAX(DATA_PEDIDO)E ela faz sentido.
Mas existe um detalhe que deixa o problema mais interessante.
Não queremos apenas saber a data da última compra.
Queremos saber:
- qual foi o pedido;
- quando aconteceu;
- qual foi o valor.
Agora o problema muda um pouco.
E é aqui que ROW_NUMBER() começa a fazer sentido.
Primeiro: o que MAX() consegue responder?
Se quisermos apenas a última data de compra de cada cliente, podemos fazer:
SELECT
ID_CLIENTE,
MAX(DATA_PEDIDO) AS DATA_ULTIMA_COMPRA
FROM pedido
GROUP BY
ID_CLIENTE;Teríamos algo assim:
| ID_CLIENTE | DATA_ULTIMA_COMPRA |
|---|---|
| 1 | 2026-10-03 |
| 2 | 2026-09-28 |
| 3 | 2026-10-05 |
Perfeito.
Se a pergunta fosse:
Quando cada cliente comprou pela última vez?
poderíamos parar aqui.
Mas queremos outra coisa.
Queremos o pedido daquela data.
Por exemplo:
| ID_CLIENTE | ID_PEDIDO | DATA_PEDIDO | VALOR_TOTAL |
|---|---|---|---|
| 1 | 158 | 2026-10-03 | 380,00 |
| 2 | 149 | 2026-09-28 | 720,00 |
| 3 | 163 | 2026-10-05 | 195,00 |
Não queremos apenas a maior data.
Queremos encontrar a linha que representa a última compra de cada cliente.
Pense em ordenar os pedidos de cada cliente
Imagine os pedidos do cliente 1:
| ID_PEDIDO | DATA_PEDIDO |
|---|---|
| 101 | 2026-07-10 |
| 124 | 2026-08-18 |
| 142 | 2026-09-11 |
| 158 | 2026-10-03 |
Se ordenarmos da compra mais recente para a mais antiga, podemos numerar:
| ID_PEDIDO | DATA_PEDIDO | POSIÇÃO |
|---|---|---|
| 158 | 2026-10-03 | 1 |
| 142 | 2026-09-11 | 2 |
| 124 | 2026-08-18 | 3 |
| 101 | 2026-07-10 | 4 |
Agora ficou fácil.
Qual é a última compra?
Aquela que recebeu o número 1.
É exatamente essa lógica que podemos construir com ROW_NUMBER().
Criando a numeração
Podemos escrever:
SELECT
ID_PEDIDO,
ID_CLIENTE,
DATA_PEDIDO,
VALOR_TOTAL,
ROW_NUMBER() OVER (
PARTITION BY ID_CLIENTE
ORDER BY
DATA_PEDIDO DESC,
ID_PEDIDO DESC
) AS NUMERO_LINHA
FROM pedido;A parte nova é:
ROW_NUMBER() OVER (
PARTITION BY ID_CLIENTE
ORDER BY
DATA_PEDIDO DESC,
ID_PEDIDO DESC
)Vamos traduzir isso.
PARTITION BY ID_CLIENTE significa:
faça a numeração separadamente para cada cliente.
E:
ORDER BY
DATA_PEDIDO DESC,
ID_PEDIDO DESCsignifica:
dentro de cada cliente, coloque os pedidos mais recentes primeiro.
O resultado começa a ficar assim:
| ID_CLIENTE | ID_PEDIDO | DATA_PEDIDO | NUMERO_LINHA |
|---|---|---|---|
| 1 | 158 | 2026-10-03 | 1 |
| 1 | 142 | 2026-09-11 | 2 |
| 1 | 124 | 2026-08-18 | 3 |
| 2 | 149 | 2026-09-28 | 1 |
| 2 | 130 | 2026-08-22 | 2 |
Agora temos uma forma de identificar o pedido mais recente de cada cliente.
Só precisamos manter o número 1
Podemos organizar a consulta utilizando uma CTE:
WITH pedido_ordenado AS (
SELECT
ID_PEDIDO,
ID_CLIENTE,
DATA_PEDIDO,
VALOR_TOTAL,
ROW_NUMBER() OVER (
PARTITION BY ID_CLIENTE
ORDER BY
DATA_PEDIDO DESC,
ID_PEDIDO DESC
) AS NUMERO_LINHA
FROM pedido
)
SELECT
ID_PEDIDO,
ID_CLIENTE,
DATA_PEDIDO,
VALOR_TOTAL
FROM pedido_ordenado
WHERE NUMERO_LINHA = 1;Pronto.
Agora temos:
uma linha por cliente, representando sua última compra.
E conseguimos manter as informações daquele pedido.
Por que colocamos ID_PEDIDO na ordenação?
Observe:
ORDER BY
DATA_PEDIDO DESC,
ID_PEDIDO DESCPor que não utilizamos apenas DATA_PEDIDO?
Imagine que um cliente faça dois pedidos na mesma data:
Pedido 170 → 2026-10-05
Pedido 175 → 2026-10-05Se utilizarmos apenas a data, existe empate.
Precisamos definir qual pedido será considerado primeiro.
Por isso adicionamos:
ID_PEDIDO DESCcomo critério de desempate.
⚠️ Isso pressupõe que um ID_PEDIDO maior representa um pedido posterior quando a data é igual. Se o banco possuir DATETIME com o horário real da compra, essa informação pode ser um critério melhor.
O importante é:
se existe possibilidade de empate, precisamos pensar em como ordenar essas linhas.
MAX ou ROW_NUMBER?
Agora podemos fazer uma comparação simples.
Se você precisa apenas saber:
Qual foi a data da última compra de cada cliente?
MAX() resolve muito bem.
SELECT
ID_CLIENTE,
MAX(DATA_PEDIDO) AS DATA_ULTIMA_COMPRA
FROM pedido
GROUP BY
ID_CLIENTE;Mas se a pergunta for:
Qual foi o último pedido de cada cliente e quais eram os dados desse pedido?
ROW_NUMBER() começa a ficar muito interessante.
Não porque seja “mais avançado”.
Mas porque representa melhor o problema.
O mesmo raciocínio resolve outros problemas
Agora que entendemos a ideia, podemos trocar a pergunta.
Em vez de:
última compra de cada cliente;
poderíamos procurar:
venda mais recente de cada vendedor;
maior pedido de cada cliente;
produto mais caro de cada categoria;
última movimentação de cada conta.
A estrutura mental continua parecida:
separar em grupos
↓
ordenar dentro de cada grupo
↓
numerar
↓
escolher a posição desejadaÉ isso que vale aprender.
Não apenas a sintaxe.
Um desafio para você
Imagine agora que a empresa quer:
As três compras mais recentes de cada cliente.
Você não precisa mudar toda a lógica.
Já temos os pedidos numerados:
1
2
3
4
5
...Então pense:
Se
NUMERO_LINHA = 1retorna apenas a última compra, qual condição poderia retornar as três últimas?
Tente resolver antes de procurar a resposta.
Esse tipo de exercício ajuda muito mais do que simplesmente copiar outra consulta.
Conclusão
ROW_NUMBER() pode parecer estranho quando aparece pela primeira vez.
Mas o raciocínio por trás dele é bastante simples.
Precisávamos descobrir:
qual é a última compra de cada cliente?
Então pensamos:
- separar os pedidos por cliente;
- ordenar do mais recente para o mais antigo;
- numerar os pedidos;
- escolher o número 1.
Só depois transformamos esse raciocínio em:
ROW_NUMBER() OVER (
PARTITION BY ID_CLIENTE
ORDER BY DATA_PEDIDO DESC
)Esse é o ponto que vale guardar.
Não comece tentando decorar ROW_NUMBER().
Entenda primeiro o problema que ele ajuda a resolver.
Quando a necessidade aparece, a sintaxe começa a fazer sentido.
Próximo passo
Na Coleção A Arte da Query, a prática segue exatamente essa lógica: você encontra cenários com contexto e precisa decidir como transformar a pergunta em uma consulta.
Porque no trabalho real dificilmente alguém vai pedir:
“Faça um
ROW_NUMBER().”
A pergunta será muito mais parecida com:
“Qual foi a última compra de cada cliente?”
E o mais importante é conseguir olhar para essa pergunta e construir o caminho até a resposta.
Para este mini-case, mantive uma única função nova (ROW_NUMBER), duas etapas de consulta e um único desdobramento como desafio. Assim preservamos a profundidade técnica sem voltar ao tamanho dos artigos anteriores.
0 Comentários