Skip to content
Empieza gratis

¿Cómo me quedo con el último registro de cada grupo en SQL?

Quédate con una fila por cliente, la más reciente, con QUALIFY o con WITH, y entiende por qué WHERE no puede.

Curso gratis. Tu Maestro, una IA, .

Es una de las preguntas más comunes con datos: de cada cliente, ¿cuál fue su pedido más reciente? No solo la fecha: el pedido completo, con su número y su monto. Con MAX(fecha) y GROUP BY tienes la fecha, pero no el resto de la fila.

La receta tiene dos pasos: numera los pedidos de cada cliente del más reciente al más antiguo y quédate con el número 1. El segundo paso es donde casi todos se tropiezan la primera vez. Ábrelo paso por paso e intenta cada uno antes de verlo.

El último pedido de cada cliente

Ejemplo resuelto · 4 pasos

La tabla pedidos tiene 60 pedidos de seis clientes. Queremos una fila por cliente: su pedido más reciente, con pedido_id, fecha y monto. Carla tiene dos pedidos el 31 de marzo de 2025; nos quedamos con el de pedido_id mayor.

Piensa el siguiente paso por tu cuenta y luego ábrelo para comparar.

Las dos formas regresan las mismas seis filas: lo comprobamos en el sandbox de Maestro. Antes de elegir una, vale la pena ver por qué el camino que casi todos prueban primero falla.

El camino con MAX y por qué se rompe

Sin ventanas, la receta clásica es sacar la última fecha de cada cliente con GROUP BY y unirla de regreso a la tabla:

SQL
WITH ultima AS (
  SELECT cliente, MAX(fecha) AS fecha
  FROM pedidos
  GROUP BY cliente
)
SELECT p.cliente, p.pedido_id, p.fecha, p.monto
FROM pedidos AS p
JOIN ultima AS u
  ON p.cliente = u.cliente AND p.fecha = u.fecha
ORDER BY p.cliente, p.pedido_id;
La receta sin ventanas: la fecha más reciente de cada cliente, unida de regreso a sus pedidos.

En el sandbox regresó 7 filas, no 6. Carla salió dos veces:

clientepedido_idfechamonto
Carla10592025-03-31330
Carla10602025-03-312050

Las dos filas tienen la fecha máxima de Carla, así que las dos pasan la unión. No es un error de la consulta: es que "el último pedido" no está bien definido cuando dos pedidos comparten la fecha. Con ROW_NUMBER() y pedido_id DESC en el ORDER BY, tú decides cuál gana y siempre sale una fila por cliente.

Las formas que sí funcionan, y en qué motor

Las tres formas de abajo regresaron las mismas seis filas en el sandbox (DuckDB 1.5.6). QUALIFY también aceptó el alias de la ventana: con ROW_NUMBER() OVER (...) AS n en el SELECT, QUALIFY n = 1 dio el mismo resultado.

SQL
SELECT cliente, pedido_id, fecha, monto
FROM pedidos
QUALIFY ROW_NUMBER() OVER (
  PARTITION BY cliente
  ORDER BY fecha DESC, pedido_id DESC
) = 1
ORDER BY cliente;
Con QUALIFY: filtra después de calcular la ventana, en una sola consulta.
SQL
SELECT cliente, pedido_id, fecha, monto
FROM (
  SELECT cliente, pedido_id, fecha, monto,
         ROW_NUMBER() OVER (
           PARTITION BY cliente
           ORDER BY fecha DESC, pedido_id DESC
         ) AS n
  FROM pedidos
) AS numerados
WHERE n = 1
ORDER BY cliente;
Sin QUALIFY: numera adentro y filtra afuera. Es la misma idea que la forma con WITH del ejemplo, escrita como subconsulta.
SQL
SELECT DISTINCT ON (cliente) cliente, pedido_id, fecha, monto
FROM pedidos
ORDER BY cliente, fecha DESC, pedido_id DESC;
Con DISTINCT ON: se queda con la primera fila de cada cliente según el ORDER BY. No es una ventana, pero resuelve el mismo problema donde existe.

Qué forma usar depende del motor de tu trabajo. Elige el tuyo; lo que dice cada pestaña viene de la documentación oficial del motor, enlazada abajo.

Corren las tres formas: QUALIFY, la subconsulta (o WITH) y DISTINCT ON. Las tres regresaron las mismas seis filas en el sandbox de Maestro. La documentación de DuckDB dice que QUALIFY filtra los resultados de las funciones de ventana y evita la subconsulta o el WITH.

Ojo con los NULL: DuckDB los pone al final también con DESC, así que una fecha vacía no gana el número 1.

La documentación de SELECT de PostgreSQL 18 no incluye QUALIFY entre sus cláusulas. Su tutorial de funciones de ventana dice que no se permiten en WHERE, y que para filtrar después de calcularlas uses una subconsulta: la forma de subconsulta o la de WITH.

PostgreSQL documenta DISTINCT ON, y advierte que la primera fila de cada grupo es impredecible sin un ORDER BY que la decida.

Ojo con los NULL: con DESC, PostgreSQL los pone primero. Si hay fechas vacías, escribe ORDER BY fecha DESC NULLS LAST.

La sintaxis de consultas de BigQuery incluye una cláusula QUALIFY, que se escribe después de HAVING, y su orden de evaluación la pone después de las ventanas. La forma con QUALIFY es la más corta.

Si tu consulta con QUALIFY te da un error que no entiendes, la forma de subconsulta no depende de QUALIFY y hace lo mismo.

La documentación de Snowflake describe QUALIFY como el filtro de los resultados de las funciones de ventana, y dice que se evalúa después de calcularlas. Su ejemplo filtra por el alias de la ventana, como QUALIFY row_num = 1.

La forma de subconsulta también funciona, si tu equipo la prefiere por ser portable.

Ahora tú, con una vuelta más

El ejemplo ya te dio el último pedido de cada cliente. El ejercicio te pide eso y algo que el ejemplo no hace: en la misma fila, cuánto gastó ese cliente en todo el trimestre. Puedes usar QUALIFY, WITH o una subconsulta: todas se marcan igual si regresan las mismas filas. Antes de correrla, contesta las dos preguntas de abajo. Si tu total sale igual al monto del último pedido, pregúntale a tu Maestro por qué. Si tu motor del trabajo no acepta ninguna de estas formas, pregúntale cómo adaptarla.

Comprueba lo que sabes: Antes de correr: si en lugar de ROW_NUMBER usas RANK() OVER (PARTITION BY cliente ORDER BY fecha DESC), sin pedido_id, y te quedas con el 1, ¿cuántas filas regresa?

Comprueba lo que sabes: Antes de correr: si en la misma consulta calculas SUM(monto) OVER (PARTITION BY cliente) y filtras con QUALIFY, ¿qué número sale en total_cliente en la fila de Carla?

Regresa una fila por cliente con su pedido más reciente (cliente, pedidoid, fecha, monto) y, en la misma fila, totalcliente: cuánto gastó ese cliente en todos sus pedidos del trimestre. Si hay dos pedidos el mismo día, quédate con el de pedidoid mayor. Ordena por cliente. Termina

Tu turno

Regresa una fila por cliente con su pedido más reciente (cliente, pedido_id, fecha, monto) y, en la misma fila, total_cliente: cuánto gastó ese cliente en todos sus pedidos del trimestre. Si hay dos pedidos el mismo día, quédate con el de pedido_id mayor. Ordena por cliente. Terminaste cuando ves 6 filas y la de Carla dice 1060 y 11910.

Tablas: pedidos (pedido_id, cliente, fecha, monto)

pedidos · 60 filas

ColumnaTipo
pedido_id Número del pedido; crece con el orden en que se registró.BIGINT
cliente Nombre del cliente.VARCHAR
fecha Día del pedido, sin hora.DATE
monto Monto del pedido, en pesos.BIGINT

Primeras filas

pedido_idclientefechamonto
1001Fer2025-01-03850
1002Diego2025-01-041360
1003Beto2025-01-11600

Cada ejecución empieza con copias nuevas de las tablas del curso, así que puedes probar lo que quieras.

Listo para ejecutar tu consulta.

Ejecútalo tú

Comprueba lo que sabes: Escribiste WHERE ROW_NUMBER() OVER (PARTITION BY cliente ORDER BY fecha DESC) = 1 y DuckDB responde WHERE clause cannot contain window functions. ¿Qué está pasando?

Antes de usarlo en el trabajo

Dónde pones la suma importa. Si calculas total_cliente en la consulta de afuera de un WITH, después del WHERE n = 1, la ventana solo ve la fila que quedó: en una corrida en el sandbox, Carla salió con 2050 en lugar de 11910. Todo lo que necesite las filas completas se calcula antes de filtrar.

Los primeros N, no solo el último. Cambia = 1 por <= 3 y tienes los tres pedidos más recientes de cada cliente. La misma consulta, con otro número.

Una fecha vacía puede ganar. En un motor que pone los NULL primero con DESC, como PostgreSQL, un pedido sin fecha quedaría como "el más reciente". En DuckDB no pasa, porque los pone al final. Escribe NULLS LAST y la consulta dice lo mismo en los dos.

En una entrevista viene disfrazado: "Devuelve la transacción más reciente de cada usuario", "deduplica esta tabla quedándote con el registro más nuevo" o "el último estatus de cada pedido". Es el mismo problema las tres veces. Lo que se evalúa es que no lo intentes en el WHERE, que pienses en el empate, y que sepas decir por qué MAX con GROUP BY no alcanza cuando quieres la fila completa. Si en tu trabajo tienes una tabla de historial y quieres el estado más reciente de cada cosa, cuéntale a tu Maestro sus columnas.

Una revisión que vale la pena: busca una consulta tuya que use MAX(fecha) y luego una unión para recuperar la fila. Córrela y cuenta las filas por grupo: si algún grupo sale dos veces, ya sabes por qué, y ya sabes cómo cambiarla.