SQL analítico · ventanas

Top N por grupo en SQL con ROW_NUMBER()

Numera las filas dentro de cada grupo y conserva solo las primeras N para resolver patrones como “los dos pedidos más recientes de cada cliente”.

Lectura: 8–10 minSQL comprobadoPráctica con pistas
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.

Patrón Top N por grupo
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

Base educativa
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;
Muestra del resultado comprobado en SQLite 3.46.1
cliente_idpedido_idfechatotalrn
11112025-06-01249,001
11032025-05-1024,902
21142025-06-08203,901
21022025-05-0449,502
31162025-06-12999,001
31072025-05-2036,902

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.

Desempate estable
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

Qué ocurre con empates
FunciónComportamientoCuá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

Intermedia · SQL analítico

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.

Qué debes recordar

  • PARTITION BY define el grupo y ORDER BY de 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.

Conceptos relacionados

Fuentes técnicas consultadas