1. Qué es
Cuatro funciones que numeran filas en vez de agregarlas. No calculan una suma ni una media: dan una posición.
| Función | Qué da | Con empates |
|---|---|---|
row_number() |
1, 2, 3, 4… | los rompe: números consecutivos siempre |
rank() |
posición de clasificación | mismo número, y deja hueco después |
dense_rank() |
posición sin huecos | mismo número, sin hueco |
ntile(n) |
en qué grupo de n cae | reparte por tamaño, no por valor |
Ninguna lleva argumento salvo ntile. Todas necesitan ORDER BY dentro de OVER — sin un orden no hay posición.
2. Las tres primeras, lado a lado
La única forma de entenderlas es verlas sobre datos con empates. Beverages los tiene: cuatro productos a 18,00 y dos a 14,00.
SELECT p.product_name, p.unit_price,
row_number() OVER (ORDER BY p.unit_price DESC) AS rn,
rank() OVER (ORDER BY p.unit_price DESC) AS rnk,
dense_rank() OVER (ORDER BY p.unit_price DESC) AS drnk
FROM products p WHERE p.category_id = 1 ORDER BY p.unit_price DESC;
product_name | unit_price | rn | rnk | drnk
---------------------------+------------+----+-----+------
Côte de Blaye | 263.50 | 1 | 1 | 1
Ipoh Coffee | 46.00 | 2 | 2 | 2
Chang | 19.00 | 3 | 3 | 3
Lakkalikööri | 18.00 | 4 | 4 | 4
Steeleye Stout | 18.00 | 5 | 4 | 4
Chartreuse verte | 18.00 | 6 | 4 | 4
Chai | 18.00 | 7 | 4 | 4
Outback Lager | 15.00 | 8 | 8 | 5
Laughing Lumberjack Lager | 14.00 | 9 | 9 | 6
Sasquatch Ale | 14.00 | 10 | 9 | 6
Rhönbräu Klosterbier | 7.75 | 11 | 11 | 7
Guaraná Fantástica | 4.50 | 12 | 12 | 8
Lee la fila de Outback Lager, que es donde se separan las tres:
rn = 8— es la octava fila, sin más.rnk = 8— es el octavo mejor precio, porque los cuatro empatados a 18,00 consumieron las posiciones 4, 5, 6 y 7.rank()salta de 4 a 8.drnk = 5— es el quinto precio distinto de la lista: 263,50 / 46,00 / 19,00 / 18,00 / 15,00.
La forma corta de recordarlo:
row_number()cuenta filas.rank()cuenta cuántos van por delante, más uno.dense_rank()cuenta valores distintos.
De ahí sale una identidad útil para comprobar consultas: el dense_rank() máximo es siempre el número de valores distintos. Aquí 8, y count(DISTINCT unit_price) sobre Beverages da 8. Si no cuadra, el ORDER BY de la ventana no es el que crees.
3. Cuál usar
row_number() — cuando necesitas un identificador único por fila. Paginación, "el más reciente de cada cliente", numerar un informe. Si hay empates, alguien tiene que ser el primero.
rank() — cuando publicas una clasificación. Es lo que hace un ranking deportivo: dos segundos y ningún tercero. Si dos comerciales venden lo mismo, no hay motivo para decidir a cara o cruz cuál es mejor.
dense_rank() — cuando cuentas niveles, no puestos. "Los 3 precios más altos" es dense_rank() <= 3. Con rank() te llevarías menos productos de los que crees si hay empates arriba.
ntile(n) — cuando quieres n grupos del mismo tamaño, no del mismo valor. Cuartiles, deciles, segmentación ABC.
3.1 El top N por grupo, otra vez
En lateral resolvimos los 3 productos más vendidos de cada categoría con CROSS JOIN LATERAL. Con ventanas es el otro camino:
WITH t AS (
SELECT c.category_name, p.product_name,
sum(od.unit_price*od.quantity*(1-od.discount)) AS f,
row_number() OVER (PARTITION BY c.category_id
ORDER BY sum(od.unit_price*od.quantity*(1-od.discount)) DESC) AS pos
FROM order_details od
JOIN products p ON p.product_id = od.product_id
JOIN categories c ON c.category_id = p.category_id
GROUP BY c.category_id, c.category_name, p.product_id, p.product_name
)
SELECT category_name, pos, product_name, round(f, 2) AS facturacion
FROM t WHERE pos <= 3 ORDER BY category_name, pos;
category_name | pos | product_name | facturacion
----------------+-----+----------------------------------+-------------
Beverages | 1 | Côte de Blaye | 141396.74
Beverages | 2 | Ipoh Coffee | 23526.70
Beverages | 3 | Chang | 16355.96
Condiments | 1 | Vegie-spread | 16701.10
...
Seafood | 3 | Boston Crab Meat | 17910.63
Las mismas 24 filas que dio la LATERAL. Dos herramientas, un resultado. Cuándo usar cada una:
row_number() + filtro |
CROSS JOIN LATERAL |
|
|---|---|---|
| Pasadas sobre los datos | una | una por grupo |
El filtro pos <= 3 |
necesita un CTE fuera | va dentro, como LIMIT |
| Grupos con lógica distinta | no | sí |
| Con muchos grupos | suele ganar | suele perder |
| Con pocos grupos y un índice | pierde | suele ganar |
La regla honesta: para un top N por grupo con la misma lógica en todos, row_number(). Para cuando cada grupo necesita su propia consulta, LATERAL. Aquí las dos valen; con 8 categorías la diferencia es irrelevante.
Y el <= 3 va fuera del CTE porque una ventana no puede ir en el WHERE. Es la regla del tema 32, y es lo que obliga al CTE.
4. ntile: repartir en grupos iguales
WITH t AS (
SELECT o.customer_id, sum(od.unit_price*od.quantity*(1-od.discount)) AS total
FROM orders o JOIN order_details od ON od.order_id = o.order_id
GROUP BY 1
),
q AS (SELECT customer_id, total, ntile(4) OVER (ORDER BY total DESC) AS cuartil FROM t)
SELECT cuartil, count(*) AS clientes, round(sum(total), 2) AS facturacion,
round(100.0 * sum(total) / sum(sum(total)) OVER (), 1) AS pct,
round(min(total), 2) AS min_cliente, round(max(total), 2) AS max_cliente
FROM q GROUP BY cuartil ORDER BY cuartil;
cuartil | clientes | facturacion | pct | min_cliente | max_cliente
---------+----------+-------------+------+-------------+-------------
1 | 23 | 852661.76 | 67.4 | 16817.10 | 110277.31
2 | 22 | 266726.91 | 21.1 | 7048.24 | 16476.57
3 | 22 | 108152.81 | 8.5 | 3239.80 | 7023.98
4 | 22 | 38251.56 | 3.0 | 100.80 | 3172.16
23 clientes de 89 —el primer cuartil— son el 67,4% de la facturación. El último cuartil, con prácticamente los mismos clientes, aporta el 3,0%. Un cliente del primer cuartil vale, de media, 21 veces uno del cuarto — 37 072,25 contra 1 738,71.
Los grupos no salen exactamente iguales porque 89 no es divisible entre 4. ntile reparte 23/22/22/22: el resto va a los primeros grupos, nunca a los últimos. Con ntile(6) sobre los mismos 89 salen cinco grupos de 15 y uno de 14.
4.1 El gotcha de ntile: parte los empates
WITH q AS (SELECT product_name, unit_price, ntile(5) OVER (ORDER BY unit_price) AS quintil FROM products)
SELECT product_name, unit_price, quintil FROM q WHERE unit_price = 18.00 ORDER BY quintil;
product_name | unit_price | quintil
------------------+------------+---------
Chartreuse verte | 18.00 | 2
Steeleye Stout | 18.00 | 2
Chai | 18.00 | 3
Lakkalikööri | 18.00 | 3
Cuatro productos al mismo precio exacto, repartidos entre dos quintiles distintos. Y no es un caso aislado: en los 77 productos hay tres precios partidos entre dos grupos — 12,50 entre el 1 y el 2, 18,00 entre el 2 y el 3, y 38,00 entre el 4 y el 5.
ntile garantiza tamaños iguales, y para lograrlo tiene que cortar por donde le toque, aunque sea en medio de un empate. Cuál de los cuatro cae en el quintil 2 es arbitrario.
Esto importa cuando el grupo tiene consecuencias. Si el quintil decide un descuento, una tarifa o a qué comercial se asigna un cliente, tienes dos casos idénticos tratados distinto y no sabrás explicar por qué. Cuando el corte tiene que respetar el valor, ntile no es la herramienta: lo son los percentiles de distribucion-y-percentiles, o un CASE con umbrales fijos.
5. El problema de los empates en row_number()
row_number() tiene que decidir un orden entre filas empatadas. Si no le dices cómo, decide él, y no promete ser consistente.
Los cuatro productos a 18,00, rankeados dentro de Beverages:
Lakkalikööri | 4
Steeleye Stout | 5
Chartreuse verte | 6
Chai | 7
Los mismos cuatro, con la misma cláusula OVER (ORDER BY unit_price DESC), pero rankeados sobre los 77 productos:
Chai | 44
Steeleye Stout | 45
Chartreuse verte | 46
Lakkalikööri | 47
El orden se ha invertido. Lakkalikööri era el mejor de los cuatro y ahora es el peor; Chai era el peor y ahora es el mejor. Misma tabla, mismos datos, misma especificación de ventana — solo cambió el alcance de la consulta.
No es un fallo: el estándar no define qué pasa con los empates, así que el motor usa el orden que le resulte más barato, y ese orden depende del plan. Un cambio de índice, un VACUUM o una lectura en paralelo pueden alterarlo mañana.
La solución es una línea:
row_number() OVER (ORDER BY unit_price DESC, product_id)
Regla sin excepción: todo row_number() lleva un desempate único al final del ORDER BY. Normalmente la clave primaria. Cuesta cuatro palabras y convierte un resultado arbitrario en uno reproducible.
Es la misma lección de ordenar-y-limitar —un ORDER BY ambiguo con LIMIT da resultados distintos— y de el-marco-rows-vs-range. Los empates son el enemigo recurrente de este bloque.
6. Errores comunes
-
row_number()sin desempate. La sección 5. El error más caro del tema porque parece funcionar. -
Usar
rank()para paginar. Con empates,rank() BETWEEN 11 AND 20puede devolver 7 filas o 15. Para paginar,row_number(). -
Esperar que
rank()llegue hasta el número de filas. Sobre Beverages elrank()máximo es 12 y eldense_rank()máximo es 8. Si el informe dice "hay 12 niveles de precio", está mal: hay 8. -
Poner la función de ranking en el
WHERE.ERROR: window functions are not allowed in WHERESiempre CTE o subconsulta, y el filtro fuera. -
Creer que
ntile(4)corta por percentiles. No: corta por número de filas. El corte del primer cuartil cae donde caiga el cliente número 23, valga lo que valga. -
Olvidar
PARTITION BYen un top N por grupo. Sin él,row_number()numera del 1 al 77 de corrido ypos <= 3devuelve 3 filas en total, no 3 por categoría. Es el mismo error que elLIMITfuera de laLATERAL.
7. Diferencias con SQL Server
| PostgreSQL | SQL Server | |
|---|---|---|
| Las cuatro funciones | idénticas | idénticas |
ORDER BY obligatorio dentro de OVER |
sí | sí |
TOP n WITH TIES |
no existe | sí |
Equivalente de WITH TIES |
rank() <= n en un CTE |
TOP n WITH TIES |
Portan al pie de la letra. La única diferencia práctica es WITH TIES: en SQL Server SELECT TOP 3 WITH TIES ... ORDER BY precio DESC devuelve más de 3 filas si hay empate en la tercera posición. En PostgreSQL se escribe con rank() <= 3 filtrado fuera de un CTE, que es más largo pero también más explícito.
8. Ejercicios
- Ranking de empleados por facturación y por número de pedidos, en la misma tabla. ¿Coinciden?
- Los 2 pedidos más caros de cada cliente, con
row_number(). Compruébalo conQUICK,ERNSHyCENTC. - ¿Cuántos precios distintos hay entre los 77 productos, y cuántos productos están involucrados en algún empate?
- Clasifica los 77 productos en 5 quintiles de precio y muestra el rango de cada uno.
- Demuestra que el
dense_rank()máximo es igual acount(DISTINCT ...). ¿Por qué tiene que serlo?
9. Pregunta de negocio
Comercial tiene tiempo para atender bien a un cuarto de la cartera. Quiere saber a cuáles, y cuánto negocio hay en juego en cada tramo.
"Un cuarto de la cartera" es una instrucción sobre el número de clientes, no sobre el dinero. Eso decide la función.