ROW_NUMBER

ROW_NUMBER asigna un número entero secuencial a cada fila dentro de su partición, empezando desde 1. A diferencia de RANK y DENSE_RANK, nunca produce empates ni huecos: cada fila recibe un número único incluso cuando varias filas tienen el mismo valor en la columna de orden. Es probablemente la función de ventana más utilizada en la práctica porque resuelve problemas cotidianos como paginar resultados, obtener el registro más reciente por grupo y eliminar duplicados.

Cuando dos filas tienen el mismo valor en la columna de orden, MySQL asigna los números de forma arbitraria pero determinista dentro de la misma ejecución. Si necesitas un orden completamente predecible en caso de empate, añade una segunda columna al ORDER BY, como la clave primaria.

Sintaxis

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

ROW_NUMBER no acepta argumentos. Todo su comportamiento se define dentro de la cláusula OVER. El ORDER BY es esencial porque determina en qué secuencia se asignan los números. Sin ORDER BY, los números se asignan en un orden no garantizado, lo cual rara vez es útil.

Comportamiento básico

En su forma más simple, ROW_NUMBER numera todas las filas del resultado según un orden:

SELECT
    ROW_NUMBER() OVER (ORDER BY salario DESC) AS num,
    nombre,
    departamento,
    salario
FROM empleados;
numnombredepartamentosalario
1Roberto FernándezMarketing72000.00
2Carlos LópezVentas65000.00
3Ana GarcíaVentas58000.00
4Marta RuizVentas55000.00
5Laura DíazMarketing52000.00
6David MorenoSoporte48000.00
7Elena TorresSoporte45000.00
8Pedro SánchezVentas43000.00

Cada empleado recibe un número único del 1 al 8 según su salario de mayor a menor. Si dos empleados tuvieran exactamente el mismo salario, uno recibiría por ejemplo el 3 y el otro el 4, sin repetir ni saltar números.

Numerar por grupo con PARTITION BY

Al añadir PARTITION BY, la numeración se reinicia en cada grupo:

SELECT
    ROW_NUMBER() OVER (
        PARTITION BY departamento
        ORDER BY salario DESC
    ) AS num_en_depto,
    nombre,
    departamento,
    salario
FROM empleados;
num_en_deptonombredepartamentosalario
1Roberto FernándezMarketing72000.00
2Laura DíazMarketing52000.00
1Carlos LópezVentas65000.00
2Ana GarcíaVentas58000.00
3Marta RuizVentas55000.00
4Pedro SánchezVentas43000.00
1David MorenoSoporte48000.00
2Elena TorresSoporte45000.00

Cada departamento tiene su propia secuencia empezando en 1. Roberto es el número 1 en Marketing, Carlos es el número 1 en Ventas, y David es el número 1 en Soporte.

Caso práctico: paginación de resultados

ROW_NUMBER es la forma más limpia de implementar paginación en consultas complejas. Supón que necesitas la página 3 de un listado de productos con 10 elementos por página:

WITH productos_numerados AS (
    SELECT
        ROW_NUMBER() OVER (ORDER BY nombre) AS num_fila,
        id_producto,
        nombre,
        precio,
        categoria
    FROM productos
    WHERE activo = 1
)
SELECT id_producto, nombre, precio, categoria
FROM productos_numerados
WHERE num_fila BETWEEN 21 AND 30;
id_productonombrepreciocategoria
78Impresora láser HP289.00Electrónica
134Jersey lana merino45.90Ropa
22Lámpara de escritorio LED34.50Hogar
91Libro SQL avanzado29.95Libros
45Mochila viaje 40L67.00Accesorios

La ventaja sobre LIMIT ... OFFSET es que ROW_NUMBER te permite numerar los registros de forma predecible y luego filtrar con WHERE. Además, puedes añadir el total de filas con otra función de ventana en el mismo CTE.

Caso práctico: top N por grupo

Uno de los problemas más clásicos de SQL es obtener los N mejores registros de cada grupo. Con ROW_NUMBER se resuelve de forma elegante. Supón que quieres los 2 productos más vendidos de cada categoría:

WITH ranking_productos AS (
    SELECT
        p.nombre,
        p.categoria,
        SUM(dp.cantidad) AS unidades_vendidas,
        SUM(dp.cantidad * dp.precio_unitario) AS ingresos,
        ROW_NUMBER() OVER (
            PARTITION BY p.categoria
            ORDER BY SUM(dp.cantidad) DESC
        ) AS rn
    FROM productos p
        JOIN detalle_pedidos dp ON p.id_producto = dp.id_producto
    GROUP BY p.id_producto, p.nombre, p.categoria
)
SELECT nombre, categoria, unidades_vendidas, ingresos
FROM ranking_productos
WHERE rn <= 2;
nombrecategoriaunidades_vendidasingresos
Monitor 27" 4KElectrónica342119700.00
Teclado mecánico RGBElectrónica28928900.00
Camiseta algodón orgánicoRopa56714175.00
Pantalón chino slimRopa42321150.00
Aceite oliva virgen extra 1LAlimentación89010680.00
Café molido premium 500gAlimentación7569072.00

El patrón es siempre el mismo: asignar ROW_NUMBER con PARTITION BY la columna de grupo, envolver en CTE y filtrar por rn <= N. Es tan frecuente que vale la pena memorizarlo.

Caso práctico: eliminar duplicados conservando el más reciente

Otro uso habitual es la deduplicación. Cuando una tabla tiene registros duplicados y necesitas quedarte solo con el más reciente de cada grupo, ROW_NUMBER te permite identificarlos sin ambigüedad:

WITH clientes_ordenados AS (
    SELECT
        *,
        ROW_NUMBER() OVER (
            PARTITION BY email
            ORDER BY fecha_registro DESC
        ) AS rn
    FROM clientes
)
SELECT id_cliente, nombre, email, fecha_registro
FROM clientes_ordenados
WHERE rn = 1;
id_clientenombreemailfecha_registro
1045Ana García Ruizana.garcia@correo.com2024-08-15
892Carlos López Martíncarlos.lopez@correo.com2024-06-22
1203Marta Díaz Sotomarta.diaz@correo.com2024-11-03

Para cada email duplicado, la consulta conserva solo el registro con la fecha de registro más reciente. Si quisieras eliminar los duplicados de forma permanente, podrías usar esta misma lógica dentro de un DELETE:

DELETE c FROM clientes c
JOIN (
    SELECT id_cliente,
        ROW_NUMBER() OVER (
            PARTITION BY email
            ORDER BY fecha_registro DESC
        ) AS rn
    FROM clientes
) ranked ON c.id_cliente = ranked.id_cliente
WHERE ranked.rn > 1;

Diferencia con RANK

La diferencia entre ROW_NUMBER y RANK se manifiesta cuando hay empates. Observa esta consulta donde dos empleados tienen el mismo salario:

SELECT
    nombre,
    salario,
    ROW_NUMBER() OVER (ORDER BY salario DESC) AS row_num,
    RANK() OVER (ORDER BY salario DESC) AS ranking
FROM empleados
WHERE departamento = 'Ventas';
nombresalariorow_numranking
Carlos López65000.0011
Ana García55000.0022
Marta Ruiz55000.0032
Pedro Sánchez43000.0044

Ana y Marta ganan lo mismo. RANK les asigna a ambas el rango 2 y salta al 4 para Pedro. ROW_NUMBER les da números distintos (2 y 3) de forma arbitraria. Usa ROW_NUMBER cuando necesitas un número único por fila, y RANK cuando los empates deben reflejarse en el resultado.

Manejo de NULL

ROW_NUMBER trata los valores NULL como cualquier otro valor a efectos de ordenación. En MySQL, los NULL aparecen primero en orden ascendente y al final en orden descendente:

SELECT
    ROW_NUMBER() OVER (ORDER BY fecha_baja) AS num,
    nombre,
    fecha_baja
FROM empleados;
numnombrefecha_baja
1Pedro SánchezNULL
2Elena TorresNULL
3Laura Díaz2023-06-15
4Roberto Fernández2024-01-20

Los empleados sin fecha de baja (activos) reciben los primeros números porque NULL se ordena antes que cualquier fecha en orden ascendente. Si prefieres que los activos aparezcan al final, puedes usar ORDER BY fecha_baja DESC o ORDER BY ISNULL(fecha_baja), fecha_baja.

En cuanto a PARTITION BY, si la columna de partición contiene NULL, todas las filas con NULL se agrupan en una sola partición.

Combinación con otras funciones

ROW_NUMBER se combina frecuentemente con funciones de agregación de ventana para crear informes enriquecidos:

SELECT
    ROW_NUMBER() OVER (
        PARTITION BY categoria ORDER BY unidades_vendidas DESC
    ) AS posicion,
    nombre,
    categoria,
    unidades_vendidas,
    ROUND(
        unidades_vendidas * 100.0 / SUM(unidades_vendidas) OVER (PARTITION BY categoria),
        1
    ) AS pct_categoria
FROM productos
ORDER BY categoria, posicion;
posicionnombrecategoriaunidades_vendidaspct_categoria
1Monitor 27" 4KElectrónica34238.6
2Teclado mecánico RGBElectrónica28932.6
3Ratón inalámbricoElectrónica25528.8
1Camiseta algodón orgánicoRopa56757.3
2Pantalón chino slimRopa42342.7

Cada producto tiene su posición dentro de su categoría y el porcentaje que representa del total de unidades vendidas en esa categoría, todo en una sola consulta.

Practica con ROW_NUMBER

Usa el editor para numerar filas dentro de particiones:

Simulador SQL
Ctrl+Enter para ejecutar

En el siguiente artículo veremos RANK para asignar rangos que permiten empates.

Escrito por Eduardo Lázaro