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.4SELECT 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;
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.4WITH 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;
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.4WITH 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.4SELECT 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.4WITH 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;
¿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
- cómo funcionan las funciones de ventana, OVER y PARTITION BY, en la lección Funciones de ventana;
- los tipos de funciones de ventana y su sintaxis, en la lección Tipos de funciones de ventana;
- la ordenación dentro de la ventana, en la lección Ordenación en las funciones de ventana;
- los ejercicios de ranking aparecen a menudo en la Práctica SQL y en las preguntas de entrevista.