Aprende SQL · lección gratuita

Lección 33 · Totales acumulados y marcos de ventana (frames)

Resumen

Cuando una función de ventana lleva ORDER BY, se activa el concepto de marco (frame): el subconjunto de filas, relativo a la fila actual, sobre el que se calcula. Esto habilita totales acumulados (running totals), medias móviles y cualquier agregado "hasta aquí" o "en esta ventana deslizante". Controlar el frame con ROWS o RANGE es lo que separa un SUM OVER plano de un análisis temporal real.

Sintaxis / Conceptos

agg(col) OVER (
  [PARTITION BY ...]
  ORDER BY ...
  { ROWS | RANGE } BETWEEN <inicio> AND <fin>
)

Límites disponibles para <inicio>/<fin>:

LímiteSignificado
UNBOUNDED PRECEDINGdesde el principio de la partición
n PRECEDINGn filas/valores antes
CURRENT ROWla fila actual
n FOLLOWINGn filas/valores después
UNBOUNDED FOLLOWINGhasta el final de la partición

ROWS vs RANGE — la diferencia que importa. ROWS razona por posición física: "las 3 filas anteriores". RANGE razona por valor del ORDER BY: "todas las filas cuyo valor sea ≤ el actual". Si hay empates, RANGE los mete todos de golpe (mismo resultado para todas las filas con ese valor), mientras ROWS los va sumando una a una. Para series temporales con una fila por período (sin empates) ambos coinciden, pero con duplicados el resultado difiere. Regla práctica: para totales acumulados y medias móviles usa ROWS, que es predecible.

Cuidado con el default. Como el frame por defecto es RANGE, un SUM(x) OVER (ORDER BY x) sobre una columna con repetidos no da un acumulado fila-a-fila, sino que "salta" en cada valor distinto. Si quieres acumulado real, escribe ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW explícitamente.

SELECT fecha, SUM(1) OVER (ORDER BY fecha ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS pedidos_acumulados FROM pedidos LIMIT 6

Ejemplos

-- Total acumulado de revenue por fecha (running total).
-- ROWS UNBOUNDED PRECEDING ... CURRENT ROW = suma desde el inicio hasta hoy.
WITH rev_dia AS (
  SELECT p.fecha,
         ROUND(SUM(d.cantidad * d.precio_unitario * (1 - d.descuento)), 2) AS revenue
  FROM pedidos p
  JOIN detalle_pedidos d ON d.pedido_id = p.id
  GROUP BY p.fecha
)
SELECT fecha,
       revenue,
       ROUND(SUM(revenue) OVER (ORDER BY fecha
              ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW), 2) AS acumulado
FROM rev_dia
ORDER BY fecha;
-- Media móvil de 3 meses (mes actual + 2 anteriores).
-- Ventana deslizante: ROWS BETWEEN 2 PRECEDING AND CURRENT ROW.
WITH rev_mes AS (
  SELECT strftime('%Y-%m', p.fecha) AS mes,
         ROUND(SUM(d.cantidad * d.precio_unitario * (1 - d.descuento)), 2) AS revenue
  FROM pedidos p
  JOIN detalle_pedidos d ON d.pedido_id = p.id
  GROUP BY mes
)
SELECT mes,
       revenue,
       ROUND(AVG(revenue) OVER (ORDER BY mes
              ROWS BETWEEN 2 PRECEDING AND CURRENT ROW), 2) AS media_movil_3m
FROM rev_mes
ORDER BY mes;
-- ROWS vs RANGE con empates reales: la columna cantidad tiene muchos valores
-- repetidos. Para cada valor, RANGE cuenta TODAS las filas con esa cantidad
-- (mismo resultado en el grupo), ROWS cuenta fila a fila.
SELECT cantidad,
       COUNT(*) OVER (ORDER BY cantidad
              RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS conteo_range,
       COUNT(*) OVER (ORDER BY cantidad
              ROWS  BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS conteo_rows
FROM detalle_pedidos
ORDER BY cantidad
LIMIT 12;
💡 Si tu "total acumulado" se ve como escalones planos en vez de crecer fila a fila, casi seguro estás usando el frame RANGE por defecto sobre una columna con valores repetidos. Cambia a ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW.

Cheatsheet

FramePara qué
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWTotal acumulado (running total)
ROWS BETWEEN n PRECEDING AND CURRENT ROWMedia móvil / suma de n+1 filas
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWINGAcumulado hacia adelante
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWAcumula por valor; agrupa empates (default con ORDER BY)
ROWSCuenta filas físicas (predecible)
RANGEAgrupa por valor del ORDER BY (incluye todos los empates)

---

← Acceso entre filas: LAG y LEADDistribución: NTILE y percentiles →

Ver todas las lecciones de Aprende SQL →