Mantén SQLtest vivo: Ayuda a cubrir los costos del servidor de este mes de $100 para mantener todas las lecciones y el playground de SQL en línea. Apoya el proyecto →
Código SQL copiado al portapapeles

Lección 12.7 · Tiempo de lectura: ~6 min

Un total acumulado (suma acumulativa) suma cada valor a la suma de todos los anteriores: ingresos hasta la fecha, saldo de una cuenta tras cada movimiento, registros acumulados. Es básico en los dashboards y un ejercicio popular en las entrevistas de SQL. En esta lección lo calcularás con SUM() OVER (ORDER BY ...), lo reiniciarás para cada grupo, evitarás la trampa de los valores de orden repetidos y verás cómo se hacía antes de las funciones de ventana.

Total acumulado en SQL

Un SUM normal con GROUP BY reduce las filas a un solo total. Un total acumulado conserva cada fila y agrega una columna con la suma hasta ese momento. Eso es justo lo que hace una función de ventana: SUM(amount) OVER (ORDER BY day) suma todas las filas desde la primera hasta la actual, en el orden de day. Las funciones de ventana y los marcos se vieron en las lecciones 7.1 y 7.3; aquí resuelven un ejercicio práctico.

Los ejemplos se ejecutan en la base de datos de ejemplo Sakila (MySQL 8.0).

Ingresos acumulados por día

Primero agrega los pagos por día; después agrega el total acumulado sobre las filas diarias:

WITH daily AS (
    SELECT DATE(payment_date) AS pay_day,
           SUM(amount) AS daily_amount
    FROM payment
    WHERE payment_date >= '2005-08-01'
      AND payment_date < '2005-09-01'
    GROUP BY DATE(payment_date)
)
SELECT pay_day,
       daily_amount,
       SUM(daily_amount) OVER (ORDER BY pay_day) AS running_total
FROM daily
ORDER BY pay_day;

Resultado: 10 días con pagos en agosto de 2005; el total acumulado crece de 2817.29 a 24072.13, los ingresos de todo el mes.

pay_daydaily_amountrunning_total
2005-08-012817.292817.29
2005-08-022726.575543.86
2005-08-16111.775655.63
2005-08-172457.078112.70
.........
2005-08-232523.0124072.13

Agregar primero es importante: SUM(...) OVER sobre los pagos individuales devolvería una fila por pago, no por día. Los días sin pagos simplemente faltan; para mostrarlos con 0, une los datos a un calendario (lección 6.6).


Reiniciar el total para cada grupo

PARTITION BY inicia un total acumulado distinto para cada grupo. Aquí los ingresos mensuales reciben dos columnas acumuladas: una desde el principio y otra que se reinicia cada año:

WITH monthly AS (
    SELECT YEAR(payment_date) AS pay_year,
           MONTH(payment_date) AS pay_month,
           SUM(amount) AS monthly_amount
    FROM payment
    GROUP BY YEAR(payment_date), MONTH(payment_date)
)
SELECT pay_year, pay_month, monthly_amount,
       SUM(monthly_amount) OVER (ORDER BY pay_year, pay_month) AS running_total,
       SUM(monthly_amount) OVER (PARTITION BY pay_year ORDER BY pay_month) AS year_to_date
FROM monthly
ORDER BY pay_year, pay_month;

Resultado: 5 meses; el total acumulado general llega a 67416.51, mientras que el acumulado del año se reinicia en 2006 con 514.18.

pay_yearpay_monthmonthly_amountrunning_totalyear_to_date
200554824.434824.434824.43
200569631.8814456.3114456.31
2005728373.8942830.2042830.20
2005824072.1366902.3366902.33
20062514.1867416.51514.18

El mismo patrón da un total acumulado por cliente (PARTITION BY customer_id ORDER BY payment_date) o por tienda.


La trampa: valores de orden iguales

Cuando el ORDER BY de la ventana tiene repetidos, el resultado sorprende a mucha gente. Dos pagos de Sakila tienen exactamente la misma hora:

SELECT payment_id, payment_date, amount,
       SUM(amount) OVER (ORDER BY payment_date) AS default_frame,
       SUM(amount) OVER (ORDER BY payment_date, payment_id
                         ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS rows_frame
FROM payment
WHERE payment_date >= '2005-05-30 14:40:00'
  AND payment_date < '2005-05-30 15:00:00'
ORDER BY payment_date, payment_id;

Resultado: los pagos 1258 y 8245 tienen ambos default_frame = 5.98, mientras que rows_frame muestra 2.99 y luego 5.98.

payment_idpayment_dateamountdefault_framerows_frame
12582005-05-30 14:47:312.995.982.99
82452005-05-30 14:47:312.995.985.98
99182005-05-30 14:49:340.996.976.97

Con ORDER BY y sin un marco explícito, el marco predeterminado es RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW: incluye todas las filas con el mismo valor de orden que la actual. Las filas empatadas reciben el mismo total. Para sumar las filas estrictamente una a una, usa un marco ROWS y un criterio de desempate único en ORDER BY, como la clave primaria.


Sin funciones de ventana

Antes de las funciones de ventana (MySQL antes de 8.0), un total acumulado era una subconsulta correlacionada: para cada fila, sumar todas las filas hasta ella.

WITH daily AS (
    SELECT DATE(payment_date) AS pay_day,
           SUM(amount) AS daily_amount
    FROM payment
    WHERE payment_date >= '2005-08-01'
      AND payment_date < '2005-09-01'
    GROUP BY DATE(payment_date)
)
SELECT d1.pay_day,
       d1.daily_amount,
       (SELECT SUM(d2.daily_amount)
        FROM daily d2
        WHERE d2.pay_day <= d1.pay_day) AS running_total
FROM daily d1
ORDER BY d1.pay_day;

Resultado: las mismas 10 filas y totales que la versión con función de ventana.

Para cada fila, la subconsulta recorre todas las filas anteriores, así que el trabajo crece de forma cuadrática. Menciónala como alternativa de respaldo y usa SUM() OVER en la práctica.

Práctica


Conclusiones clave:

  • SUM(x) OVER (ORDER BY key) devuelve la suma acumulada hasta cada fila y conserva todas las filas.
  • Primero agrega al nivel necesario (día, mes) y luego agrega el total acumulado.
  • PARTITION BY reinicia el total para cada grupo: cliente, tienda, año.
  • El marco predeterminado es RANGE, así que las filas con valores de orden iguales comparten un total; usa ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW y un criterio de desempate único.
  • Sin funciones de ventana, usa una subconsulta correlacionada, pero es mucho más lenta.

Preguntas frecuentes

¿Cómo calculo un total acumulado en SQL?

Usa una función de ventana: SELECT day, amount, SUM(amount) OVER (ORDER BY day) AS running_total FROM daily;. Funciona en MySQL 8.0+, MariaDB 10.2+, PostgreSQL, SQL Server, Oracle y SQLite 3.25+.

¿Cómo calculo un total acumulado por grupo?

Agrega PARTITION BY: SUM(amount) OVER (PARTITION BY customer_id ORDER BY payment_date). La suma se reinicia en la primera fila de cada cliente.

¿Por qué algunas filas tienen el mismo total acumulado?

Porque tienen el mismo valor en la columna del ORDER BY de la ventana, y el marco RANGE predeterminado incluye todas esas filas empatadas. Indica ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW y agrega una columna única, como el id, al ORDER BY.

¿Cómo calculo una media móvil en lugar de un total acumulado?

Limita el marco a un número fijo de filas: AVG(amount) OVER (ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) promedia el día actual y los seis anteriores. Los marcos de ventana se ven en la lección 7.3.