Running total… with a twist

萝らか妹 提交于 2021-02-08 04:43:22

问题


I am trying to figure out the SQL to do a running total for a daily quota system. The system works like this...

Each day a user gets a quota of 2 "consumable things". If they use them all up, the next day they get another 2. If they somehow over use them (use more than 2), the next day they still get 2 (they can't have a negative balance). If they don't use them all, the remainder carries to the next day (which can carry to the next, etc...).

Here is a chart of data to use as validation. It's laid out as quota for the day, amount used that day, amount left at the end of the day:

2 - 2 - 0
2 - 0 - 2
4 - 3 - 1
3 - 0 - 3
5 - 7 - 0
2 - 1 - 1
3 - 0 - 3
5 - 2 - 3
5 - 1 - 4
6 - 9 - 0

The SQL to start of with would be:

WITH t(x, y) AS (
  VALUES (2, '2013-09-16'),
              (0, '2013-09-17'),
              (3, '2013-09-18'),
              (0, '2013-09-19'),
              (7, '2013-09-20'),
              (1, '2013-09-21'),
              (0, '2013-09-22'),
              (2, '2013-09-23'),
              (1, '2013-09-24'),
              (9, '2013-09-25')
)

For the life of me, trying recursive with statements and window aggregates, I cannot figure out how to make it work (but I can certainly see the pattern).

It should be something like 2 - x + SUM(previous row), but I don't know how to put that in to SQL.


回答1:


Try creating custom aggregate function like:

CREATE FUNCTION quota_calc_func(numeric, numeric, numeric) -- carry over, daily usage and daily quota
RETURNS numeric AS 
$$
  SELECT GREATEST(0, $1 + $3 - $2);
$$
LANGUAGE SQL STRICT IMMUTABLE;

CREATE AGGREGATE quota_calc( numeric, numeric ) -- daily usage and daily quota
(
    SFUNC = quota_calc_func,
    STYPE = numeric,
    INITCOND = '0'
);

WITH t(x, y) AS (
  VALUES (2, '2013-09-16'),
              (0, '2013-09-17'),
              (3, '2013-09-18'),
              (0, '2013-09-19'),
              (7, '2013-09-20'),
              (1, '2013-09-21'),
              (0, '2013-09-22'),
              (2, '2013-09-23'),
              (1, '2013-09-24'),
              (9, '2013-09-25')
)
SELECT x, y, quota_calc(x, 2) over (order by y)
FROM t;

May contain bugs, haven't tested it.




回答2:


they can't have a negative balance

That triggered my memory :-)

I had a similar problem >10 years ago on a Teradata system.

The logic could be easily implemented using recursion, for each row do:

add 2 "new" and substract x "used" quota, if this is less than zero use zero instead.

I can't remember how i found that solution, but i finally implemented it using simple cumulative sums:

SELECT
  dt.*, 
  CASE -- used in following calculation, this is just for illustration
     WHEN MIN(quota_raw) OVER (ORDER BY datecol ROWS UNBOUNDED PRECEDING) >= 0 THEN 0 
     ELSE MIN(quota_raw) OVER (ORDER BY datecol ROWS UNBOUNDED PRECEDING)
  END AS correction,
  quota_raw
  - CASE
       WHEN MIN(quota_raw) OVER (ORDER BY datecol ROWS UNBOUNDED PRECEDING) >= 0 THEN 0 
       ELSE MIN(quota_raw) OVER (ORDER BY datecol ROWS UNBOUNDED PRECEDING)
    END AS quote_left
FROM
 (
   SELECT quota, datecol, 
      SUM(quota) OVER (ORDER BY datecol ROWS UNBOUNDED PRECEDING) AS quota_used,
      2*COUNT(*) OVER (ORDER BY datecol ROWS UNBOUNDED PRECEDING) AS quota_available,
      quota_available - quota_used AS quota_raw 
   FROM t
 ) AS dt
ORDER BY datecol                           

The secret sauce is the moving min "correction" which adjusts negative results to zero.




回答3:


simple recursive cte solution, assuming you have no gaps in dates:

with recursive cte as (
    select
        t.dt,
        2 as quote_day,
        t.quote_used,
        greatest(2 - t.quote_used, 0) as quote_left
    from t
    where t.dt = '2013-09-16'
    union all
    select
        t.dt,
        2 + c.quote_left as quote_day,
        t.quote_used,
        greatest(2 + c.quote_left - t.quote_used, 0) as quote_left
    from cte as c
        inner join t on t.dt = c.dt + 1  
)
select *
from cte

sql fiddle demo

another solution - with cumulative aggregates:

with cte1 as (
    select
        dt, quote_used,
        sum(2 - quote_used) over(order by dt asc) as quote_raw
    from t
), cte2 as (
    select
        dt, quote_used, quote_raw,
        least(min(quote_raw) over(order by dt asc), 0) as quote_corr
    from cte1
)
select
    dt,
    quote_raw - quote_corr + quote_used as quote_day,
    quote_used,
    quote_raw - quote_corr as quote_left
from cte2

sql fiddle demo



来源:https://stackoverflow.com/questions/18897847/running-total-with-a-twist

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!