Saltar al contenido

lección 3

Rankings: ROW_NUMBER, RANK y DENSE_RANK

Dame los top 3 por categoría, deduplica registros, y domina el patrón CTE + ROW_NUMBER + WHERE.

55 min

Si hay UNA window function que debes dominar por encima de todas, es ROW_NUMBER. No exagero cuando digo que he usado ROW_NUMBER más que cualquier otra función en mis 15 años de carrera. La pregunta "dame los top N por grupo" aparece en el 90% de los proyectos de datos, en el 100% de las entrevistas técnicas, y tiene una solución elegantísima con ROW_NUMBER.

Pero ROW_NUMBER tiene dos primos que a veces confunden: RANK y DENSE_RANK. Los tres asignan números a las filas, pero se comportan diferente ante empates. Vamos a desmontarlos uno por uno, entender cuándo usar cada uno, y dominar los patrones prácticos que te van a salvar en el mundo real.

### ROW_NUMBER: un número único por fila, sin excepciones

ROW_NUMBER asigna un número secuencial (1, 2, 3, 4...) a cada fila dentro de su partición, según el ORDER BY especificado. Es determinista: nunca repite números, nunca salta números. Si hay empates, desempata arbitrariamente (pero consistentemente dentro de la misma ejecución).

La analogía: imagina una cola de personas esperando para entrar a un concierto. Cada persona recibe un número único de ticket. Si dos personas llegaron "al mismo tiempo", el portero decide quién va primero — pero ambas obtienen un número diferente. No hay dos tickets iguales.

1-- ROW_NUMBER: un numero unico por fila
2SELECT
3 nombre,
4 categoria,
5 ventas,
6 ROW_NUMBER() OVER (
7 PARTITION BY categoria
8 ORDER BY ventas DESC
9 ) AS ranking
10FROM vendedores;
11
12-- Resultado:
13-- Electronica | Ana | 5000 | 1
14-- Electronica | Luis | 5000 | 2 → mismo valor, distinto ranking
15-- Electronica | Eva | 4200 | 3
16-- Hogar | Juan | 8000 | 1 → nueva particion, reinicia en 1
17-- Hogar | Sara | 6500 | 2

ROW_NUMBER nunca repite: aunque Ana y Luis vendieron lo mismo, una es 1 y otro es 2. Añade una columna de desempate (ORDER BY ventas DESC, id ASC) para que el orden sea determinista.

### RANK: respeta los empates, salta posiciones

RANK hace lo mismo que ROW_NUMBER, pero respeta los empates. Si dos personas tienen el mismo valor, ambas obtienen la misma posición. Pero después SALTA la siguiente posición. Es como el medallero olímpico: si hay dos oros, no hay plata — el siguiente es bronce (posición 3).

1-- RANK: empates comparten posicion, luego salta
2SELECT
3 nombre, ventas,
4 RANK() OVER (ORDER BY ventas DESC) AS ranking
5FROM vendedores;
6
7-- Resultado:
8-- Ana | 5000 | 1
9-- Luis | 5000 | 1 → empate: misma posicion
10-- Eva | 4200 | 3 → salta el 2! (habia 2 en posicion 1)
11-- Juan | 3800 | 4

RANK: dos primeros puestos → no hay segundo puesto. El siguiente es tercero.

### DENSE_RANK: respeta empates, NO salta posiciones

DENSE_RANK es como RANK pero sin saltar números. Si hay empate en la posición 1, el siguiente valor distinto es posición 2 (no 3). Es el ranking "denso" — sin huecos. útil cuando quieres saber "ícuántos niveles de rendimiento distintos hay?" en vez de "¿en qué posición absoluta está?".

1-- DENSE_RANK: empates comparten posicion, NO salta
2SELECT
3 nombre, ventas,
4 ROW_NUMBER() OVER (ORDER BY ventas DESC) AS row_num,
5 RANK() OVER (ORDER BY ventas DESC) AS rank,
6 DENSE_RANK() OVER (ORDER BY ventas DESC) AS dense_rank
7FROM vendedores;
8
9-- Resultado comparativo:
10-- Ana | 5000 | row_num: 1 | rank: 1 | dense_rank: 1
11-- Luis | 5000 | row_num: 2 | rank: 1 | dense_rank: 1
12-- Eva | 4200 | row_num: 3 | rank: 3 | dense_rank: 2 → rank salta, dense no
13-- Juan | 3800 | row_num: 4 | rank: 4 | dense_rank: 3

Los tres lado a lado. ROW_NUMBER: siempre único. RANK: salta. DENSE_RANK: no salta.

¿Cuál usar? ROW_NUMBER para top-N y deduplicación. RANK para competiciones. DENSE_RANK para niveles.

### EL patrón estrella: CTE + ROW_NUMBER + WHERE rn = 1

Este es probablemente el patrón de SQL más útil que existe. Lo usarás para: obtener el top N por grupo, deduplicar registros, obtener el último registro de cada entidad, encontrar el primer evento de cada usuario. Es TAN común que merece su propio nombre: "el patrón de ranking con filtro".

1-- PATRON: Top N por grupo
2-- "Dame los 3 clientes que mas gastan en cada ciudad"
3WITH ranked AS (
4 SELECT
5 c.ciudad,
6 c.nombre,
7 SUM(p.importe) AS gasto_total,
8 ROW_NUMBER() OVER (
9 PARTITION BY c.ciudad
10 ORDER BY SUM(p.importe) DESC
11 ) AS rn
12 FROM clientes c
13 JOIN pedidos p ON p.cliente_id = c.id
14 WHERE p.estado = 'completado'
15 GROUP BY c.ciudad, c.nombre
16)
17SELECT ciudad, nombre, gasto_total
18FROM ranked
19WHERE rn <= 3
20ORDER BY ciudad, rn;

Top 3 por ciudad. Cambia el 3 por cualquier N. Cambia ciudad por cualquier grupo.

### Caso real: deduplicación con ROW_NUMBER

En el mundo real, los datos están sucios. Recibes un export con clientes duplicados (misma persona, múltiples registros). Quieres quedarte solo con el registro más reciente de cada email. ROW_NUMBER es tu herramienta:

1-- Deduplicacion: quedate con el registro mas reciente por email
2WITH deduped AS (
3 SELECT
4 *,
5 ROW_NUMBER() OVER (
6 PARTITION BY email
7 ORDER BY fecha_actualizacion DESC
8 ) AS rn
9 FROM clientes_raw
10)
11SELECT * FROM deduped WHERE rn = 1;
12-- Solo el registro mas reciente de cada email sobrevive

Deduplicación elegante. PARTITION BY la clave duplicada, ORDER BY el criterio de desempate.

Consejo de senior: el 80% de mis usos de ROW_NUMBER caen en dos patrones: "top N por grupo" y "deduplicar por clave". Si dominas estos dos, estás cubierto para la mayoría del trabajo real. El otro 20% son variaciones creativas de estos mismos patrones.

En DuckDB puedes ahorrarte la CTE entera con QUALIFY: SELECT *, ROW_NUMBER() OVER (PARTITION BY ciudad ORDER BY gasto DESC) AS rn FROM ... QUALIFY rn <= 3. Mismo resultado, sin envoltorio. Pero la CTE funciona en cualquier motor — úsala cuando el SQL tenga que ser portable (PostgreSQL, Redshift, BigQuery).

### ¿Cuándo usar RANK en vez de ROW_NUMBER?

Usa RANK cuando los empates importan semánticamente. Ejemplo: "muéstrame los productos con el precio más alto de cada categoría". Si dos productos cuestan lo mismo, ambos merecen ser "el más caro" — no es justo que uno sea 1 y otro sea 2 arbitrariamente. Ahí RANK es más correcto que ROW_NUMBER.

1-- Productos mas caros por categoria (empates incluidos)
2WITH ranked AS (
3 SELECT
4 categoria,
5 nombre,
6 precio,
7 RANK() OVER (PARTITION BY categoria ORDER BY precio DESC) AS rn
8 FROM productos
9)
10SELECT * FROM ranked WHERE rn = 1;
11-- Si hay 2 productos a 199.99 en Electronica, AMBOS aparecen

Con RANK, si hay empate en el primer puesto, se devuelven ambos. Con ROW_NUMBER solo uno.

Cuidado: si usas RANK() WHERE rn <= 3, podrías obtener MÁS de 3 filas por grupo (si hay empates). Si necesitas exactamente 3 filas sin importar empates, usa ROW_NUMBER. Si necesitas "todos los que están en el top 3 de rendimiento" aunque sean 5 personas empatadas, usa RANK.

Regla práctica: ¿necesitas exactamente N filas? → ROW_NUMBER. ¿Necesitas respetar empates? → RANK. ¿Necesitas saber "cuántos niveles distintos hay"? → DENSE_RANK. ¿Necesitas dividir en grupos iguales (cuartiles, deciles)? → NTILE.

### NTILE: dividir en N grupos iguales

NTILE(N) reparte las filas de cada partición en N grupos de tamaño lo más similar posible. No te dice la posición de cada fila — te dice en qué "cubo" cae. Es la función que necesitas cuando el negocio pide cuartiles, quintiles, deciles o "divide a los clientes en 4 tiers".

1-- Dividir clientes en 4 cuartiles por gasto
2WITH gasto AS (
3 SELECT cliente_id, SUM(importe)::INTEGER AS total
4 FROM pedidos WHERE estado = 'completado'
5 GROUP BY cliente_id
6)
7SELECT
8 cliente_id,
9 total,
10 NTILE(4) OVER (ORDER BY total DESC) AS cuartil
11FROM gasto
12ORDER BY total DESC
13LIMIT 20;
14
15-- cuartil 1 = top 25%, cuartil 4 = bottom 25%

NTILE(4) = cuartiles. NTILE(10) = deciles. NTILE(100) = percentiles.

## ejercicios

[01]

Top 3 clientes por ciudad

El director regional quiere una tabla con los 3 clientes que más gastan en cada ciudad (solo pedidos completados). Usa el patrón CTE + ROW_NUMBER + filtro.

Cargando editor...
[02]

Deduplicar pedidos duplicados

Hay pedidos duplicados en la tabla (mismo cliente_id + misma fecha + mismo importe). Quédate solo con el de menor id (el primero que llegó). Muestra cuántos registros se eliminan.

Cargando editor...
[03]

último pedido de cada cliente

Customer Success quiere contactar a cada cliente sobre su último pedido. Obtén: cliente_id, nombre, fecha del último pedido, importe del último pedido. Usa ROW_NUMBER ordenando por fecha DESC.

Cargando editor...
[04]

Productos más caros por categoría (con empates)

Producto quiere ver los 3 productos más caros de cada categoría. Calcula ROW_NUMBER, RANK y DENSE_RANK sobre la misma ventana para ver cómo se comportan las tres ante empates. Filtra por row_num <= 3.

Cargando editor...

Regístrate para guardar tu progreso.

## comentarios

Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.

Inicia sesión para comentar y responder.

cargando comentarios...