Saltar al contenido

lección 4

Viajando en el tiempo: LAG, LEAD y comparaciones

¿Cuánto creció vs el mes anterior? Variaciones, detección de cambios y análisis temporal con funciones de navegación.

55 min

Hay una pregunta que aparece en CADA reunión de dirección, en CADA dashboard, en CADA informe trimestral: "ícómo estamos comparados con el periodo anterior?". Las ventas de enero no significan nada en aislamiento — lo que importa es si subieron o bajaron respecto a diciembre. El beneficio de Q3 solo cobra sentido comparado con Q3 del año pasado.

LAG y LEAD son las window functions que resuelven esto de forma elegante. LAG mira hacia atrás ("dame el valor de la fila anterior"), LEAD mira hacia adelante ("dame el valor de la fila siguiente"). Son como una máquina del tiempo dentro de tu query: puedes acceder al pasado y al futuro de cada fila sin necesidad de self-joins ni subconsultas correlacionadas.

### LAG: mirar hacia atrás

La analogía: estás leyendo tu extracto bancario. Para cada movimiento, quieres saber "ícuánto tenía ayer?". LAG es esa columna extra que dice "el valor anterior era...". No necesitas otra tabla, no necesitas un self-join — simplemente miras la fila de arriba.

1-- LAG(columna, offset, valor_default) OVER (ORDER BY ...)
2-- offset: cuantas filas atras mirar (default: 1)
3-- valor_default: que devolver si no hay fila anterior (default: NULL)
4
5SELECT
6 fecha,
7 importe,
8 LAG(importe) OVER (ORDER BY fecha) AS importe_anterior,
9 importe - LAG(importe) OVER (ORDER BY fecha) AS variacion,
10 ROUND(
11 100.0 * (importe - LAG(importe) OVER (ORDER BY fecha))
12 / LAG(importe) OVER (ORDER BY fecha), 1
13 ) AS variacion_pct
14FROM ventas_mensuales;

LAG(importe) = "el importe de la fila anterior según el ORDER BY"

### LEAD: mirar hacia adelante

LEAD es el espejo de LAG. En vez de mirar la fila anterior, mira la siguiente. útil para calcular "tiempo hasta el próximo evento", "diferencia con el siguiente periodo", o "días hasta la próxima compra del cliente".

1-- Para cada pedido: dias hasta el siguiente pedido del mismo cliente
2SELECT
3 cliente_id,
4 fecha,
5 importe,
6 LEAD(fecha) OVER (PARTITION BY cliente_id ORDER BY fecha) AS proxima_compra,
7 DATEDIFF('day', fecha,
8 LEAD(fecha) OVER (PARTITION BY cliente_id ORDER BY fecha)
9 ) AS dias_hasta_proxima
10FROM pedidos
11WHERE estado = 'completado';

LEAD + PARTITION BY cliente_id = "la próxima compra de ESTE cliente"

### LAG con particiones: comparar dentro de cada grupo

El poder real de LAG aparece cuando lo combinas con PARTITION BY. No quieres comparar "la venta anterior" en general — quieres comparar "la venta anterior DE ESTE CLIENTE" o "el mes anterior DE ESTA REGIÓN". La partición define el grupo dentro del cual se hace la navegación.

1-- Variacion mensual por ciudad
2WITH ventas_mes AS (
3 SELECT
4 c.ciudad,
5 DATE_TRUNC('month', p.fecha) AS mes,
6 SUM(p.importe)::INTEGER AS total
7 FROM clientes c
8 JOIN pedidos p ON p.cliente_id = c.id
9 GROUP BY c.ciudad, DATE_TRUNC('month', p.fecha)
10)
11SELECT
12 ciudad,
13 mes,
14 total,
15 LAG(total) OVER (PARTITION BY ciudad ORDER BY mes) AS mes_anterior,
16 total - LAG(total) OVER (PARTITION BY ciudad ORDER BY mes) AS variacion,
17 CASE
18 WHEN LAG(total) OVER (PARTITION BY ciudad ORDER BY mes) IS NULL THEN 'primer mes'
19 WHEN total > LAG(total) OVER (PARTITION BY ciudad ORDER BY mes) THEN 'crecimiento'
20 WHEN total < LAG(total) OVER (PARTITION BY ciudad ORDER BY mes) THEN 'caida'
21 ELSE 'estable'
22 END AS tendencia
23FROM ventas_mes
24ORDER BY ciudad, mes;

LAG particionado por ciudad: cada ciudad se compara consigo misma, no con las demás.

LAG mira al pasado, LEAD al futuro. NULL cuando no hay fila anterior/siguiente.

Consejo de senior: el segundo parámetro de LAG/LEAD es el offset. LAG(importe, 12) te da "el valor de hace 12 filas" — perfecto para comparar mismo mes del año anterior si tus datos están ordenados por mes. Evita self-joins innecesarios.

Esto me pasó en mi primer trabajo: usé LAG(ventas, 12) para comparar con el mismo mes del año anterior, y los números no cuadraban. ¿El problema? Había un mes sin ventas que no aparecía en la tabla. El offset 12 se saltaba ese hueco y me comparaba junio con abril del año pasado. La solución: cruza primero con un calendario denso (generate_series de meses) y rellena huecos con COALESCE(..., 0). Así LAG siempre mira exactamente 12 meses atrás.

### Detección de cambios: cuándo algo cambió

Otro uso brillante de LAG: detectar cuándo un valor cambió. "¿En qué momento este cliente cambió de plan?", "ícuándo subió el precio de este producto?", "ícuántos cambios de estado tuvo este pedido?". Comparas el valor actual con el anterior y si son distintos, hubo un cambio.

1-- Detectar cambios de estado en pedidos
2WITH cambios AS (
3 SELECT
4 pedido_id,
5 estado,
6 fecha_cambio,
7 LAG(estado) OVER (PARTITION BY pedido_id ORDER BY fecha_cambio) AS estado_anterior
8 FROM pedido_historico
9)
10SELECT *
11FROM cambios
12WHERE estado != estado_anterior -- Solo filas donde hubo un cambio
13 OR estado_anterior IS NULL; -- O es el primer registro

LAG para detección de cambios: si el valor actual difiere del anterior, algo pasó.

Cuando LAG devuelve NULL (primera fila de la partición), cualquier operación aritmética con NULL da NULL. Usa COALESCE para manejar este caso: COALESCE(LAG(importe) OVER (...), 0) si quieres tratar "sin valor anterior" como 0.

### FIRST_VALUE y LAST_VALUE: los extremos de la ventana

Dos funciones más de la familia de navegación: FIRST_VALUE te da el primer valor de la partición (el más antiguo, el más barato, el primero según el ORDER BY). LAST_VALUE te da el último. Son útiles para calcular "diferencia con el máximo de mi grupo" o "porcentaje respecto al líder".

1-- Comparar cada vendedor con el mejor y peor de su equipo
2SELECT
3 equipo,
4 nombre,
5 ventas,
6 FIRST_VALUE(nombre) OVER (
7 PARTITION BY equipo ORDER BY ventas DESC
8 ) AS lider_equipo,
9 FIRST_VALUE(ventas) OVER (
10 PARTITION BY equipo ORDER BY ventas DESC
11 ) AS ventas_lider,
12 ventas - FIRST_VALUE(ventas) OVER (
13 PARTITION BY equipo ORDER BY ventas DESC
14 ) AS gap_vs_lider
15FROM vendedores;

FIRST_VALUE con ORDER BY DESC te da el máximo. Con ASC, el mínimo.

En la práctica uso FIRST_VALUE mucho más que LAST_VALUE. Si necesitas el último, suele ser más claro usar FIRST_VALUE con ORDER BY invertido. LAST_VALUE tiene un comportamiento sorprendente con frames que veremos en la lección 5.

Por qué LAST_VALUE engaña: si escribes LAST_VALUE(ventas) OVER (ORDER BY ventas DESC) esperando obtener el mínimo del grupo, no lo obtienes — te devuelve el valor de la FILA ACTUAL. Esto ocurre porque el frame por defecto con ORDER BY es RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, es decir, "desde el inicio hasta aquí". El "último" de ese rango es la propia fila actual, no el final de la partición. Para que funcione como esperas: LAST_VALUE(ventas) OVER (ORDER BY ventas DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING). Pero es más claro invertir el ORDER BY y usar FIRST_VALUE.

En el ejercicio de caída de ventas usas dos CTEs solo para filtrar por la variación. En DuckDB, QUALIFY variacion < 0 haría lo mismo sin el envoltorio (ver lección 2). La CTE sigue siendo el camino portable — pero si solo trabajas en DuckDB, QUALIFY simplifica.

## ejercicios

[01]

Variación de ventas mes a mes

El CFO necesita un informe de variación mensual: para cada mes, el total de ventas, el total del mes anterior, la variación absoluta y la variación porcentual. Solo pedidos completados.

Cargando editor...
[02]

Días entre compras de cada cliente

El equipo de CRM quiere saber cuántos días pasan entre compras de cada cliente. Para cada pedido completado: cliente_id, fecha, fecha_siguiente_compra, dias_entre_compras. Usa LEAD.

Cargando editor...
[03]

Comparación con el mismo mes del año anterior

El board quiere ver cada mes comparado con el mismo mes del año anterior (Year over Year). Usa LAG con offset 12 (asumiendo datos mensuales consecutivos). Muestra: mes, total, total_yoy, crecimiento_pct.

Cargando editor...
[04]

Detectar meses con caída de ventas

Alerta temprana: filtra solo los meses donde las ventas CAYERON respecto al mes anterior. Muestra: mes, total, total_anterior, caida_pct. Ordena por caída más grave primero.

Cargando editor...

Regístrate para guardar tu progreso.

## comentarios

Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.

Inicia sesión para comentar y responder.

cargando comentarios...