Search code examples
sqlloopsssmscommon-table-expression

Subtract from a running quantity based off values in multiple rows (SQL)


I'm trying to write a SQL query that will do a few things, but i'll start with the easiest first:

I have a group of objects with IDs (in this example, different colors of marbles). My query will SUM these marbles and result in a table like this:

marbles number_of_marbles
Green Marble 50

Then, I have a table of baskets where I want to drop these marbles into. The quantity in this table represents how many are needed to satisfy a full basket of marbles.

basket required_marbles marble_type
basket_one 60 Green Marble
basket_two 30 Green Marble

The end result is that I want to prioritize filling a basket with the least amount of required marbles. The reason is if I can fill up a basket, I can set aside and remove from table eventually.

The desired output in this case would be:

basket required_marbles marble_type
basket_one 40 Green Marble
basket_two 0 Green Marble

I used a CTE provided by a previous answer but ran into a slight issue:

select top 1 @number_of_marbles = number_of_marbles from @marbles
        ;with cte as (select *,ROW_NUMBER() over(order by [required_marbles] asc
        ) as rno from @required_marbles_table),

            cte1(rno,tot) as (select top 1 rno, @number_of_marbles-required_marbles as tot from cte t
            union all select t.rno, tot-required_marbles as tot from cte t,cte1  where (t.rno-1)=cte1.rno
            )
            update t set required_marbles=(case when tot>0 then 0 else -tot end) from cte t inner join cte1 on t.rno=cte1.rno
        
            select * from @required_marbles_table

In this case, the result is correct (i.e. look at desired output table). However, if the quantity wouldn't 0 out a row (i.e. if number_of_marbles is actually 5 for example), the number is subtracted from every single row.

basket required_marbles marble_type
basket_one 55 Green Marble
basket_two 25 Green Marble

That's the 'easy' part

The other piece I would like to add is, if you have two different types of marbles.

marbles number_of_marbles
Green Marble 50
Blue Marble 30

and I want to fill up some baskets:

basket required_marbles marble_type
basket_one 60 Green Marble
basket_two 30 Green Marble
basket_three 50 Blue Marble

I would like to be able to use the 'new' code to loop through based off marble color and allocate to it's respective basekt. End result being something like:

basket required_marbles marble_type
basket_one 40 Green Marble
basket_two 0 Green Marble
basket_three 20 Blue Marble

Solution

  • I do not use your environment, but as this question is marked with SQL tag I made two solutions in Oracle. They are close to standard SQL, so can be easily adapted to your needs, I hope. First solution uses only analytic sum and some logic with greatest and least:

    select basket, type, req, amt, 
           least(greatest(
             sum(req) over (partition by type order by req, basket) - amt, 
             0), req) result
      from baskets join marbles using(type) order by type desc, basket
    

    Second solution uses recursive query:

    with base as (
      select basket, type, req, amt, 
             row_number() over (partition by type order by req, basket) rn
      from baskets b join marbles using (type) ),
    r(rn, basket, type, req, amt) as (
      select rn, basket, type, greatest(req - amt, 0), 
             greatest(amt - req, 0) 
      from base where rn = 1
      union all
      select r.rn + 1, b.basket, b.type, greatest(b.req - r.amt, 0), 
             greatest(r.amt - b.req, 0) 
      from r join base b on b.rn = r.rn + 1 and b.type = r.type )
    select basket, type, req from r order by type desc, basket;
    

    dbfiddles for both solutions: fiddle 1 and fiddle 2