SQL · Guías · 30 de octubre de 2026 · 13 min de lectura

ROW_NUMBER, RANK y DENSE_RANK en SQL: diferencias con empates

Ilustración plana de pasajeros en un podio de tres escalones con dorsales numerados, dos de ellos compartiendo el mismo escalón, y un avión al fondo

ROW_NUMBER(), RANK() y DENSE_RANK() numeran las filas en el orden que indicas en OVER (ORDER BY ...). Dan el mismo resultado hasta que dos filas empatan. Con un empate, ROW_NUMBER sigue repartiendo números únicos (1, 2, 3, 4), RANK da el mismo número a las filas empatadas y luego salta (1, 2, 2, 4), y DENSE_RANK da el mismo número sin saltar (1, 2, 2, 3). En la práctica: ROW_NUMBER para «exactamente N filas» y para quitar duplicados, RANK para clasificaciones en las que los empatados comparten puesto y DENSE_RANK para «el N-ésimo valor más alto».

Los ejemplos usan la base de datos de una aerolínea del curso de SQL de Nomto, con sintaxis de PostgreSQL sobre DuckDB (el motor de la sala de SQL de Nomto), y cada tabla de resultados es la salida real. Para el panorama completo de OVER, marcos, totales acumulados y LAG, mira la guía de funciones de ventana; esta página trata solo de las funciones de ranking.

La diferencia en una sola tabla

Un ranking de socios del programa de fidelización por número de tramos volados. Varios socios tienen el mismo número, y ahí es justo donde las tres funciones se separan:

WITH m AS (
  SELECT tm.member_id, COUNT(*) AS flights
  FROM ticket_members tm
  JOIN ticket_flights tf ON tf.ticket_no = tm.ticket_no
  GROUP BY tm.member_id
)
SELECT member_id, flights,
       ROW_NUMBER() OVER (ORDER BY flights DESC, member_id) AS row_num,
       RANK()       OVER (ORDER BY flights DESC) AS rnk,
       DENSE_RANK() OVER (ORDER BY flights DESC) AS dense_rnk
FROM m
ORDER BY row_num
LIMIT 11;
member_id flights row_num rnk dense_rnk
12589 33 1 1 1
2969 32 2 2 2
1659 31 3 3 3
8943 31 4 3 3
37704 31 5 3 3
39173 31 6 3 3
9128 30 7 7 4
15845 30 8 7 4
35285 30 9 7 4
38397 30 10 7 4
6702 29 11 11 5

Léela fila a fila:

La misma regla en una frase: RANK cuenta filas, DENSE_RANK cuenta valores distintos, ROW_NUMBER ignora los empates.

Cuántas filas devuelve un «top 5» con cada una

Como tratan los empates de forma distinta, un filtro como <= 5 devuelve un número de filas diferente con cada función:

WITH m AS (
  SELECT tm.member_id, COUNT(*) AS flights
  FROM ticket_members tm
  JOIN ticket_flights tf ON tf.ticket_no = tm.ticket_no
  GROUP BY tm.member_id
), r AS (
  SELECT member_id, flights,
         ROW_NUMBER() OVER (ORDER BY flights DESC, member_id) AS row_num,
         RANK()       OVER (ORDER BY flights DESC) AS rnk,
         DENSE_RANK() OVER (ORDER BY flights DESC) AS dense_rnk
  FROM m
)
SELECT COUNT(*) FILTER (WHERE row_num <= 5)   AS by_row_number,
       COUNT(*) FILTER (WHERE rnk <= 5)       AS by_rank,
       COUNT(*) FILTER (WHERE dense_rnk <= 5) AS by_dense_rank
FROM r;
by_row_number by_rank by_dense_rank
5 6 14

Ninguna está mal; responden a preguntas distintas. Antes de elegir función, aclara cuál es la pregunta.

Top N por grupo con PARTITION BY

Añade PARTITION BY y la numeración vuelve a empezar en cada grupo. Los 3 mejores socios por aeropuerto de origen, para dos aeropuertos, con las tres funciones lado a lado:

WITH m AS (
  SELECT lm.home_airport, tm.member_id, COUNT(*) AS flights
  FROM ticket_members tm
  JOIN ticket_flights tf ON tf.ticket_no = tm.ticket_no
  JOIN loyalty_members lm ON lm.member_id = tm.member_id
  WHERE lm.home_airport IN ('VKO', 'OVB')
  GROUP BY lm.home_airport, tm.member_id
), r AS (
  SELECT home_airport, member_id, flights,
         ROW_NUMBER() OVER (PARTITION BY home_airport ORDER BY flights DESC, member_id) AS rn,
         RANK()       OVER (PARTITION BY home_airport ORDER BY flights DESC) AS rnk,
         DENSE_RANK() OVER (PARTITION BY home_airport ORDER BY flights DESC) AS dense_rnk
  FROM m
)
SELECT * FROM r
WHERE rnk <= 3
ORDER BY home_airport, rn;
home_airport member_id flights rn rnk dense_rnk
OVB 39502 28 1 1 1
OVB 9835 25 2 2 2
OVB 4544 24 3 3 3
OVB 27427 24 4 3 3
OVB 38226 24 5 3 3
OVB 39050 24 6 3 3
VKO 39173 31 1 1 1
VKO 6702 29 2 2 2
VKO 17160 29 3 2 2
VKO 32434 29 4 2 2

Filtrar por rnk <= 3 devuelve seis filas para Novosibirsk (cuatro empatados en el 3.er puesto) y cuatro para Vnúkovo (tres empatados en el 2.º). Con rn <= 3 obtendrías exactamente tres por aeropuerto, y cuál de los empatados entra dependería solo del desempate. Para una lista de premiados, eso es injusto; para un widget que «muestra tres ejemplos por aeropuerto», es justo lo que quieres.

Dos reglas valen para cualquier consulta de top N:

  1. El filtro va fuera. WHERE rnk <= 3 no puede ir en el mismo SELECT que calcula rnk, porque WHERE se ejecuta antes que las funciones de ventana. Calcula el puesto en una CTE o subconsulta y filtra en la consulta exterior. DuckDB, Snowflake, BigQuery y Databricks aceptan además una cláusula QUALIFY rnk <= 3; PostgreSQL, MySQL y SQL Server no.
  2. Particiona por el grupo, ordena por la métrica. PARTITION BY home_airport ORDER BY flights DESC significa «dentro de cada aeropuerto, por vuelos». Si olvidas PARTITION BY, el ranking es sobre toda la tabla.

Quitar duplicados: una fila por clave con ROW_NUMBER

Para quitar duplicados, los empates son precisamente la cuestión: las filas duplicadas empatan en todo y quieres conservar exactamente una. Es trabajo para ROW_NUMBER, nunca para RANK, que numeraría las dos copias con 1 y conservaría ambas.

La tabla app_events recibe algunos eventos dos veces: mismo cliente, mismo evento, mismo event_time, distinto received_at. Numeramos las copias empezando por la que llegó primero:

SELECT event_id, event_name, event_time, received_at,
       ROW_NUMBER() OVER (PARTITION BY client_id, event_name, event_time
                          ORDER BY received_at, event_id) AS rn
FROM app_events
WHERE client_id = '000034d98b23' AND event_time < '2026-07-05 02:00'
ORDER BY event_time, rn;
event_id event_name event_time received_at rn
131068 app_open 2026-07-05 01:02:07 2026-07-05 01:02:07 1
131081 search 2026-07-05 01:02:31 2026-07-05 01:02:31 1
131116 search 2026-07-05 01:02:31 2026-07-05 01:03:56 2
131085 view_offer 2026-07-05 01:02:41 2026-07-05 01:02:42 1
131096 select_fare 2026-07-05 01:03:32 2026-07-05 01:03:34 1

El evento search llegó dos veces, con 85 segundos de diferencia; la copia recibe rn = 2. Nos quedamos con rn = 1 en toda la tabla:

WITH ranked AS (
  SELECT ROW_NUMBER() OVER (PARTITION BY client_id, event_name, event_time
                            ORDER BY received_at, event_id) AS rn
  FROM app_events
)
SELECT COUNT(*) AS all_rows,
       COUNT(*) FILTER (WHERE rn = 1) AS kept,
       COUNT(*) FILTER (WHERE rn > 1) AS duplicates
FROM ranked;
all_rows kept duplicates
1494723 1484034 10689

Las columnas del PARTITION BY definen qué es «la misma fila»; el ORDER BY decide qué copia sobrevive (aquí, la primera recibida; con ORDER BY received_at DESC te quedarías con la última). Añade al final una columna única como event_id para que la elección sea repetible. Otras formas de encontrar y borrar duplicados se comparan en cómo encontrar duplicados en SQL.

El N-ésimo valor más alto: DENSE_RANK

«Encuentra la segunda tarifa más alta» es una pregunta clásica de entrevista, y es donde RANK falla sin hacer ruido. Muchos billetes comparten precio:

WITH p AS (
  SELECT amount,
         RANK()       OVER (ORDER BY amount DESC) AS rnk,
         DENSE_RANK() OVER (ORDER BY amount DESC) AS dense_rnk
  FROM ticket_flights
)
SELECT amount, MIN(rnk) AS rnk, MIN(dense_rnk) AS dense_rnk, COUNT(*) AS tickets
FROM p
WHERE dense_rnk <= 4
GROUP BY amount
ORDER BY amount DESC;
amount rnk dense_rnk tickets
2033.00 1 1 427
2013.00 428 2 70
1998.00 498 3 691
1993.00 1189 4 856

427 billetes cuestan la tarifa máxima de 2.033 $, así que RANK le da al siguiente precio, 2.013 $, el puesto 428. Una consulta con WHERE rnk = 2 devuelve cero filas; WHERE dense_rnk = 2 devuelve 2.013 $, la respuesta correcta. ROW_NUMBER() = 2 devolvería otra vez 2.033 $, porque la segunda fila es simplemente otro billete al precio máximo.

WITH p AS (
  SELECT amount, DENSE_RANK() OVER (ORDER BY amount DESC) AS dense_rnk
  FROM ticket_flights
)
SELECT DISTINCT amount FROM p WHERE dense_rnk = 2;
amount
2013.00

Más preguntas de este estilo en preguntas de entrevista de SQL.

Cuál usar

Tarea Función Por qué
Exactamente N filas, top N por grupo de tamaño fijo ROW_NUMBER Números únicos; los empates los corta el desempate
Quitar duplicados, última fila por clave ROW_NUMBER Las copias empatan y necesitas exactamente una con rn = 1
Paginación con un orden estable ROW_NUMBER Cada fila tiene su propia posición
Clasificación, puestos de competición RANK Los empatados comparten puesto y el siguiente se salta (1, 2, 2, 4)
Top N incluyendo a todos los empatados en el corte RANK rnk <= N conserva todas las filas empatadas en el puesto N
N-ésimo valor más alto o más bajo DENSE_RANK Cuenta valores distintos, sin huecos
Top N de valores distintos (los tres precios más altos) DENSE_RANK dense_rnk <= 3 devuelve todas las filas con uno de esos valores

Una regla más: con ROW_NUMBER, termina siempre el ORDER BY con una columna única. Si el orden tiene empates, la base de datos puede numerar las filas empatadas de otra forma en la siguiente ejecución, y tu «top 5» o tu tabla sin duplicados cambia sin que cambien los datos. RANK y DENSE_RANK no lo necesitan, porque las filas empatadas reciben el mismo número de todos modos.

NTILE, PERCENT_RANK y CUME_DIST

Otras tres funciones de ranking que conviene conocer:

Con los nueve modelos de avión, ordenados por autonomía:

SELECT model, range,
       NTILE(4) OVER (ORDER BY range DESC) AS quartile,
       ROUND(PERCENT_RANK() OVER (ORDER BY range), 3) AS pct_rank,
       ROUND(CUME_DIST() OVER (ORDER BY range), 3) AS cume_dist
FROM aircrafts
ORDER BY range DESC;
model range quartile pct_rank cume_dist
Boeing 777-300 11100 1 1 1
Boeing 767-300 7900 1 0.875 0.889
Airbus A319-100 6700 1 0.75 0.778
Airbus A320-200 5700 2 0.625 0.667
Airbus A321-200 5600 2 0.5 0.556
Boeing 737-300 4200 3 0.375 0.444
Sukhoi Superjet-100 3000 3 0.25 0.333
Bombardier CRJ-200 2700 4 0.125 0.222
Cessna 208 Caravan 1200 4 0 0.111

Nueve filas no se reparten en cuatro grupos iguales, así que el primero se lleva la fila de más (3, 2, 2, 2).

NTILE tiene el mismo punto ciego que ROW_NUMBER: ignora los empates. Repartimos todos los socios en deciles por vuelos:

WITH m AS (
  SELECT tm.member_id, COUNT(*) AS flights
  FROM ticket_members tm
  JOIN ticket_flights tf ON tf.ticket_no = tm.ticket_no
  GROUP BY tm.member_id
), t AS (
  SELECT member_id, flights,
         NTILE(10) OVER (ORDER BY flights DESC, member_id) AS decile
  FROM m
)
SELECT decile, COUNT(*) AS members, MIN(flights) AS min_flights, MAX(flights) AS max_flights
FROM t
GROUP BY decile
ORDER BY decile;
decile members min_flights max_flights
1 4158 8 33
2 4158 6 8
3 4158 6 6
4 4158 4 6
5 4157 4 4
6 4157 4 4
7 4157 3 4
8 4157 2 3
9 4157 2 2
10 4157 1 2

Los grupos tienen el mismo tamaño, pero los socios con 8 vuelos caen tanto en el decil 1 como en el 2, y los de 4 vuelos se reparten entre cuatro deciles. Si el «10 % superior» no debe partir valores iguales, usa PERCENT_RANK() <= 0.1 o CUME_DIST(), que dan el mismo valor a las filas empatadas.

Notas por motor

ROW_NUMBER, RANK, DENSE_RANK, NTILE, PERCENT_RANK y CUME_DIST son SQL estándar y funcionan igual en PostgreSQL, DuckDB, SQL Server (las dos últimas desde 2012), Oracle, BigQuery y Snowflake. MySQL las tiene desde la versión 8.0; MySQL 5.7, todavía frecuente en hostings compartidos y aplicaciones antiguas, no tiene funciones de ventana. SQL Server exige ORDER BY dentro de OVER en todas ellas, incluida ROW_NUMBER; PostgreSQL y DuckDB aceptan ROW_NUMBER() OVER (), pero entonces la numeración es arbitraria. Si trabajas con Power BI, la misma lógica existe en DAX con RANKX (con opción Skip o Dense para los empates).

Preguntas frecuentes

¿Qué diferencia hay entre ROW_NUMBER, RANK y DENSE_RANK?

Solo se diferencian en los empates. ROW_NUMBER da a cada fila un número único (1, 2, 3, 4). RANK da el mismo número a las filas empatadas y se salta los siguientes (1, 2, 2, 4). DENSE_RANK da el mismo número a las empatadas sin saltar (1, 2, 2, 3).

¿Cuál uso para el top N por grupo?

ROW_NUMBER si cada grupo debe devolver exactamente N filas. RANK si deben entrar todos los empatados en el puesto N, lo que puede devolver más de N filas por grupo. En ambos casos, calcula el número en una CTE y filtra en la consulta exterior.

¿Por qué RANK se salta números?

Porque RANK es el número de filas que hay delante de la actual, más uno. Si cuatro filas empatan en el 3.er puesto, la siguiente tiene seis filas delante, así que es la 7.ª. DENSE_RANK cuenta valores distintos, así que la siguiente es la 4.ª.

¿Cómo obtengo el segundo valor más alto en SQL?

Usa DENSE_RANK() OVER (ORDER BY valor DESC) y quédate con dense_rnk = 2. RANK puede no devolver nada si el valor máximo lo comparten varias filas, y ROW_NUMBER puede devolver otra vez el valor máximo.

¿ROW_NUMBER es determinista?

Solo si el ORDER BY dentro de OVER es único. Con empates en el orden, la base de datos puede numerar las filas empatadas de forma distinta entre ejecuciones. Añade una columna única, como un id, como último criterio de orden.

También en English, Português (Brasil), Tiếng Việt, 简体中文