问题
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