Cálculo de la suma acumulativa en PostgreSQL

85

Quiero encontrar la cantidad acumulada o en ejecución de campo e insertarla desde la etapa de preparación a la tabla. Mi estructura de puesta en escena es algo como esto:

ea_month    id       amount    ea_year    circle_id
April       92570    1000      2014        1
April       92571    3000      2014        2
April       92572    2000      2014        3
March       92573    3000      2014        1
March       92574    2500      2014        2
March       92575    3750      2014        3
February    92576    2000      2014        1
February    92577    2500      2014        2
February    92578    1450      2014        3          

Quiero que mi tabla de destino se vea así:

ea_month    id       amount    ea_year    circle_id    cum_amt
February    92576    1000      2014        1           1000 
March       92573    3000      2014        1           4000
April       92570    2000      2014        1           6000
February    92577    3000      2014        2           3000
March       92574    2500      2014        2           5500
April       92571    3750      2014        2           9250
February    92578    2000      2014        3           2000
March       92575    2500      2014        3           4500
April       92572    1450      2014        3           5950

Realmente estoy muy confundido con la forma de lograr este resultado. Quiero lograr este resultado usando PostgreSQL.

¿Alguien puede sugerir cómo lograr este conjunto de resultados?

Yousuf Sultan
fuente
1
¿Cómo se obtiene el cum_amount de 1000 en su tabla de destino? Para circle_id, la cantidad parece ser 2000.

Respuestas:

130

Básicamente, necesitas una función de ventana . Esa es una característica estándar hoy en día. Además de las funciones de ventana genuinas, puede usar cualquier función agregada como función de ventana en Postgres agregando una OVERcláusula.

La dificultad especial aquí es conseguir particiones y ordenar correctamente:

SELECT ea_month, id, amount, ea_year, circle_id
     , sum(amount) OVER (PARTITION BY circle_id
                         ORDER BY ea_year, ea_month) AS cum_amt
FROM   tbl
ORDER  BY circle_id, month;

Y hay GROUP BY .

La suma de cada fila se calcula desde la primera fila de la partición hasta la fila actual, o citando el manual para ser precisos:

La opción de encuadre predeterminada es RANGE UNBOUNDED PRECEDING, que es la misma que RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Con ORDER BY, esto establece el marco para que sean todas las filas desde el inicio de la partición hasta el último ORDER BYpar de la fila actual .

... que es la suma acumulada o corriente que busca. El énfasis audaz es mío.

Las filas con lo mismo (circle_id, ea_year, ea_month)son "pares" en esta consulta. Todos ellos muestran la misma suma acumulada con todos los pares agregados a la suma. Pero supongo que su tabla está UNIQUEactivada (circle_id, ea_year, ea_month), entonces el orden de clasificación es determinista y ninguna fila tiene pares.

Ahora, ORDER BY ... ea_month no funcionará con cadenas para nombres de meses. . Postgres ordenaría alfabéticamente según la configuración regional.

Si tiene datevalores reales almacenados en su tabla, puede ordenarlos correctamente. Si no es así, sugiero reemplazar ea_yeary ea_monthcon una sola columna monde tipo dateen su tabla.

  • Transforma lo que tienes con to_date():

      to_date(ea_year || ea_month , 'YYYYMonth') AS mon
    
  • Para la visualización, puede obtener cadenas originales con to_char():

      to_char(mon, 'Month') AS ea_month
      to_char(mon, 'YYYY') AS ea_year
    

Mientras se queda con el desafortunado diseño, esto funcionará:

SELECT ea_month, id, amount, ea_year, circle_id
     , sum(amount) OVER (PARTITION BY circle_id ORDER BY mon) AS cum_amt
FROM   (SELECT *, to_date(ea_year || ea_month, 'YYYYMonth') AS mon FROM tbl)
ORDER  BY circle_id, mon;
Erwin Brandstetter
fuente
Gracias por la solución. ¿Puedes ayudarme con una cosa más? Quiero implementar lo mismo usando un cursor con la lógica de que cada círculo tendrá solo un registro durante un mes de un año. Y se supone que la función se ejecuta una vez al mes. ¿Cómo puedo conseguir esto?
Yousuf Sultan
4
@YousufSultan: La mayoría de las veces hay una mejor solución que un cursor. Definitivamente es algo para una nueva pregunta. Inicie una nueva pregunta.
Erwin Brandstetter
Encuentro esta respuesta incompleta sin al menos una nota de que hay un "encuadre" que está sucediendo aquí, que por defecto range unbounded precedinges lo mismo que range between unbounded preceding and current row. Es por eso que sum()cuando se usa como una función de ventana produce un total acumulado, mientras que otras funciones de ventana no tienen este marco predeterminado.
Colin 't Hart
1
@ Colin'tHart: Agregué más arriba para aclarar.
Erwin Brandstetter
Aquí hay un enlace a una pregunta similar con una consulta más simple ( PARTITIONno siempre se necesita para crear un total acumulado
Jason Axelson