Contenido de esta guía
Respuesta rápida
Para obtener las N mejores filas de cada grupo, numera las filas dentro de cada grupo con ROW_NUMBER() y filtra esa numeración en una consulta exterior. El patrón separa dos tareas: ordenar dentro de cada grupo y después conservar solo las primeras N posiciones.
WITH ranked AS (
SELECT
columna_grupo,
columnas_resultado,
ROW_NUMBER() OVER (
PARTITION BY columna_grupo
ORDER BY criterio DESC
) AS rn
FROM tabla
)
SELECT *
FROM ranked
WHERE rn <= 2;Si N vale 1, obtienes una fila principal por grupo. Si N vale 3, conservas las tres primeras según el criterio definido.
Modelo mental: reiniciar el contador para cada grupo
PARTITION BY divide el conjunto en grupos lógicos sin colapsar las filas como haría GROUP BY. Dentro de cada partición, ROW_NUMBER() asigna 1, 2, 3… siguiendo el ORDER BY de la ventana.
Dos órdenes distintos: el ORDER BY dentro de OVER(...) decide quién obtiene rn = 1, rn = 2, etc. El ORDER BY exterior decide cómo se presenta finalmente el resultado.
Ejemplo comprobado: los dos pedidos más recientes de cada cliente
WITH ranked AS (
SELECT
cliente_id,
id AS pedido_id,
fecha,
total,
ROW_NUMBER() OVER (
PARTITION BY cliente_id
ORDER BY fecha DESC, id DESC
) AS rn
FROM pedidos
)
SELECT cliente_id, pedido_id, fecha, total, rn
FROM ranked
WHERE rn <= 2
ORDER BY cliente_id, rn;| cliente_id | pedido_id | fecha | total | rn |
|---|---|---|---|---|
| 1 | 111 | 2025-06-01 | 249,00 | 1 |
| 1 | 103 | 2025-05-10 | 24,90 | 2 |
| 2 | 114 | 2025-06-08 | 203,90 | 1 |
| 2 | 102 | 2025-05-04 | 49,50 | 2 |
| 3 | 116 | 2025-06-12 | 999,00 | 1 |
| 3 | 107 | 2025-05-20 | 36,90 | 2 |
La CTE calcula primero una posición independiente para cada cliente. La consulta exterior conserva rn <= 2. Los clientes con un único pedido aparecen una sola vez: el patrón no inventa una segunda fila.
Define un desempate determinista
Si dos pedidos comparten la misma fecha y solo ordenas por fecha DESC, su orden relativo puede quedar sin definir. PostgreSQL documenta que ROW_NUMBER() asigna números en el orden de la ventana y que las filas empatadas pueden numerarse en un orden no especificado. Por eso el ejemplo añade id DESC como segundo criterio.
ROW_NUMBER() OVER (
PARTITION BY cliente_id
ORDER BY fecha DESC, id DESC
)Elige un segundo criterio que tenga sentido para tus datos. No añadas una columna arbitraria si puede cambiar la interpretación de “mejor” o “más reciente”.
Por qué el filtro va fuera
La posición rn se calcula como resultado de una función de ventana. No puedes tratar ese alias como si ya existiera en el WHERE del mismo nivel de consulta en los motores habituales. La CTE o subconsulta crea primero la columna calculada; el nivel exterior puede filtrarla después.
Si este punto te genera errores, revisa la guía función de ventana en WHERE.
ROW_NUMBER, RANK o DENSE_RANK
| Función | Comportamiento | Cuándo encaja |
|---|---|---|
ROW_NUMBER() | Da un número distinto a cada fila. | Quieres exactamente hasta N filas por grupo y defines un desempate. |
RANK() | Comparte posición entre empates y deja huecos. | Quieres conservar empates como la misma posición competitiva. |
DENSE_RANK() | Comparte posición sin dejar huecos. | Quieres N niveles de valor, aunque eso produzca más de N filas. |
Para un “Top 2 filas exactas por cliente”, ROW_NUMBER() suele expresar mejor la intención. Para “todos los productos empatados entre los dos precios más altos”, puede interesarte RANK o DENSE_RANK.
Compatibilidad
El patrón se apoya en funciones de ventana y CTE/subconsultas. PostgreSQL, SQLite y SQL Server documentan ROW_NUMBER() con partición y orden. En cualquier motor, confirma la versión concreta si trabajas con una instalación antigua y evita asumir que el orden final coincide con el orden de la ventana: son etapas distintas.
Errores frecuentes
Olvidar PARTITION BY
Sin partición, numeras todo el resultado como un único grupo y obtienes un Top N global, no uno por cliente, categoría o entidad.
Filtrar rn en el mismo SELECT
Calcula primero la ventana en una CTE o subconsulta y filtra en el nivel exterior.
No definir desempate
Si el criterio principal puede repetirse y necesitas resultados reproducibles, añade un segundo criterio coherente.
Práctica guiada
Producto más caro de cada categoría
La tabla productos(id, categoria, nombre, precio) contiene varios productos por categoría. Escribe una consulta que conserve solo el producto más caro de cada categoría. Si hay empate de precio, gana el id más alto.
Criterio de éxito: cada categoría reinicia la numeración y la consulta exterior conserva únicamente rn = 1.
PARTITION BY categoria.precio DESC y después id DESC.rn = 1 fuera de la CTE.Solución razonada
WITH ranked AS (
SELECT
id,
categoria,
nombre,
precio,
ROW_NUMBER() OVER (
PARTITION BY categoria
ORDER BY precio DESC, id DESC
) AS rn
FROM productos
)
SELECT id, categoria, nombre, precio
FROM ranked
WHERE rn = 1
ORDER BY categoria;La ventana conserva todas las filas pero asigna una posición dentro de cada categoría. El filtro exterior selecciona solo la primera, con un desempate explícito por id.
Qué debes recordar
PARTITION BYdefine el grupo yORDER BYde la ventana define la posición dentro de él.ROW_NUMBER()permite conservar exactamente las primeras N filas de cada grupo.- Filtra la numeración en una consulta exterior.
- Añade un desempate si el criterio principal puede repetirse.
- El orden de la ventana y el orden final de presentación son independientes.