SQL en la práctica
4 min de lectura·

En qué se diferencian ROW_NUMBER, RANK y DENSE_RANK

Las tres son funciones de ventana que numeran filas según un orden dado. La diferencia solo aparece con valores iguales:

  • ROW_NUMBER ignora las repeticiones: cada fila recibe su propio número, 1, 2, 3, 4.
  • RANK da el mismo puesto a los valores repetidos y asigna el siguiente dejando un hueco: 1, 1, 3, 4.
  • DENSE_RANK da el mismo puesto a los valores repetidos sin huecos: 1, 1, 2, 3.

Toda la diferencia en una sola consulta

Tomemos los pedidos de un servicio de reparto y ordenémoslos por importe con las tres funciones a la vez. Los datos tienen repeticiones: dos pedidos de 99,96 y tres de 74,97. Justo ahí se ven las diferencias:

MySQL 8.4
SELECT
    order_id,
    total_amount,
    ROW_NUMBER() OVER (ORDER BY total_amount DESC) AS row_num,
    RANK() OVER (ORDER BY total_amount DESC) AS rnk,
    DENSE_RANK() OVER (ORDER BY total_amount DESC) AS dense_rnk
FROM orders
ORDER BY total_amount DESC
LIMIT 8;
order_idtotal_amountrow_numrnkdense_rnk
2099.96111
2699.96211
21891.81332
23780.62443
25879.15554
2974.97665
32674.97765
38974.97865

Lo leemos fila a fila:

  • Los pedidos 20 y 26 cuestan lo mismo. Aun así, ROW_NUMBER les ha repartido los números 1 y 2, y cuál va primero lo decide el SGBD de forma arbitraria. RANK y DENSE_RANK les han dado a los dos el puesto 1, como corresponde.
  • El pedido 218 es el siguiente por importe. RANK le ha dado el 3: los dos pedidos de arriba «ocuparon» los puestos 1 y 2. DENSE_RANK le ha dado el 2: no deja huecos.
  • Con los tres pedidos de 74,97 se repite la historia: RANK da 6, 6, 6 y después vendría el 9; DENSE_RANK da 5, 5, 5 y después el 6.

Es fácil de recordar: RANK cuenta como en el deporte, donde tras dos medallas de oro viene directamente el bronce y no hay segundo puesto. DENSE_RANK («ranking denso») funciona como una numeración de niveles: el siguiente valor siempre es exactamente uno más.

Una función ordena los valores 100, 100, 90 de mayor a menor. ¿Qué puestos devolverá DENSE_RANK?

Cuándo usar cada una

ROW_NUMBER: top-N de cada grupo

La tarea práctica más habitual con funciones de ventana es «elige el mejor elemento de cada categoría». Busquemos el producto que más ingresos genera en cada categoría:

MySQL 8.4
WITH ranked AS (
    SELECT
        p.category,
        p.name,
        SUM(o.total_amount) AS revenue,
        ROW_NUMBER() OVER (PARTITION BY p.category ORDER BY SUM(o.total_amount) DESC) AS rn
    FROM orders o
    JOIN products p ON p.product_id = o.product_id
    GROUP BY p.category, p.name
)
SELECT category, name, revenue
FROM ranked
WHERE rn = 1
ORDER BY category;
categorynamerevenue
DrinksGreen Smoothie272.48
FoodSushi Set1728.19
GroceryOlive Oil 500ml789.92
SnacksDark Chocolate306.47

PARTITION BY category reinicia la numeración en cada categoría, así que rn = 1 es el líder de su grupo. Cambia la condición a rn <= 3 y tendrás el top 3.

Aquí lo importante es que ROW_NUMBER garantiza exactamente una fila por grupo. Con ingresos iguales, RANK devolvería dos «líderes»: a veces es lo que buscas, pero lo más frecuente es que rompa un informe pensado para una sola fila.

Necesitas exactamente un pedido, el más caro, de cada cliente, aunque un cliente tenga dos pedidos con el mismo importe máximo. ¿Qué función te sirve?

ROW_NUMBER: deduplicación

El segundo escenario clásico es eliminar duplicados dejando un solo registro. Numeramos las filas dentro de los grupos de valores iguales y quitamos todo lo que vaya después de la primera:

MySQL 8.4
WITH numbered AS (
    SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at) AS rn
    FROM users
)
DELETE FROM users
WHERE id IN (SELECT id FROM numbered WHERE rn > 1);

Para cada email se queda el registro más antiguo (ORDER BY created_at) y el resto se borra.

RANK y DENSE_RANK: puestos y niveles

RANK se elige allí donde rige la lógica deportiva de los puestos: clasificaciones, concursos, «en qué puesto ha quedado el deportista». DENSE_RANK, donde hacen falta niveles sin huecos: escalas salariales, agrupación por umbrales, «el N-ésimo valor más alto».

Extra: NTILE reparte las filas en grupos

De la misma familia de funciones de ranking es NTILE(n): divide las filas en n partes aproximadamente iguales y devuelve el número de la parte. Así, con una sola consulta, los clientes quedan repartidos en cuartiles según su gasto:

MySQL 8.4
SELECT
    user_id,
    SUM(total_amount) AS spent,
    NTILE(4) OVER (ORDER BY SUM(total_amount) DESC) AS quartile
FROM orders
GROUP BY user_id
ORDER BY spent DESC
LIMIT 5;

Los clientes con quartile = 1 son el cuarto superior por gasto: un segmento listo para un programa de fidelización.

Ejercicio de entrevista: el segundo importe más alto

«Encuentra el segundo salario más alto» es una pregunta que lleva décadas pasando de entrevista en entrevista. Con DENSE_RANK se resuelve de forma muy clara:

MySQL 8.4
WITH ranked AS (
    SELECT order_id, total_amount, DENSE_RANK() OVER (ORDER BY total_amount DESC) AS rnk
    FROM orders
)
SELECT DISTINCT total_amount
FROM ranked
WHERE rnk = 2;
total_amount
91.81

¿Por qué precisamente DENSE_RANK? Tenemos dos importes máximos de 99,96. RANK les daría el puesto 1 y al siguiente el puesto 3, y la condición rnk = 2 no encontraría nada. DENSE_RANK no deja huecos y le da a 91,81 el puesto 2: justo el «segundo más alto» tal como lo entiende quien pregunta.

Siguientes pasos

Artículos relacionados