LAST_VALUE
LAST_VALUE devuelve el valor de una expresión evaluada en la última fila de la ventana. Es la contraparte de FIRST_VALUE, pero con una trampa crucial que genera errores constantemente incluso entre desarrolladores experimentados: el marco de ventana por defecto no incluye todas las filas de la partición. Si no lo corriges, LAST_VALUE no devolverá lo que esperas.
Esta función es útil para comparar cada fila con el último valor de su grupo, obtener el registro más reciente, o calcular diferencias respecto al extremo opuesto de la partición. Pero antes de usarla, necesitas entender el problema del marco por defecto.
Sintaxis
LAST_VALUE(expresion) OVER (
[PARTITION BY columna1, columna2, ...]
ORDER BY columna1 [ASC|DESC], columna2 [ASC|DESC], ...
[especificación_de_marco]
)La función acepta un solo argumento: la expresión cuyo último valor quieres obtener. La especificación de marco es donde radica la diferencia entre un resultado correcto y uno incorrecto.
La trampa del marco por defecto
Este es el concepto más importante de todo el artículo. Cuando usas LAST_VALUE con ORDER BY y sin especificar un marco, MySQL aplica el marco por defecto:
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWEsto significa que para cada fila, la ventana solo incluye desde la primera fila de la partición hasta la fila actual. El "último valor" de ese rango es siempre la fila actual misma. Observa el resultado incorrecto:
-- INCORRECTO: LAST_VALUE devuelve el valor de la fila actual
SELECT
nombre,
departamento,
salario,
LAST_VALUE(nombre) OVER (
PARTITION BY departamento
ORDER BY salario DESC
) AS peor_pagado
FROM empleados;| nombre | departamento | salario | peor_pagado |
|---|---|---|---|
| Roberto Fernández | Marketing | 72000.00 | Roberto Fernández |
| Laura Díaz | Marketing | 58000.00 | Laura Díaz |
| Sofía Moreno | Marketing | 45000.00 | Sofía Moreno |
| Carlos López | Ventas | 68000.00 | Carlos López |
| Ana García | Ventas | 62000.00 | Ana García |
El resultado es inútil: cada fila se muestra a sí misma como "peor pagado" porque el marco solo llega hasta la fila actual. La solución es extender el marco hasta el final de la partición:
-- CORRECTO: especificar el marco completo
SELECT
nombre,
departamento,
salario,
LAST_VALUE(nombre) OVER (
PARTITION BY departamento
ORDER BY salario DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS peor_pagado
FROM empleados;| nombre | departamento | salario | peor_pagado |
|---|---|---|---|
| Roberto Fernández | Marketing | 72000.00 | Sofía Moreno |
| Laura Díaz | Marketing | 58000.00 | Sofía Moreno |
| Sofía Moreno | Marketing | 45000.00 | Sofía Moreno |
| Carlos López | Ventas | 68000.00 | Pedro Sánchez |
| Ana García | Ventas | 62000.00 | Pedro Sánchez |
| Marta Ruiz | Ventas | 55000.00 | Pedro Sánchez |
| Pedro Sánchez | Ventas | 48000.00 | Pedro Sánchez |
Ahora sí: ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING hace que la ventana abarque toda la partición, y el último valor es realmente el último de todo el grupo.
Comportamiento básico
Con el marco correcto, LAST_VALUE devuelve consistentemente el último valor de la partición para todas las filas:
SELECT
nombre,
departamento,
salario,
FIRST_VALUE(salario) OVER w AS salario_maximo,
LAST_VALUE(salario) OVER w AS salario_minimo,
salario - LAST_VALUE(salario) OVER w AS diferencia_vs_minimo
FROM empleados
WINDOW w AS (
PARTITION BY departamento
ORDER BY salario DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
);| nombre | departamento | salario | salario_maximo | salario_minimo | diferencia_vs_minimo |
|---|---|---|---|---|---|
| Roberto Fernández | Marketing | 72000.00 | 72000.00 | 45000.00 | 27000.00 |
| Laura Díaz | Marketing | 58000.00 | 72000.00 | 45000.00 | 13000.00 |
| Sofía Moreno | Marketing | 45000.00 | 72000.00 | 45000.00 | 0.00 |
| Carlos López | Ventas | 68000.00 | 68000.00 | 48000.00 | 20000.00 |
| Ana García | Ventas | 62000.00 | 68000.00 | 48000.00 | 14000.00 |
| Marta Ruiz | Ventas | 55000.00 | 68000.00 | 48000.00 | 7000.00 |
| Pedro Sánchez | Ventas | 48000.00 | 68000.00 | 48000.00 | 0.00 |
La ventana con nombre w permite usar FIRST_VALUE y LAST_VALUE con la misma definición de ventana, evitando repetir el marco. Roberto gana 27000 más que el peor pagado de Marketing, mientras que Carlos supera al peor pagado de Ventas por 20000.
Caso práctico: rango salarial por departamento
Combinando FIRST_VALUE y LAST_VALUE puedes calcular el rango completo de salarios de cada departamento y la posición relativa de cada empleado dentro de ese rango:
SELECT
nombre,
departamento,
salario,
FIRST_VALUE(salario) OVER w AS techo,
LAST_VALUE(salario) OVER w AS piso,
FIRST_VALUE(salario) OVER w - LAST_VALUE(salario) OVER w AS amplitud,
ROUND(
(salario - LAST_VALUE(salario) OVER w)
/ (FIRST_VALUE(salario) OVER w - LAST_VALUE(salario) OVER w) * 100,
1
) AS posicion_pct
FROM empleados
WINDOW w AS (
PARTITION BY departamento
ORDER BY salario DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
)
ORDER BY departamento, salario DESC;| nombre | departamento | salario | techo | piso | amplitud | posicion_pct |
|---|---|---|---|---|---|---|
| Roberto Fernández | Marketing | 72000.00 | 72000.00 | 45000.00 | 27000.00 | 100.0 |
| Laura Díaz | Marketing | 58000.00 | 72000.00 | 45000.00 | 27000.00 | 48.1 |
| Sofía Moreno | Marketing | 45000.00 | 72000.00 | 45000.00 | 27000.00 | 0.0 |
| Carlos López | Ventas | 68000.00 | 68000.00 | 48000.00 | 20000.00 | 100.0 |
| Ana García | Ventas | 62000.00 | 68000.00 | 48000.00 | 20000.00 | 70.0 |
La posición porcentual indica dónde se sitúa cada empleado dentro del rango de su departamento. Roberto está al 100% (techo) y Sofía al 0% (piso). Laura, con 58000, está al 48.1% del rango entre 45000 y 72000.
Caso práctico: comparar con el último pedido
En análisis de actividad de clientes, LAST_VALUE permite comparar cada pedido con el más reciente:
SELECT
c.nombre,
p.fecha_pedido,
p.total,
LAST_VALUE(p.total) OVER (
PARTITION BY p.id_cliente
ORDER BY p.fecha_pedido
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS ultimo_pedido_total,
LAST_VALUE(p.fecha_pedido) OVER (
PARTITION BY p.id_cliente
ORDER BY p.fecha_pedido
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS fecha_ultimo_pedido
FROM pedidos p
JOIN clientes c ON p.id_cliente = c.id_cliente
WHERE c.nombre = 'Ana García'
ORDER BY p.fecha_pedido;| nombre | fecha_pedido | total | ultimo_pedido_total | fecha_ultimo_pedido |
|---|---|---|---|---|
| Ana García | 2024-01-15 | 250.00 | 680.00 | 2024-07-18 |
| Ana García | 2024-02-22 | 380.00 | 680.00 | 2024-07-18 |
| Ana García | 2024-04-10 | 175.00 | 680.00 | 2024-07-18 |
| Ana García | 2024-05-03 | 520.00 | 680.00 | 2024-07-18 |
| Ana García | 2024-07-18 | 680.00 | 680.00 | 2024-07-18 |
Cada pedido de Ana muestra el importe y fecha de su pedido más reciente. El último pedido fue de 680 el 18 de julio. La primera compra de 250 en enero da una perspectiva de cómo ha ido creciendo el ticket medio.
Caso práctico: último valor del periodo para comparación
En informes financieros, comparar cada registro con el valor de cierre del periodo es un patrón habitual:
SELECT
fecha,
precio_cierre,
LAST_VALUE(precio_cierre) OVER (
PARTITION BY YEAR(fecha), MONTH(fecha)
ORDER BY fecha
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS cierre_fin_mes,
ROUND(
(precio_cierre / LAST_VALUE(precio_cierre) OVER (
PARTITION BY YEAR(fecha), MONTH(fecha)
ORDER BY fecha
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) - 1) * 100,
2
) AS vs_cierre_mes
FROM cotizaciones
WHERE ticker = 'AAPL' AND YEAR(fecha) = 2024 AND MONTH(fecha) = 3
ORDER BY fecha;| fecha | precio_cierre | cierre_fin_mes | vs_cierre_mes |
|---|---|---|---|
| 2024-03-01 | 178.50 | 171.20 | 4.27 |
| 2024-03-08 | 175.30 | 171.20 | 2.40 |
| 2024-03-15 | 172.80 | 171.20 | 0.93 |
| 2024-03-22 | 169.40 | 171.20 | -1.05 |
| 2024-03-29 | 171.20 | 171.20 | 0.00 |
Cada día muestra cuánto se desvía del precio de cierre del mes. El 1 de marzo estaba un 4.27% por encima de donde terminaría el mes.
Manejo de NULL
Si la última fila de la partición tiene NULL en la expresión, LAST_VALUE devuelve NULL. Al igual que con FIRST_VALUE, la función devuelve exactamente lo que hay en la posición correspondiente sin intentar buscar el último valor no nulo:
SELECT
nombre,
fecha_baja,
LAST_VALUE(fecha_baja) OVER (
ORDER BY fecha_contratacion
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS ultima_baja
FROM empleados;Si el último empleado contratado no tiene fecha de baja, LAST_VALUE(fecha_baja) devolverá NULL para todas las filas. Para obtener la última fecha de baja no nula, podrías usar MAX(fecha_baja) OVER () que ignora los NULL por naturaleza.
Combinación con otras funciones
LAST_VALUE junto con FIRST_VALUE y funciones de agregación permite crear informes completos de cada fila respecto a su grupo:
SELECT
nombre,
departamento,
salario,
FIRST_VALUE(nombre) OVER w AS mejor_pagado,
LAST_VALUE(nombre) OVER w AS peor_pagado,
ROUND(AVG(salario) OVER w, 2) AS promedio_depto,
CASE
WHEN salario = FIRST_VALUE(salario) OVER w THEN 'Techo'
WHEN salario = LAST_VALUE(salario) OVER w THEN 'Piso'
WHEN salario > AVG(salario) OVER w THEN 'Sobre promedio'
ELSE 'Bajo promedio'
END AS posicion
FROM empleados
WINDOW w AS (
PARTITION BY departamento
ORDER BY salario DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
)
ORDER BY departamento, salario DESC;| nombre | departamento | salario | mejor_pagado | peor_pagado | promedio_depto | posicion |
|---|---|---|---|---|---|---|
| Roberto Fernández | Marketing | 72000.00 | Roberto Fernández | Sofía Moreno | 58333.33 | Techo |
| Laura Díaz | Marketing | 58000.00 | Roberto Fernández | Sofía Moreno | 58333.33 | Bajo promedio |
| Sofía Moreno | Marketing | 45000.00 | Roberto Fernández | Sofía Moreno | 58333.33 | Piso |
| Carlos López | Ventas | 68000.00 | Carlos López | Pedro Sánchez | 58250.00 | Techo |
| Ana García | Ventas | 62000.00 | Carlos López | Pedro Sánchez | 58250.00 | Sobre promedio |
El CASE clasifica cada empleado según su posición relativa dentro del departamento, usando FIRST_VALUE, LAST_VALUE y AVG para establecer los límites y el promedio.
Practica con LAST_VALUE
Usa el editor para obtener el ultimo valor de cada particion:
En el siguiente artículo veremos NTH_VALUE para obtener el valor en una posición específica.
Escrito por Eduardo Lázaro
