user1281598
user1281598

Reputation: 317

How can I calculate the remaining amount per row?

I have a table that I want to find for each row id the amount remaining from the total. However, the order of amounts is in an ascending order.

id   amount
1    3
2    2
3    1
4    5

The results should look like this:

id   remainder
1    10
2    8
3    5
4    0

Any thoughts on how to accomplish this? I'm guessing that the over clause is the way to go, but I can't quite piece it together.Thanks.

Upvotes: 0

Views: 1230

Answers (3)

Michael Buen
Michael Buen

Reputation: 39393

SQL Server 2008 answer, I can't provide an SQL Fiddle, it seems it strips the begin keyword, resulting to syntax errors. I tested this on my machine though:

create function RunningTotalGuarded()
returns @ReturnTable table(
    Id int, 
    Amount int not null, 
    RunningTotal int not null, 
    RN int identity(1,1) not null primary key clustered
)

as
begin

  insert into @ReturnTable(id, amount, RunningTotal) 
  select id, amount, 0 from tbl order by amount;

  declare @RunningTotal numeric(16,4) = 0;
  declare @rn_check int = 0;

  update @ReturnTable
    set 
        @rn_check = @rn_check + 1
        ,@RunningTotal = 
        case when rn = @rn_check then
            @RunningTotal + Amount
        else
            1 / 0
        end
        ,RunningTotal = @RunningTotal;     
  return;    
end;

To achieve your desired output:

with a as
(
    select *, sum(amount) over() - RunningTotal as remainder
        , row_number() over(order by id) as id_order
    from RunningTotalGuarded()
)
select a.id, amount_order.remainder
from a
inner join a amount_order on amount_order.rn = a.id_order;

Rationale for guarded running total: http://www.ienablemuch.com/2012/05/recursive-cte-is-evil-and-cursor-is.html

Choose the lesser evil ;-)

Upvotes: -1

Michael Buen
Michael Buen

Reputation: 39393

Since you didn't specify your RDBMS, I will just assume it's Postgresql ;-)

select  *, sum(amount) over() - sum(amount) over(order by amount) as remainder
from tbl;

Output:

| ID | AMOUNT | REMAINDER |
---------------------------
|  3 |      1 |        10 |
|  2 |      2 |         8 |
|  1 |      3 |         5 |
|  4 |      5 |         0 |

How it works: http://www.sqlfiddle.com/#!1/c446a/5

It works in SQL Server 2012 too: http://www.sqlfiddle.com/#!6/c446a/1

Thinking of solution for SQL Server 2008...


Btw, is your ID just a mere row number? If it is, just do this:

select 
  row_number() over(order by amount) as rn
  , sum(amount) over() - sum(amount) over(order by amount) as remainder
from tbl
order by rn;

Output:

| RN | REMAINDER |
------------------
|  1 |        10 |
|  2 |         8 |
|  3 |         5 |
|  4 |         0 |

But if you really need the ID intact and move the smallest amount on top, do this:

with a as
(
  select  *, sum(amount) over() - sum(amount) over(order by amount) as remainder,
      row_number() over(order by id) as id_sort,
      row_number() over(order by amount) as amount_sort
  from tbl
)
select a.id, sort.remainder
from a
join a sort on sort.amount_sort = a.id_sort
order by a.id_sort;

Output:

| ID | REMAINDER |
------------------
|  1 |        10 |
|  2 |         8 |
|  3 |         5 |
|  4 |         0 |

See query progression here: http://www.sqlfiddle.com/#!6/c446a/11

Upvotes: 2

Gordon Linoff
Gordon Linoff

Reputation: 1269753

I just want to offer a simpler way to do this in descending order:

select id, sum(amount) over (order by id desc) as Remainder
from t

This will work in Oracle, SQL Server 2012, and Postgres.

The general solution requres a self join:

select t.id, coalesce(sum(tafter.amount), 0) as Remainder
from t left outer join
     t tafter
     on t.id < tafter.id
group by t.id

Upvotes: 1

Related Questions