DENSE_RANK

DENSE_RANK asigna rangos a las filas dentro de su partición de forma similar a RANK, pero sin dejar huecos cuando hay empates. Si dos filas comparten el rango 2, la siguiente fila recibe el rango 3, no el 4. El nombre "dense" (denso) hace referencia precisamente a esta propiedad: la secuencia de rangos es compacta, sin saltos.

Esta función es especialmente útil cuando necesitas saber cuántos valores distintos están por encima de una fila determinada. Si un empleado tiene DENSE_RANK igual a 4, significa que hay exactamente 3 niveles salariales por encima del suyo, independientemente de cuántas personas estén en cada nivel.

Sintaxis

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

Igual que ROW_NUMBER y RANK, DENSE_RANK no recibe argumentos y se configura completamente a través de OVER. El ORDER BY es necesario para que los rangos tengan sentido.

Comportamiento básico

Compara directamente DENSE_RANK con RANK para apreciar la diferencia:

SELECT
    nombre,
    salario,
    RANK() OVER (ORDER BY salario DESC) AS rank_val,
    DENSE_RANK() OVER (ORDER BY salario DESC) AS dense_rank_val
FROM empleados;
nombresalariorank_valdense_rank_val
Carlos López72000.0011
Ana García65000.0022
Marta Ruiz65000.0022
Roberto Fernández58000.0043
Laura Díaz52000.0054
David Moreno48000.0065
Elena Torres48000.0065
Pedro Sánchez43000.0086

Observa que RANK salta del 2 al 4 después del empate de Ana y Marta, mientras que DENSE_RANK continúa con 3. Igualmente, después del empate de David y Elena en la posición 6 según RANK, Pedro es 8. Pero con DENSE_RANK, Pedro es 6. El rango máximo de DENSE_RANK (6) te dice que hay exactamente 6 niveles salariales distintos en la tabla.

Las tres funciones lado a lado

La comparación completa de las tres funciones de numeración aclara definitivamente cuándo usar cada una:

SELECT
    nombre,
    salario,
    ROW_NUMBER() OVER (ORDER BY salario DESC) AS row_num,
    RANK() OVER (ORDER BY salario DESC) AS rank_val,
    DENSE_RANK() OVER (ORDER BY salario DESC) AS dense_val
FROM empleados
ORDER BY salario DESC;
nombresalariorow_numrank_valdense_val
Carlos López72000.00111
Ana García65000.00222
Marta Ruiz65000.00322
Roberto Fernández58000.00443
Laura Díaz52000.00554
David Moreno48000.00665
Elena Torres48000.00765
Pedro Sánchez43000.00886

ROW_NUMBER produce 1 a 8 sin repetir. RANK produce 1, 2, 2, 4, 5, 6, 6, 8 con huecos. DENSE_RANK produce 1, 2, 2, 3, 4, 5, 5, 6 sin huecos. La regla práctica es: ROW_NUMBER para identificadores únicos, RANK para clasificaciones tipo competición, y DENSE_RANK cuando los huecos distorsionarían el análisis.

Caso práctico: niveles salariales por departamento

DENSE_RANK brilla cuando necesitas identificar cuántos niveles distintos existen. Supón que Recursos Humanos quiere saber cuántas bandas salariales reales hay en cada departamento:

SELECT
    nombre,
    departamento,
    salario,
    DENSE_RANK() OVER (
        PARTITION BY departamento
        ORDER BY salario DESC
    ) AS banda_salarial
FROM empleados
ORDER BY departamento, banda_salarial;
nombredepartamentosalariobanda_salarial
Roberto FernándezMarketing72000.001
Laura DíazMarketing58000.002
Javier RuizMarketing58000.002
Sofía MorenoMarketing45000.003
Carlos LópezVentas68000.001
Ana GarcíaVentas62000.002
Marta RuizVentas55000.003
Pedro SánchezVentas48000.004

Marketing tiene 3 bandas salariales distintas (a pesar de tener 4 empleados) mientras que Ventas tiene 4. El valor máximo de DENSE_RANK en cada departamento indica el número de niveles salariales distintos. Con RANK, el máximo sería 4 en Marketing debido al hueco, perdiendo esa información semántica.

Caso práctico: top N con empates incluidos

Una de las ventajas clave de DENSE_RANK sobre ROW_NUMBER para el patrón top-N es que incluye todos los empates. Si pides "los 3 mejores precios" y hay dos productos con el mismo tercer precio, DENSE_RANK los incluye a ambos:

WITH ranking_precios AS (
    SELECT
        nombre,
        categoria,
        precio,
        DENSE_RANK() OVER (
            PARTITION BY categoria
            ORDER BY precio DESC
        ) AS dr
    FROM productos
    WHERE activo = 1
)
SELECT nombre, categoria, precio, dr AS nivel_precio
FROM ranking_precios
WHERE dr <= 3
ORDER BY categoria, dr;
nombrecategoriaprecionivel_precio
Monitor 27" 4KElectrónica349.001
Auriculares Bluetooth ProElectrónica189.002
Teclado mecánico RGBElectrónica129.003
Ratón gaming inalámbricoElectrónica129.003
Abrigo lana italianoRopa245.001
Botas cuero premiumRopa189.002
Vestido seda naturalRopa175.003

En Electrónica aparecen 4 productos porque el teclado y el ratón comparten el tercer nivel de precio. Con ROW_NUMBER, uno de los dos se habría quedado fuera arbitrariamente. Con RANK, el resultado sería idéntico a DENSE_RANK en este caso porque el filtro es <= 3. Sin embargo, si el empate estuviera en la posición 2 y filtraras por <= 3, RANK asignaría el siguiente como 4 y lo excluiría, mientras que DENSE_RANK le asignaría 3 y lo incluiría.

Caso práctico: encontrar el segundo valor más alto

Un problema frecuente es obtener el segundo valor más alto (o el tercero, cuarto, etc.) de cada grupo. DENSE_RANK garantiza que siempre exista un rango 2 si hay al menos dos valores distintos:

WITH salarios_rankeados AS (
    SELECT
        departamento,
        salario,
        DENSE_RANK() OVER (
            PARTITION BY departamento
            ORDER BY salario DESC
        ) AS dr
    FROM empleados
)
SELECT DISTINCT departamento, salario AS segundo_salario_mas_alto
FROM salarios_rankeados
WHERE dr = 2;
departamentosegundo_salario_mas_alto
Marketing58000.00
Ventas62000.00
Soporte45000.00

Si usaras RANK y el primer salario estuviera empatado entre dos personas, el siguiente rango sería 3 y la consulta con WHERE dr = 2 no devolvería nada. DENSE_RANK siempre produce rangos consecutivos, así que el filtro dr = 2 siempre funciona si hay al menos dos valores distintos.

Manejo de NULL

El comportamiento de DENSE_RANK con NULL es el mismo que el de RANK: todos los valores NULL se consideran iguales y reciben el mismo rango:

SELECT
    nombre,
    bonus,
    DENSE_RANK() OVER (ORDER BY bonus DESC) AS dr
FROM empleados;
nombrebonusdr
Carlos López8000.001
Ana García5000.002
Marta Ruiz5000.002
Roberto Fernández3000.003
Pedro SánchezNULL4
Elena TorresNULL4

Los NULL se agrupan en el mismo rango (4 en este caso, en orden descendente aparecen al final). El rango máximo sigue siendo la cuenta de valores distintos, incluyendo el grupo de NULL como uno más.

Combinación con otras funciones

DENSE_RANK es particularmente útil combinado con funciones de agregación para segmentar datos en niveles:

SELECT
    nombre,
    departamento,
    salario,
    DENSE_RANK() OVER (ORDER BY salario DESC) AS nivel_global,
    CONCAT(
        'Nivel ', DENSE_RANK() OVER (ORDER BY salario DESC),
        ' de ', (SELECT COUNT(DISTINCT salario) FROM empleados)
    ) AS etiqueta,
    ROUND(
        salario / MAX(salario) OVER () * 100, 1
    ) AS pct_del_maximo
FROM empleados
ORDER BY nivel_global;
nombredepartamentosalarionivel_globaletiquetapct_del_maximo
Carlos LópezVentas72000.001Nivel 1 de 6100.0
Ana GarcíaVentas65000.002Nivel 2 de 690.3
Marta RuizVentas65000.002Nivel 2 de 690.3
Roberto FernándezMarketing58000.003Nivel 3 de 680.6
Laura DíazMarketing52000.004Nivel 4 de 672.2
David MorenoSoporte48000.005Nivel 5 de 666.7
Elena TorresSoporte48000.005Nivel 5 de 666.7
Pedro SánchezVentas43000.006Nivel 6 de 659.7

La etiqueta "Nivel 2 de 6" comunica de forma inmediata que este salario ocupa la segunda posición entre 6 niveles distintos, información que RANK distorsionaría al producir un máximo de 8 en lugar de 6.

Practica con DENSE_RANK

Usa el editor para asignar rankings consecutivos sin saltos:

Simulador SQL
Ctrl+Enter para ejecutar

En el siguiente artículo veremos NTILE para dividir filas en grupos iguales.

Escrito por Eduardo Lázaro