sqlsql-serverresetcumulative-sum

Running total to retain previous row total instead of resetting to zero when a specific condition is met


I want to achieve the following calculation highlighted in the Desired Output column:

highlighted row has a running total of 6.4 M, that exceeds the 5 M hard limit so the SQL should return the previous row value 3.64M
(thus in Excel the formula would be I9=IF(I8+F9 > $H$2, I8, I8+F9))
same logic should happen for the following rows when ever the Running total breaches the Hard limit

F G H I
SaleAmount RunningTotal Hard limit Desired output
2 8,778.52 8,778.52 5,000,000 8,778.52
3 1,496,692.06 1,505,470.58 1,505,470.58
4 1,849,601.42 3,355,072 3,355,072
5 445.41 3,355,517.41 3,355,517.41
6 109.98 3,355,627.39 3,355,627.39
7 3,933.56 3,359,560.95 3,359,560.95
8 280,915.44 3,640,476.39 3,640,476.39
9 2,778,827.65 6,419,304.04 3,640,476.39
10 1,349,832.28 7,769,136.32 4,990,308.67
11 2,881.94 7,772,018.26 4,993,190.61
12 145,824.06 7,917,842.32 4,993,190.61
13 316.97 7,918,159.29 4,993,507.58
14 1,181,685.55 9,099,844.84 4,993,507.58
15 2,568.51 9,102,413.35 4,996,076.09
16 3,000 9,105,413.35 4,999,076.09
17 1,286,983.8 1,039,2,397.15 4,999,076.09
18 2,735.87 1,039,5,133.02 4,999,076.09
19 1,442,096.02 1,183,7,229.04 4,999,076.09

request you to please help me out with Ideas to achieve this.


Solution

  • Your best bet will be to use a recursive Common Table Expression.

    I voluntarily chose the simplest use (row-by-row iterations), because it will be a lot to learn at first,
    but you will then be able to optimize it (for example by using a windowed sum() over () to make a first pass containing "everything surely under the cutoff" instead of the row-by-row iterations),
    or use RDBMS specific functions (PostgreSQL User-Defined Aggregates).

    Here is it with PostgreSQL (you didn't tell which RDBMS you use, so I choose the one I prefer…):

    with recursive
    -- Give each row its position, we'll need it to iterate from row to row:
    p as (select row_number() over() pos, * from t),
    -- Now "recurse" (iterate) over rows:
    r as
    (
      -- Init our recursion with the first amount, use its amount as the first value of the running sum, and as the capped sum if it's less than the cap:
      select *, amount rs, case when amount > 5000000 then 0 else amount end capped from p where pos = 1
      -- Now the recursive part:
      union all
      select p.*, rs + p.amount, capped + case when capped + p.amount > 5000000 then 0 else p.amount end
      -- Use r itself (the previous iteration of our recursive CTE) to have the pos of the last iteration, and fetch the row having the next pos
      from r join p on p.pos = r.pos + 1
    )
    select * from r order by pos;
    
    pos amount rs capped
    1 8778.52 8778.52 8778.52
    2 1496692.06 1505470.58 1505470.58
    3 1849601.42 3355072 3355072
    4 445.41 3355517.41 3355517.41
    5 109.98 3355627.39 3355627.39
    6 3933.56 3359560.95 3359560.95
    7 280915.44 3640476.39 3640476.39
    8 2778827.65 6419304.04 3640476.39
    9 1349832.28 7769136.32 4990308.67
    10 2881.94 7772018.260000001 4993190.61
    11 145824.06 7917842.32 4993190.61
    12 316.97 7918159.29 4993507.58
    13 1181685.55 9099844.84 4993507.58
    14 2568.51 9102413.35 4996076.09
    15 3000 9105413.35 4999076.09
    16 1286983.8 10392397.15 4999076.09
    17 2735.87 10395133.02 4999076.09
    18 1442096.02 11837229.04 4999076.09

    I've put it running on your data in an example fiddle you can play with.