Primer y último registro en MySQL: alternativas a FIRST() y LAST()

Si buscas FIRST() y LAST() en MySQL, la respuesta corta es que no existen como funciones de agregación (sí en Microsoft Access). Lo que quieres hacer se resuelve de otras formas, según qué entiendas por «primero» y «último».

Usaré una tabla de pedidos:

pedidos (id, cliente_id, fecha, total)

1. El primer y el último de toda la tabla: ORDER BY + LIMIT

-- El más antiguo
SELECT * FROM pedidos ORDER BY fecha ASC, id ASC LIMIT 1;

-- El más reciente
SELECT * FROM pedidos ORDER BY fecha DESC, id DESC LIMIT 1;

Añade siempre un segundo criterio (id) para desempatar: sin él, dos filas con la misma fecha pueden salir en el orden que decida el motor.

2. Solo el valor: MIN y MAX

SELECT MIN(fecha) AS primero, MAX(fecha) AS ultimo FROM pedidos;

Si lo que necesitas es el valor de una columna (la fecha del primer y último pedido), esto es lo más rápido y claro. Ojo! no mezcles: SELECT MAX(fecha), total no te da el total del último pedido, sino el de una fila cualquiera (o un error, si tienes activado ONLY_FULL_GROUP_BY).

3. El primero y el último de cada grupo

El caso que más se busca: el último pedido de cada cliente. Con funciones de ventana (MySQL 8 o MariaDB 10.2 en adelante):

SELECT *
FROM (
 SELECT p.*,
 ROW_NUMBER() OVER (PARTITION BY cliente_id ORDER BY fecha DESC, id DESC) AS n
 FROM pedidos p
) t
WHERE n = 1;

Cambiando DESC por ASC tienes el primero de cada cliente. En versiones más antiguas, sin funciones de ventana, se usa una subconsulta:

SELECT p.*
FROM pedidos p
JOIN (SELECT cliente_id, MAX(fecha) AS fecha FROM pedidos GROUP BY cliente_id) u
 ON u.cliente_id = p.cliente_id AND u.fecha = p.fecha;

Ojo: si un cliente tiene dos pedidos en la misma fecha, esta versión devuelve los dos.

4. FIRST_VALUE() y LAST_VALUE()

MySQL 8 y MariaDB sí traen estas funciones, pero son de ventana, no de agregación:

SELECT DISTINCT cliente_id,
 FIRST_VALUE(total) OVER w AS primer_total,
 LAST_VALUE(total) OVER w AS ultimo_total
FROM pedidos
WINDOW w AS (PARTITION BY cliente_id ORDER BY fecha, id
 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING);

La cláusula ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING es la que más se olvida. Sin ella, LAST_VALUE devuelve el valor de la fila actual en vez del último del grupo.

5. Mi truco favorito GROUP_CONCAT

Antes de las funciones de ventana, la forma de sacar el primer y el último valor dentro de un GROUP BY, junto a otras agregaciones, era concatenar ordenado y cortar:

SELECT cliente_id,
 SUBSTRING_INDEX(GROUP_CONCAT(total ORDER BY fecha, id), ',', 1) AS primer_total,
 SUBSTRING_INDEX(GROUP_CONCAT(total ORDER BY fecha DESC, id DESC), ',', 1) AS ultimo_total
FROM pedidos
GROUP BY cliente_id;

Sigue funcionando, pero devuelve texto y depende de group_concat_max_len: si el grupo es largo, la cadena se corta sin avisar. Lo cuento con más detalle en Cómo utilizar la función LAST() y FIRST() en MySQL, el post donde lo usé por primera vez.

Velocidad: los índices

Todas estas consultas ordenan, y ordenar una tabla grande sin índice es leerla entera. Para el último de toda la tabla basta un índice por (fecha, id); para el último de cada cliente, uno por (cliente_id, fecha, id). Con eso, ORDER BY … LIMIT 1 lee una sola fila del índice. Compruébalo con EXPLAIN: si aparece Using filesort en una tabla con millones de filas, falta el índice.

Cuál usar

  • Un valor suelto: MIN / MAX.
  • Una fila entera de toda la tabla: ORDER BY … LIMIT 1.
  • Una fila por grupo: ROW_NUMBER() en MySQL 8 o MariaDB; subconsulta con MAX en versiones antiguas.
  • Primer y último valor junto a otras agregaciones: FIRST_VALUE / LAST_VALUE, o GROUP_CONCAT si no tienes funciones de ventana.

Y si buscas condicionales dentro de la consulta: IF en consultas MySQL.

0 comentarios sobre "Primer y último registro en MySQL: alternativas a FIRST() y LAST()"

Sé el primero en comentar.