LAG
LAG permite acceder al valor de una fila anterior dentro de la misma partición, sin necesidad de hacer un JOIN de la tabla consigo misma. Es la función de ventana por excelencia para análisis temporales: calcular variaciones mes a mes, detectar tendencias de crecimiento o caída, y comparar cada registro con su predecesor.
Antes de que existieran las funciones de ventana, obtener el valor de la fila anterior requería un LEFT JOIN complejo con subconsultas correlacionadas. LAG resuelve esto en una sola línea, haciendo que el código sea más legible y el rendimiento significativamente mejor.
Sintaxis
LAG(expresion, offset, valor_defecto) OVER (
[PARTITION BY columna1, columna2, ...]
ORDER BY columna1 [ASC|DESC], columna2 [ASC|DESC], ...
)La función acepta tres argumentos. El primero, expresion, es la columna o expresión cuyo valor anterior quieres obtener. El segundo, offset, es opcional y define cuántas filas hacia atrás mirar (por defecto es 1). El tercero, valor_defecto, también opcional, es el valor que se devuelve cuando no existe fila anterior (por defecto es NULL). El ORDER BY es obligatorio porque sin un orden definido, "anterior" no tiene significado.
Comportamiento básico
En su forma más simple, LAG devuelve el valor de la columna indicada en la fila inmediatamente anterior:
SELECT
mes,
ingresos,
LAG(ingresos) OVER (ORDER BY mes) AS ingresos_mes_anterior
FROM resumen_mensual
WHERE anio = 2024
ORDER BY mes;| mes | ingresos | ingresos_mes_anterior |
|---|---|---|
| 1 | 57000.00 | NULL |
| 2 | 51750.00 | 57000.00 |
| 3 | 65600.00 | 51750.00 |
| 4 | 72250.00 | 65600.00 |
| 5 | 67660.00 | 72250.00 |
| 6 | 79200.00 | 67660.00 |
Enero no tiene mes anterior, así que LAG devuelve NULL. Febrero muestra 57000, que son los ingresos de enero. Marzo muestra 51750, los de febrero. La ventana se desliza fila a fila, siempre mirando una posición hacia atrás.
Usar un valor por defecto
Para evitar el NULL en la primera fila, puedes especificar un valor por defecto como tercer argumento:
SELECT
mes,
ingresos,
LAG(ingresos, 1, 0) OVER (ORDER BY mes) AS ingresos_anterior
FROM resumen_mensual
WHERE anio = 2024
ORDER BY mes
LIMIT 3;| mes | ingresos | ingresos_anterior |
|---|---|---|
| 1 | 57000.00 | 0.00 |
| 2 | 51750.00 | 57000.00 |
| 3 | 65600.00 | 51750.00 |
Ahora enero muestra 0 en lugar de NULL, lo cual es útil si necesitas hacer cálculos con ese valor sin preocuparte por los NULL.
Caso práctico: variación mes a mes
El uso más habitual de LAG es calcular el cambio porcentual entre periodos consecutivos. Este análisis es la base de cualquier informe financiero:
SELECT
mes,
ingresos,
LAG(ingresos) OVER (ORDER BY mes) AS mes_anterior,
ROUND(
(ingresos - LAG(ingresos) OVER (ORDER BY mes))
/ LAG(ingresos) OVER (ORDER BY mes) * 100,
2
) AS variacion_pct
FROM resumen_mensual
WHERE anio = 2024
ORDER BY mes;| mes | ingresos | mes_anterior | variacion_pct |
|---|---|---|---|
| 1 | 57000.00 | NULL | NULL |
| 2 | 51750.00 | 57000.00 | -9.21 |
| 3 | 65600.00 | 51750.00 | 26.76 |
| 4 | 72250.00 | 65600.00 | 10.14 |
| 5 | 67660.00 | 72250.00 | -6.35 |
| 6 | 79200.00 | 67660.00 | 17.06 |
Febrero cayó un 9.21% respecto a enero. Marzo se recuperó con un crecimiento del 26.76%. Mayo volvió a caer un 6.35% para luego subir un 17% en junio. Este tipo de análisis sería mucho más complejo sin LAG, requiriendo un JOIN de la tabla consigo misma igualando cada mes con su predecesor.
Caso práctico: detectar tendencias de crecimiento
Combinando LAG con CASE puedes clasificar automáticamente cada periodo como crecimiento, caída o estable:
SELECT
departamento,
mes,
total_ventas,
LAG(total_ventas) OVER (
PARTITION BY departamento
ORDER BY mes
) AS ventas_anterior,
CASE
WHEN LAG(total_ventas) OVER (PARTITION BY departamento ORDER BY mes) IS NULL
THEN 'Sin referencia'
WHEN total_ventas > LAG(total_ventas) OVER (PARTITION BY departamento ORDER BY mes)
THEN 'Crecimiento'
WHEN total_ventas < LAG(total_ventas) OVER (PARTITION BY departamento ORDER BY mes)
THEN 'Caída'
ELSE 'Estable'
END AS tendencia
FROM ventas_mensuales
WHERE anio = 2024
ORDER BY departamento, mes;| departamento | mes | total_ventas | ventas_anterior | tendencia |
|---|---|---|---|---|
| Marketing | 1 | 42000.00 | NULL | Sin referencia |
| Marketing | 2 | 38500.00 | 42000.00 | Caída |
| Marketing | 3 | 45200.00 | 38500.00 | Crecimiento |
| Marketing | 4 | 48900.00 | 45200.00 | Crecimiento |
| Ventas | 1 | 85000.00 | NULL | Sin referencia |
| Ventas | 2 | 91200.00 | 85000.00 | Crecimiento |
| Ventas | 3 | 91200.00 | 91200.00 | Estable |
| Ventas | 4 | 98400.00 | 91200.00 | Crecimiento |
El PARTITION BY departamento hace que LAG se reinicie para cada departamento. Las ventas de enero en Marketing no se comparan con las de Ventas del mes anterior, sino que cada departamento tiene su propia secuencia independiente.
Caso práctico: offset mayor que 1
El segundo argumento de LAG permite mirar más de una fila hacia atrás. Esto es útil para comparaciones interanuales donde quieres comparar con el mismo mes del año anterior:
SELECT
anio,
trimestre,
ingresos,
LAG(ingresos, 4) OVER (ORDER BY anio, trimestre) AS mismo_trim_anio_anterior,
ROUND(
(ingresos - LAG(ingresos, 4) OVER (ORDER BY anio, trimestre))
/ LAG(ingresos, 4) OVER (ORDER BY anio, trimestre) * 100,
2
) AS crecimiento_interanual
FROM resumen_trimestral
ORDER BY anio, trimestre;| anio | trimestre | ingresos | mismo_trim_anio_anterior | crecimiento_interanual |
|---|---|---|---|---|
| 2023 | 1 | 145000.00 | NULL | NULL |
| 2023 | 2 | 162000.00 | NULL | NULL |
| 2023 | 3 | 158000.00 | NULL | NULL |
| 2023 | 4 | 198000.00 | NULL | NULL |
| 2024 | 1 | 168000.00 | 145000.00 | 15.86 |
| 2024 | 2 | 185000.00 | 162000.00 | 14.20 |
| 2024 | 3 | 179000.00 | 158000.00 | 13.29 |
| 2024 | 4 | 224000.00 | 198000.00 | 13.13 |
Con LAG(ingresos, 4), cada trimestre de 2024 se compara con el mismo trimestre de 2023 (4 trimestres antes). Los primeros 4 registros no tienen referencia y devuelven NULL. Este patrón es esencial en informes financieros donde la estacionalidad hace que la comparación mes a mes sea engañosa.
Manejo de NULL
Cuando la columna de la expresión contiene valores NULL, LAG los devuelve tal cual. No los salta ni busca el siguiente valor no nulo:
SELECT
mes,
ingresos,
LAG(ingresos) OVER (ORDER BY mes) AS anterior
FROM resumen_mensual;| mes | ingresos | anterior |
|---|---|---|
| 1 | 57000.00 | NULL |
| 2 | NULL | 57000.00 |
| 3 | 65600.00 | NULL |
Si febrero tiene ingresos NULL, marzo obtendrá NULL como valor anterior, porque LAG devuelve exactamente lo que hay en la fila anterior, sin importar si es NULL o no. Para manejar estos casos, envuelve LAG en COALESCE:
SELECT
mes,
ingresos,
COALESCE(
LAG(ingresos) OVER (ORDER BY mes),
0
) AS anterior_seguro
FROM resumen_mensual;Combinación con otras funciones
LAG se combina naturalmente con funciones matemáticas y de formato para crear dashboards analíticos completos:
SELECT
mes,
ingresos,
LAG(ingresos) OVER (ORDER BY mes) AS anterior,
ingresos - LAG(ingresos) OVER (ORDER BY mes) AS diferencia_absoluta,
CONCAT(
CASE
WHEN ingresos >= LAG(ingresos) OVER (ORDER BY mes) THEN '+'
ELSE ''
END,
ROUND(
(ingresos - LAG(ingresos) OVER (ORDER BY mes))
/ LAG(ingresos) OVER (ORDER BY mes) * 100,
1
),
'%'
) AS variacion
FROM resumen_mensual
WHERE anio = 2024
ORDER BY mes;| mes | ingresos | anterior | diferencia_absoluta | variacion |
|---|---|---|---|---|
| 1 | 57000.00 | NULL | NULL | NULL |
| 2 | 51750.00 | 57000.00 | -5250.00 | -9.2% |
| 3 | 65600.00 | 51750.00 | 13850.00 | +26.8% |
| 4 | 72250.00 | 65600.00 | 6650.00 | +10.1% |
| 5 | 67660.00 | 72250.00 | -4590.00 | -6.4% |
| 6 | 79200.00 | 67660.00 | 11540.00 | +17.1% |
El CONCAT con el CASE añade el signo + a los crecimientos positivos para mejorar la legibilidad. La diferencia absoluta muestra el monto exacto del cambio, y la variación lo expresa en porcentaje.
Practica con LAG
Usa el editor para acceder a filas anteriores:
En el siguiente artículo veremos LEAD para acceder a filas siguientes.
Escrito por Eduardo Lázaro
