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 ROW

Esto 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;
nombredepartamentosalariopeor_pagado
Roberto FernándezMarketing72000.00Roberto Fernández
Laura DíazMarketing58000.00Laura Díaz
Sofía MorenoMarketing45000.00Sofía Moreno
Carlos LópezVentas68000.00Carlos López
Ana GarcíaVentas62000.00Ana 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;
nombredepartamentosalariopeor_pagado
Roberto FernándezMarketing72000.00Sofía Moreno
Laura DíazMarketing58000.00Sofía Moreno
Sofía MorenoMarketing45000.00Sofía Moreno
Carlos LópezVentas68000.00Pedro Sánchez
Ana GarcíaVentas62000.00Pedro Sánchez
Marta RuizVentas55000.00Pedro Sánchez
Pedro SánchezVentas48000.00Pedro 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
);
nombredepartamentosalariosalario_maximosalario_minimodiferencia_vs_minimo
Roberto FernándezMarketing72000.0072000.0045000.0027000.00
Laura DíazMarketing58000.0072000.0045000.0013000.00
Sofía MorenoMarketing45000.0072000.0045000.000.00
Carlos LópezVentas68000.0068000.0048000.0020000.00
Ana GarcíaVentas62000.0068000.0048000.0014000.00
Marta RuizVentas55000.0068000.0048000.007000.00
Pedro SánchezVentas48000.0068000.0048000.000.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;
nombredepartamentosalariotechopisoamplitudposicion_pct
Roberto FernándezMarketing72000.0072000.0045000.0027000.00100.0
Laura DíazMarketing58000.0072000.0045000.0027000.0048.1
Sofía MorenoMarketing45000.0072000.0045000.0027000.000.0
Carlos LópezVentas68000.0068000.0048000.0020000.00100.0
Ana GarcíaVentas62000.0068000.0048000.0020000.0070.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;
nombrefecha_pedidototalultimo_pedido_totalfecha_ultimo_pedido
Ana García2024-01-15250.00680.002024-07-18
Ana García2024-02-22380.00680.002024-07-18
Ana García2024-04-10175.00680.002024-07-18
Ana García2024-05-03520.00680.002024-07-18
Ana García2024-07-18680.00680.002024-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;
fechaprecio_cierrecierre_fin_mesvs_cierre_mes
2024-03-01178.50171.204.27
2024-03-08175.30171.202.40
2024-03-15172.80171.200.93
2024-03-22169.40171.20-1.05
2024-03-29171.20171.200.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;
nombredepartamentosalariomejor_pagadopeor_pagadopromedio_deptoposicion
Roberto FernándezMarketing72000.00Roberto FernándezSofía Moreno58333.33Techo
Laura DíazMarketing58000.00Roberto FernándezSofía Moreno58333.33Bajo promedio
Sofía MorenoMarketing45000.00Roberto FernándezSofía Moreno58333.33Piso
Carlos LópezVentas68000.00Carlos LópezPedro Sánchez58250.00Techo
Ana GarcíaVentas62000.00Carlos LópezPedro Sánchez58250.00Sobre 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:

Simulador SQL
Ctrl+Enter para ejecutar

En el siguiente artículo veremos NTH_VALUE para obtener el valor en una posición específica.

Escrito por Eduardo Lázaro