¿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:
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;En el sandbox regresó 7 filas, no 6. Carla salió dos veces:
| cliente | pedido_id | fecha | monto |
|---|---|---|---|
| Carla | 1059 | 2025-03-31 | 330 |
| Carla | 1060 | 2025-03-31 | 2050 |
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.
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;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;SELECT DISTINCT ON (cliente) cliente, pedido_id, fecha, monto
FROM pedidos
ORDER BY cliente, fecha DESC, pedido_id DESC;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
| Columna | Tipo |
|---|---|
| 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_id | cliente | fecha | monto |
|---|---|---|---|
| 1001 | Fer | 2025-01-03 | 850 |
| 1002 | Diego | 2025-01-04 | 1360 |
| 1003 | Beto | 2025-01-11 | 600 |
Cada ejecución empieza con copias nuevas de las tablas del curso, así que puedes probar lo que quieras. Tab agrega sangría; para salir del editor, presiona Escape y luego Tab.
Listo para ejecutar tu consulta.
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.