RANK

RANK asigna un número de rango a cada fila dentro de su partición según el orden especificado. Cuando dos o más filas tienen el mismo valor en la columna de orden, reciben el mismo rango, y el siguiente rango se incrementa teniendo en cuenta cuántas filas empataron. Esto produce huecos en la secuencia: si dos filas comparten el rango 2, el siguiente rango será 4, no 3.

Este comportamiento es exactamente el que se usa en competiciones deportivas. Si dos atletas terminan en segundo lugar, no hay tercer lugar: el siguiente puesto es el cuarto. Por eso RANK es la función natural cuando modelas clasificaciones donde los empates deben reflejarse con fidelidad.

Sintaxis

RANK() OVER (
    [PARTITION BY columna1, columna2, ...]
    ORDER BY columna1 [ASC|DESC], columna2 [ASC|DESC], ...
)

Al igual que ROW_NUMBER, RANK no acepta argumentos. Su comportamiento se define completamente por la cláusula OVER. El ORDER BY es obligatorio en la práctica: sin él, todas las filas recibirían el rango 1 ya que no habría criterio para diferenciarlas.

Comportamiento básico

Observa cómo RANK asigna rangos cuando hay valores repetidos en la columna de orden:

SELECT
    RANK() OVER (ORDER BY total_ventas DESC) AS ranking,
    nombre,
    departamento,
    total_ventas
FROM empleados;
rankingnombredepartamentototal_ventas
1Carlos LópezVentas185000.00
2Roberto FernándezMarketing142000.00
2Ana GarcíaVentas142000.00
4Marta RuizVentas128000.00
5Laura DíazMarketing115000.00
6David MorenoSoporte98000.00
6Elena TorresSoporte98000.00
8Pedro SánchezVentas87000.00

Roberto y Ana comparten el rango 2 porque tienen el mismo total de ventas. El siguiente rango es 4, no 3, porque hay dos personas en el puesto 2. Lo mismo ocurre con David y Elena en el rango 6: el siguiente es 8. Los huecos en la numeración (no hay 3, no hay 7) son la característica definitoria de RANK.

RANK con PARTITION BY

Al particionar, el ranking se reinicia en cada grupo:

SELECT
    RANK() OVER (
        PARTITION BY departamento
        ORDER BY total_ventas DESC
    ) AS ranking_depto,
    nombre,
    departamento,
    total_ventas
FROM empleados;
ranking_deptonombredepartamentototal_ventas
1Roberto FernándezMarketing142000.00
2Laura DíazMarketing115000.00
1Carlos LópezVentas185000.00
2Ana GarcíaVentas142000.00
3Marta RuizVentas128000.00
4Pedro SánchezVentas87000.00
1David MorenoSoporte98000.00
2Elena TorresSoporte95000.00

Cada departamento tiene su propio ranking independiente. Dentro de Marketing, Roberto es primero y Laura segunda. Dentro de Ventas, la secuencia va del 1 al 4.

Caso práctico: ranking de ventas mensuales

Supón que quieres crear un ranking mensual de vendedores para un programa de incentivos. Los empates importan porque si dos vendedores alcanzan la misma cifra, ambos merecen la misma bonificación:

SELECT
    RANK() OVER (
        PARTITION BY mes
        ORDER BY monto_ventas DESC
    ) AS posicion,
    nombre,
    mes,
    monto_ventas,
    CASE
        WHEN RANK() OVER (PARTITION BY mes ORDER BY monto_ventas DESC) = 1
            THEN 'Oro'
        WHEN RANK() OVER (PARTITION BY mes ORDER BY monto_ventas DESC) <= 3
            THEN 'Plata'
        ELSE 'Participante'
    END AS categoria_incentivo
FROM ventas_empleados
WHERE mes IN ('2024-10', '2024-11');
posicionnombremesmonto_ventascategoria_incentivo
1Carlos López2024-1032000.00Oro
2Ana García2024-1028500.00Plata
2Marta Ruiz2024-1028500.00Plata
4Pedro Sánchez2024-1021000.00Participante
1Ana García2024-1135200.00Oro
2Carlos López2024-1133800.00Plata
3Marta Ruiz2024-1129100.00Plata
4Pedro Sánchez2024-1122400.00Participante

En octubre, Ana y Marta empatan en segundo lugar y ambas reciben la categoría Plata. Pedro queda en posición 4 (no 3) debido al empate, pero eso no afecta su categoría porque el filtro es <= 3.

Caso práctico: calificaciones de estudiantes

RANK es natural para clasificar notas donde los empates son frecuentes. Imagina un sistema de calificaciones por asignatura:

SELECT
    a.nombre AS asignatura,
    e.nombre AS estudiante,
    c.nota_final,
    RANK() OVER (
        PARTITION BY a.id_asignatura
        ORDER BY c.nota_final DESC
    ) AS posicion,
    CASE
        WHEN RANK() OVER (PARTITION BY a.id_asignatura ORDER BY c.nota_final DESC) = 1
            THEN 'Matrícula de Honor'
        ELSE ''
    END AS distincion
FROM calificaciones c
    JOIN estudiantes e ON c.id_estudiante = e.id_estudiante
    JOIN asignaturas a ON c.id_asignatura = a.id_asignatura
WHERE a.nombre = 'Bases de Datos'
ORDER BY posicion;
asignaturaestudiantenota_finalposiciondistincion
Bases de DatosLucía Martín9.801Matrícula de Honor
Bases de DatosDiego Navarro9.502
Bases de DatosSara Romero9.502
Bases de DatosPablo Gil8.754
Bases de DatosIrene Molina8.205
Bases de DatosAdrián Serrano7.906

Diego y Sara empatan con un 9.50 y comparten la segunda posición. Pablo, con 8.75, queda en cuarta posición. Si esta clasificación determinase quiénes reciben beca (los tres primeros), tanto Diego como Sara entrarían, y el siguiente candidato sería el cuarto.

Caso práctico: ranking global vs departamental

Un análisis interesante es mostrar a cada empleado tanto su posición global como su posición dentro de su departamento:

SELECT
    nombre,
    departamento,
    total_ventas,
    RANK() OVER (ORDER BY total_ventas DESC) AS ranking_global,
    RANK() OVER (
        PARTITION BY departamento
        ORDER BY total_ventas DESC
    ) AS ranking_depto
FROM empleados
ORDER BY ranking_global;
nombredepartamentototal_ventasranking_globalranking_depto
Carlos LópezVentas185000.0011
Roberto FernándezMarketing142000.0021
Ana GarcíaVentas142000.0022
Marta RuizVentas128000.0043
Laura DíazMarketing115000.0052
David MorenoSoporte98000.0061

Roberto es el segundo a nivel global pero el primero en Marketing. David es sexto globalmente pero lidera Soporte. Esta doble perspectiva es valiosa en evaluaciones de rendimiento donde importa tanto la posición absoluta como la relativa al equipo.

Manejo de NULL

Los valores NULL en la columna de orden reciben el mismo rango entre sí, ya que MySQL los considera equivalentes a efectos de comparación:

SELECT
    RANK() OVER (ORDER BY comision DESC) AS ranking,
    nombre,
    comision
FROM empleados;
rankingnombrecomision
1Carlos López15000.00
2Ana García12000.00
3Marta Ruiz8000.00
4Pedro SánchezNULL
4Elena TorresNULL
4David MorenoNULL

Los tres empleados sin comisión comparten el rango 4 porque NULL se considera igual a NULL en el contexto del ranking. Si necesitas que los NULL no participen en el ranking, filtra con WHERE comision IS NOT NULL.

Combinación con otras funciones

RANK se combina con funciones de agregación de ventana para crear informes de clasificación enriquecidos con contexto estadístico:

SELECT
    RANK() OVER (ORDER BY total_ventas DESC) AS posicion,
    nombre,
    total_ventas,
    ROUND(total_ventas - AVG(total_ventas) OVER (), 2) AS vs_promedio,
    ROUND(total_ventas / MAX(total_ventas) OVER () * 100, 1) AS pct_del_lider
FROM empleados
ORDER BY posicion;
posicionnombretotal_ventasvs_promediopct_del_lider
1Carlos López185000.0063625.00100.0
2Roberto Fernández142000.0020625.0076.8
2Ana García142000.0020625.0076.8
4Marta Ruiz128000.006625.0069.2
5Laura Díaz115000.00-6375.0062.2

Cada fila muestra no solo la posición del empleado, sino cuánto está por encima o por debajo del promedio y qué porcentaje alcanza respecto al líder. Esto convierte un ranking simple en un cuadro de mando completo.

Practica con RANK

Usa el editor para asignar rankings con posibles saltos:

Simulador SQL
Ctrl+Enter para ejecutar

En el siguiente artículo veremos DENSE_RANK para rangos sin huecos.

Escrito por Eduardo Lázaro