Window Functions - Running Total with reset

后端 未结 3 487
梦如初夏
梦如初夏 2020-12-09 22:46

I am using SQL Server 2012 to build an inventory planning / reorder engine.

I have a bunch of dated transactions, call them credits and debits. I want to do two thi

相关标签:
3条回答
  • 2020-12-09 23:21

    Ugh, based on your comments, the only thing I can think to do is use a cursor, which I hate doing.

    SQL Fiddle

    declare @Date date
    declare @Qty int
    declare @RR int
    
    
    declare @running int  = 0
    
    declare @results table
    (dt date,
     qty int,
     rt int,
     rr int
    )
    
    declare C cursor for
    select TDate, Qty,
    RecommendedReplenish 
    from (
        select 
            TDate, 
            Qty,
            -1 * (CASE WHEN Qty < 0 AND SUM(Qty) OVER (ORDER BY TDate ROWS UNBOUNDED     PRECEDING) < 0 
                    THEN 
                CASE WHEN Qty >  SUM(Qty) OVER (ORDER BY TDate ROWS UNBOUNDED PRECEDING)     THEN Qty ELSE SUM(Qty) OVER (ORDER                        BY TDate ROWS UNBOUNDED PRECEDING) END
            ELSE 0 END) as RecommendedReplenish
            /* Wrong, does not account for balance resetting to zero */
        from TX 
    ) T order by TDate
    
    open c
    fetch next from c into @date,@qty,@rr
    WHILE @@FETCH_STATUS = 0
    BEGIN
    
    
    
      set @running = @running + @qty
      if @running <0
        begin
          set @running = 0
        end
    
      insert into @results values (@date,@qty,@running,@rr)
    
      fetch next from c into @date,@qty,@rr
    end
    close c
    deallocate c
    select
    *
    from @results
    

    Which as far as I can tell, gives you the desired result. It ain't pretty, I'm sure it could use some cleanup, but it works.

    +-------------+------+-----+----+
    |     DT      | QTY  | RT  | RR |
    +-------------+------+-----+----+
    | 2014-03-01  |  20  | 20  |  0 |
    | 2014-03-02  | -10  | 10  |  0 |
    | 2014-03-03  | -20  |  0  | 10 |
    | 2014-03-04  | -10  |  0  | 10 |
    | 2014-03-05  |  30  | 30  |  0 |
    | 2014-03-06  | -20  | 10  | 10 |
    | 2014-03-07  |  10  | 20  |  0 |
    | 2014-03-08  | -20  |  0  | 20 |
    | 2014-03-09  |  -5  |  0  |  5 |
    +-------------+------+-----+----+
    
    0 讨论(0)
  • 2020-12-09 23:28

    Using a temp-table you could apply the Replenishment as you go. Not sure if it would be much faster than the cursor approach from @Andrew; probably depends on how often the RT dips below zero. I used a simple subquery to calculate the RT, less typing, same result although I agree it takes an extra step.

    SQL Fiddle

    CREATE TABLE TX (TDate DATETIME, Qty   INT, Replenish INT NULL, RT INT NULL);
    
    INSERT INTO TX VALUES ('2014-03-01', 20, NULL, NULL);  
    INSERT INTO TX VALUES ('2014-03-02',-10, NULL, NULL); 
    INSERT INTO TX VALUES ('2014-03-03',-20, NULL, NULL); 
    INSERT INTO TX VALUES ('2014-03-04',-10, NULL, NULL); 
    INSERT INTO TX VALUES ('2014-03-05', 30, NULL, NULL); 
    INSERT INTO TX VALUES ('2014-03-06',-20, NULL, NULL);  
    INSERT INTO TX VALUES ('2014-03-07', 10, NULL, NULL);  
    INSERT INTO TX VALUES ('2014-03-08',-20, NULL, NULL); 
    INSERT INTO TX VALUES ('2014-03-09', -5, NULL, NULL);
    GO
    
    -- calculate (real) running-totals
    UPDATE TX 
       SET RT = (SELECT SUM(p.Qty)
                   FROM TX p
                  WHERE p.TDate <= upd.TDate)
      FROM TX upd
    GO
    
    -- create a loop to find if there are negative RT's and fix them untill there are none left
    DECLARE @below_zero_date DATETIME,
            @below_zero_value INT
    
    -- SELECT * FROM TX ORDER BY TDate
    
    SELECT @below_zero_value = NULL
    SELECT TOP 1 @below_zero_date = TDate,
                 @below_zero_value = RT
      FROM TX
     WHERE RT < 0
     ORDER BY TDate
    
    WHILE @below_zero_value IS NOT NULL
        BEGIN
            UPDATE TX
               SET RT = RT - @below_zero_value,
                   Replenish = (CASE TDate WHEN @below_zero_date THEN - @below_zero_value ELSE NULL END)
             WHERE TDate >= @below_zero_date
    
            -- SELECT * FROM TX ORDER BY TDate
    
            SELECT @below_zero_value = NULL
    
            SELECT TOP 1 @below_zero_date = TDate,
                         @below_zero_value = RT
              FROM TX
             WHERE RT < 0
               AND TDate > @below_zero_date
             ORDER BY TDate
        END
    
    SELECT * FROM TX ORDER BY TDate
    

    UPDATE: added AND TDate > @below_zero_date as (minor) improvement; it will only have a significant effect when there is 'quite a bit' of data in the table.

    0 讨论(0)
  • 2020-12-09 23:42

    This can be done using a set-based solution:

    1.Compute the normal running total (call it RT)

    2.Compute the running minimum of RT (call it MN)

    When MN is negative, -MN is the total quantity you had to replenish so far. Let replenish_rt be -MN when MN is negative. So, the new running total (call it new_rt) is rt + replenish_rt. And if you need to return the current replenish quantity needed, subtract the pervious replenish_rt (using LAG) from the current.

    Here's the complete solution query:

    with c1 as
    (
      select *,
        sum(qty) over(order by tdate rows unbounded preceding) as rt
      from tx
    ),
    c2 as
    (
      select *,
        -- when negative, mn is the total qty that had to be
        -- replenished until now, inclusive
        min(rt) over(order by tdate rows unbounded preceding) as mn_cur
      from c1
    )
    select tdate, qty, rt,
      replenish_rt - lag(replenish_rt, 1, 0) over(order by tdate) as replenish,
      rt + replenish_rt as new_rt
    from c2
      cross apply(values(case when mn_cur < 0 then -mn_cur else 0 end)) as a1(replenish_rt);
    
    Cheers, Itzik

    0 讨论(0)
提交回复
热议问题