Bounded cumulative sum in SQL

后端 未结 1 1174
名媛妹妹
名媛妹妹 2021-01-27 08:31

How can I use SQL to compute a cumulative sum over a column, so that the cumulative sum always stays within upper/lower bounds. Example with lower bound -2 and upper bound 10, s

相关标签:
1条回答
  • 2021-01-27 09:08

    You can (almost) always use a cursor to implement whatever cumulative logic you have. The technique is quite routine so can be used to tackle a variety of problems easily once you get it.

    One specific thing to note: Here I update the table in-place, so the [id] column must be uniquely indexed.

    (Tested on SQL Server 2017 latest linux docker image)

    Test Dataset

    use [testdb];
    if OBJECT_ID('testdb..test') is not null
        drop table testdb..test;
    
    create table test (
        [id] int,
        [input] int,
    );
    
    insert into test (id, input)
    values (1,5), (2,7), (3,-10), (4,-10), (5,5), (6,10);
    

    Solution

    /* A generic row-by-row cursor solution */
    
    -- First of all, make [id] uniquely indexed to enable "where current of" 
    create unique index idx_id on test(id);
    
    -- append answer columns
    alter table test 
        add [cum_sum] int,
            [bounded_cum_sum] int;
    
    -- storage for each row
    declare @id int,
            @input int,
            @cum_sum int, 
            @bounded_cum_sum int;
    -- record accumulated values
    declare @prev_cum_sum int = 0,
            @prev_bounded_cum_sum int = 0;
    
    -- open a cursor ordered by [id] and updatable for assigned columns
    declare cur CURSOR local
    for select [id], [input], [cum_sum], [bounded_cum_sum]
        from test
        order by id
    for update of [cum_sum], [bounded_cum_sum];
    open cur;
    
    while 1=1 BEGIN
    
        /* fetch next row and check termination condition */
        fetch next from cur 
            into @id, @input, @cum_sum, @bounded_cum_sum;
    
        if @@FETCH_STATUS <> 0
            break;
    
        /* program body */
    
        -- main logic
        set @cum_sum = @prev_cum_sum + @input;
        set @bounded_cum_sum = @prev_bounded_cum_sum + @input;
        if @bounded_cum_sum > 10 set @bounded_cum_sum=10
        else if @bounded_cum_sum < -2 set @bounded_cum_sum=-2;
    
        -- write the result back
        update test 
            set [cum_sum] = @cum_sum,
                [bounded_cum_sum] = @bounded_cum_sum
            where current of cur;
    
        -- setup for next row
        set @prev_cum_sum = @cum_sum;
        set @prev_bounded_cum_sum = @bounded_cum_sum;
    END
    
    -- cleanup
    close cur;
    deallocate cur;
    
    -- show
    select * from test;
    

    Result

    |   | id | input | cum_sum | bounded_cum_sum |
    |---|----|-------|---------|-----------------|
    | 1 | 1  | 5     | 5       | 5               |
    | 2 | 2  | 7     | 12      | 10              |
    | 3 | 3  | -10   | 2       | 0               |
    | 4 | 4  | -10   | -8      | -2              |
    | 5 | 5  | 5     | -3      | 3               |
    | 6 | 6  | 10    | 7       | 10              |
    
    0 讨论(0)
提交回复
热议问题