Sample Header Ad - 728x90

Cumulative sum by day with non-negative floor with PostgreSQL

2 votes
1 answer
1279 views
Using PostgreSQL 12, I need to calculate the cumulative sum by day while filling in missing days. I *also* need to set a minimum value of 0 if any cumulative sum goes negative. ### Background There are two excellent answers for [cumulative sum with missing days](https://stackoverflow.com/questions/32490946/cumulative-sum-of-values-by-month-filling-in-for-missing-months?noredirect=1&lq=1) and [non-negative floor values](https://dba.stackexchange.com/questions/179367/set-non-negative-floor-for-rolling-sum-in-postgresql) . I need to combine these two solutions. Take this table and data as an example (note, this is also contained in [this db fiddle](https://dbfiddle.uk/?rdbms=postgres_12&fiddle=e84c62c5b4e95ebdd4c027b5f241a21f))
CREATE TABLE inventory_test (
    "id" serial,
    "account_id" bigint NOT NULL,
    "is_active" boolean NOT NULL DEFAULT 'true',
    "object_timestamp" bigint NOT NULL,
    "amount" numeric(12, 1) NOT NULL,
    "object_id" bigint NOT NULL,
    PRIMARY KEY ("id")
);
*Note that object_timestamp is epoch time in milliseconds
SELECT id, to_timestamp((object_timestamp + 0 )/1000), amount
FROM inventory_test
ORDER BY object_timestamp;
| id | to_timestamp | amount | |----|--------------|--------| | 1| 2022-04-08 15:13:30+01| 10000.0 | | 2| 2022-04-09 15:13:30+01| -2000.0 | | 3| 2022-04-09 15:13:31+01| -1000.0 | | 4| 2022-04-10 15:13:30+01| -2000.0 | | 5| 2022-04-11 15:13:30+01| -3000.0 | | 6| 2022-04-12 15:13:30+01| -2500.0 | | 7| 2022-04-15 15:13:30+01| -2500.0 | | 8| 2022-04-16 15:13:30+01| -3000.0 | | 9| 2022-04-17 15:13:30+01| 1000.0 | I can generate the daily totals accounting for missing days with:
WITH cte as (
    SELECT 
        date_trunc('day', to_timestamp((object_timestamp)/1000)) as day,
        sum(amount) as day_sum
    FROM inventory_test
    WHERE object_id = 1 AND is_active = true
    GROUP by 1
    )
SELECT 
    day, 
    sum(c.day_sum) OVER (ORDER BY day) AS running_sum
FROM 
    (SELECT min(day) AS min_day FROM cte) init,
    generate_series(init.min_day, now(), interval '1 day') day
LEFT   JOIN cte c USING (day)
ORDER  BY day;
|day | running_sum | |-----------|---------------| | 2022-04-08 00:00:00+00| 10000 | | 2022-04-09 00:00:00+00| 7000 | | 2022-04-10 00:00:00+00| 5000 | | 2022-04-11 00:00:00+00| 2000 | | 2022-04-12 00:00:00+00| -500 | | 2022-04-13 00:00:00+00| -500 | | 2022-04-14 00:00:00+00| -500 | | 2022-04-15 00:00:00+00| -3000 | | 2022-04-16 00:00:00+00| -6000 | | 2022-04-17 00:00:00+00| -5000 | ### DB fiddle You can see this in [this DB fiddle](https://dbfiddle.uk/?rdbms=postgres_12&fiddle=e84c62c5b4e95ebdd4c027b5f241a21f) ### Desired results What I _want_ the results to look like are non-negative, where any positive changes are applied to the adjusted running total which should be MAX(0, running_sum). I'd prefer to solve this in a SQL statement rather than a custom function. Desired output: |day | running_sum | |-----------|---------------| | 2022-04-08 00:00:00+00| 10000 | | 2022-04-09 00:00:00+00| 7000 | | 2022-04-10 00:00:00+00| 5000 | | 2022-04-11 00:00:00+00| 2000 | | 2022-04-12 00:00:00+00| 0 | | 2022-04-13 00:00:00+00| 0| | 2022-04-14 00:00:00+00| 0 | | 2022-04-15 00:00:00+00| 0 | | 2022-04-16 00:00:00+00| 0 | | 2022-04-17 00:00:00+00| 1000 |
Asked by brianz (121 rep)
Apr 18, 2022, 03:34 AM
Last activity: Apr 21, 2022, 02:35 PM