Em análises do dia a dia, “pegar o top N” parece simples: ordenar e limitar. O problema começa quando você precisa lidar com empates, priorizar registros dentro de um grupo (por exemplo, “um cliente pode ter vários pedidos no mesmo dia”) ou escolher “o registro mais relevante” seguindo regras de negócio. É aí que ROW_NUMBER e RANK entram como ferramentas centrais para criar rankings consistentes, controlar empates e fazer deduplicação com critério.
O que são ROW_NUMBER e RANK (e por que não são a mesma coisa)
ROW_NUMBER() e RANK() são funções de janela usadas para numerar linhas dentro de uma partição (um grupo) conforme uma ordenação. A diferença principal está em como tratam empates.
ROW_NUMBER: numeração única, mesmo com empate
ROW_NUMBER() atribui um número sequencial único para cada linha dentro da partição, seguindo o ORDER BY da janela. Se duas linhas empatarem no critério de ordenação, ainda assim cada uma receberá um número diferente (por exemplo, 1 e 2). Isso é ideal quando você precisa escolher “apenas um” registro por grupo, mesmo que haja empate, e quer resolver o empate com um critério adicional.
RANK: empates compartilham posição e “pulam” números
RANK() atribui a mesma posição para linhas empatadas. Se duas linhas empatarem em 1º lugar, ambas recebem 1 e a próxima linha recebe 3 (há um “pulo”). Isso é ideal quando você quer respeitar empates e retornar todos os registros empatados no top N.
Quando usar cada um
- Use
ROW_NUMBERquando a regra pede um único registro por grupo (ex.: “pedido mais recente por cliente”, “último status por ticket”, “um endereço principal por usuário”). - Use
RANKquando a regra pede top N com empates incluídos (ex.: “top 3 produtos por receita, incluindo empates”, “top 10 vendedores por vendas, sem desempatar”).
Estrutura mental: PARTITION BY e ORDER BY na janela
As duas funções seguem o mesmo padrão:
Continue em nosso aplicativo
Você poderá ouvir o audiobook com a tela desligada, ganhar gratuitamente o certificado deste curso e ainda ter acesso a outros 5.000 cursos online gratuitos.
ou continue lendo abaixo...Baixar o aplicativo
ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)RANK() OVER (PARTITION BY ... ORDER BY ...)PARTITION BY define “o grupo” onde a contagem/regras reiniciam. ORDER BY define o critério de ranking dentro do grupo. Se você omitir PARTITION BY, o ranking é calculado no conjunto inteiro.
Exemplo base (tabelas fictícias)
Para os exemplos, imagine uma tabela de pedidos com colunas típicas:
orders(order_id, customer_id, order_date, total_amount, status)order_items(order_id, product_id, quantity, unit_price)products(product_id, product_name, category)
O foco aqui não é como montar as tabelas, e sim como aplicar ROW_NUMBER e RANK para top N, empates e priorização.
ROW_NUMBER na prática: “último pedido por cliente” (deduplicação com critério)
Um caso comum é: “quero uma linha por cliente, trazendo o pedido mais recente”. Se você tentar resolver isso apenas com MAX(order_date), você pode cair em ambiguidades quando há mais de um pedido na mesma data/hora ou quando precisa trazer colunas do pedido (valor, status, etc.).
Passo a passo
Passo 1: atribuir um número por cliente ordenando do mais recente para o mais antigo.
WITH ranked_orders AS ( SELECT o.*, ROW_NUMBER() OVER ( PARTITION BY o.customer_id ORDER BY o.order_date DESC, o.order_id DESC ) AS rn FROM orders o) SELECT * FROM ranked_orders WHERE rn = 1;O que está acontecendo:
PARTITION BY customer_idreinicia a contagem para cada cliente.ORDER BY order_date DESCcoloca o pedido mais recente primeiro.order_id DESCé um critério de desempate para garantir determinismo quando dois pedidos têm a mesma data. Sem um desempate, o banco pode escolher qualquer ordem entre empatados, e o “rn = 1” pode variar.
Resultado: uma linha por cliente, com o pedido “mais recente” conforme a regra definida.
Priorizar registros: “status mais relevante por ticket”
Outro cenário típico: uma entidade tem vários registros de status/etapas, e você precisa escolher o status “mais importante” para relatório. Exemplo: um pedido pode ter status cancelled, refunded, delivered, shipped, processing. Para um painel, você pode querer priorizar cancelled acima de delivered, e delivered acima de shipped, etc.
Passo a passo com ORDER BY por regra de negócio
Passo 1: criar uma ordenação por prioridade usando CASE.
WITH ranked_status AS ( SELECT o.order_id, o.customer_id, o.status, o.order_date, ROW_NUMBER() OVER ( PARTITION BY o.order_id ORDER BY CASE o.status WHEN 'cancelled' THEN 1 WHEN 'refunded' THEN 2 WHEN 'delivered' THEN 3 WHEN 'shipped' THEN 4 WHEN 'processing' THEN 5 ELSE 99 END, o.order_date DESC ) AS rn FROM orders o) SELECT order_id, customer_id, status, order_date FROM ranked_status WHERE rn = 1;Ideia-chave: você não precisa ordenar apenas por data ou valor. Pode ordenar por uma “tabela mental” de prioridades. O ROW_NUMBER escolhe um único registro por pedido, respeitando a prioridade.
Cuidados práticos:
- Garanta que a regra de prioridade seja completa (inclua
ELSE). - Inclua um critério secundário (como data) para desempatar dentro da mesma prioridade.
Top N por grupo com ROW_NUMBER: “top 3 pedidos por cliente”
Às vezes você quer mais de um registro por grupo, mas ainda com controle. Exemplo: “top 3 pedidos de maior valor por cliente”.
Passo a passo
Passo 1: numerar pedidos por cliente do maior para o menor valor.
WITH ranked_orders AS ( SELECT o.customer_id, o.order_id, o.total_amount, o.order_date, ROW_NUMBER() OVER ( PARTITION BY o.customer_id ORDER BY o.total_amount DESC, o.order_date DESC, o.order_id DESC ) AS rn FROM orders o) SELECT customer_id, order_id, total_amount, order_date FROM ranked_orders WHERE rn <= 3 ORDER BY customer_id, rn;Observação importante: com ROW_NUMBER, mesmo que haja empate no valor, você ainda terá exatamente 3 linhas por cliente (se existirem 3 ou mais pedidos). Isso é útil quando o relatório precisa de um número fixo de linhas por grupo.
Top N com empates: usando RANK para “top 3 produtos por receita, incluindo empates”
Agora o cenário oposto: você quer top N, mas não quer “quebrar” empates. Exemplo: “top 3 produtos por receita no mês”. Se o 3º e o 4º produto empatarem, faz sentido listar ambos.
Passo a passo
Passo 1: calcular receita por produto.
WITH product_revenue AS ( SELECT oi.product_id, SUM(oi.quantity * oi.unit_price) AS revenue FROM order_items oi GROUP BY oi.product_id), ranked_products AS ( SELECT pr.product_id, pr.revenue, RANK() OVER (ORDER BY pr.revenue DESC) AS rnk FROM product_revenue pr) SELECT product_id, revenue, rnk FROM ranked_products WHERE rnk <= 3 ORDER BY rnk, revenue DESC;O que muda aqui:
- Com
RANK, se dois produtos empatarem em 3º, ambos terãornk = 3e serão incluídos. - O resultado pode ter mais de 3 linhas. Isso é esperado e desejado quando a regra é “top 3 posições” e não “3 itens”.
Top N por categoria com empates: RANK + PARTITION BY
Um relatório comum é “top N por categoria”. Aqui, você quer reiniciar o ranking para cada categoria.
WITH product_revenue AS ( SELECT p.category, oi.product_id, SUM(oi.quantity * oi.unit_price) AS revenue FROM order_items oi JOIN products p ON p.product_id = oi.product_id GROUP BY p.category, oi.product_id), ranked AS ( SELECT category, product_id, revenue, RANK() OVER ( PARTITION BY category ORDER BY revenue DESC ) AS rnk FROM product_revenue) SELECT category, product_id, revenue, rnk FROM ranked WHERE rnk <= 5 ORDER BY category, rnk, revenue DESC;Interpretação: você obtém as 5 primeiras posições por categoria, incluindo empates. Se houver empate na 5ª posição, podem aparecer mais itens naquela categoria.
Empates e determinismo: por que seu top N pode “mudar sozinho”
Quando há empates no critério de ordenação, o banco pode retornar as linhas empatadas em qualquer ordem se você não especificar um desempate. Isso afeta diretamente ROW_NUMBER, porque ele precisa escolher uma ordem para numerar.
Regra prática
- Se você usa
ROW_NUMBERpara selecionarrn = 1(ourn <= N), sempre inclua critérios adicionais noORDER BYda janela para desempatar (por exemplo, um identificador único comoorder_id). - Se você usa
RANK, o empate é preservado, mas ainda pode ser útil adicionar critérios secundários para ordenar a exibição final (não para o rank em si, se você quer manter o conceito de empate).
ROW_NUMBER para “último registro por chave” em tabelas de eventos (deduplicação de logs)
Em dados operacionais, é comum ter uma tabela de eventos com múltiplas linhas por entidade: atualizações de cadastro, mudanças de plano, alterações de endereço, etc. Um padrão robusto é: “pegar o registro mais recente por entidade” usando ROW_NUMBER.
Passo a passo genérico
Passo 1: definir a chave de partição (entidade) e o critério de recência.
WITH latest AS ( SELECT e.*, ROW_NUMBER() OVER ( PARTITION BY e.entity_id ORDER BY e.updated_at DESC, e.event_id DESC ) AS rn FROM entity_events e) SELECT * FROM latest WHERE rn = 1;Por que isso funciona bem:
- Evita subqueries complexas para “trazer colunas do registro do MAX(updated_at)”.
- Permite desempate com um identificador do evento.
- Escala para regras mais ricas (por exemplo, priorizar tipos de evento antes de olhar a data).
RANK para “faixas de posição” e relatórios com empates explícitos
Em alguns relatórios, a posição em si é uma informação (ex.: “quem está em 1º, 2º, 3º”). Com empates, você quer que a posição reflita isso. RANK é apropriado quando a semântica do relatório é “posição no ranking” e não “ordem arbitrária”.
Exemplo: ranking de vendedores por faturamento
WITH seller_revenue AS ( SELECT o.seller_id, SUM(o.total_amount) AS revenue FROM orders o GROUP BY o.seller_id), ranked AS ( SELECT seller_id, revenue, RANK() OVER (ORDER BY revenue DESC) AS rnk FROM seller_revenue) SELECT seller_id, revenue, rnk FROM ranked ORDER BY rnk, seller_id;Leitura do resultado: se dois vendedores empatarem em faturamento, ambos aparecem com o mesmo rnk. Isso evita “forçar” um desempate que não existe no dado.
Priorizar e depois ranquear: combinando regras de negócio com top N
Às vezes o top N não é apenas “maior valor”. Você pode precisar ranquear por uma regra composta: primeiro por prioridade (ex.: pedidos com status “delivered” contam, “cancelled” não), depois por valor, depois por data.
Passo a passo: top 5 pedidos “válidos” por cliente
Suponha que você queira listar os 5 pedidos mais relevantes por cliente, onde pedidos cancelados vão para o fim (ou são excluídos), e dentro dos válidos você ordena por valor e recência.
WITH ranked AS ( SELECT o.customer_id, o.order_id, o.status, o.total_amount, o.order_date, ROW_NUMBER() OVER ( PARTITION BY o.customer_id ORDER BY CASE WHEN o.status = 'cancelled' THEN 2 ELSE 1 END, o.total_amount DESC, o.order_date DESC, o.order_id DESC ) AS rn FROM orders o) SELECT customer_id, order_id, status, total_amount, order_date FROM ranked WHERE rn <= 5 ORDER BY customer_id, rn;Variação comum: se cancelados não devem aparecer, filtre antes de ranquear (por exemplo, em uma CTE anterior) e use o ranking apenas nos “válidos”. A escolha depende do objetivo do relatório: excluir ou apenas rebaixar.
Checklist prático para escolher entre ROW_NUMBER e RANK em top N
- Preciso de exatamente N linhas por grupo? Use
ROW_NUMBERe filtrern <= N. - Preciso de “N posições” e incluir empates? Use
RANKe filtrernk <= N. - Vou selecionar apenas 1 linha por grupo? Use
ROW_NUMBERcom desempate determinístico noORDER BY. - O ranking é uma informação de negócio (posição) e empates devem ser visíveis? Use
RANK. - Há chance de empate no critério principal? Para
ROW_NUMBER, sempre adicione um desempate (id único, timestamp mais preciso, etc.).