(
https://livesql.oracle.com/ords/livesql/s/da34i74lkmxt2mqrtp0k4xsk6 )
(I was able to format better with screenshots on the Oracle forums -
https://forums.oracle.com/ords/apexds/post/help-solving-a-logical-problem-using-analytical-query-3778 )
Hi,
I am trying to solve a problem using analytical functions but I am stuck.
1. I have a list of coupons that I can use. The usage sequence is in alphabetical order of the coupon name.
+---------+-------+
| Coupons | Value |
+---------+-------+
| A | 100 |
+---------+-------+
| B | 40 |
+---------+-------+
| C | 120 |
+---------+-------+
| D | 10 |
+---------+-------+
| E | 200 |
+---------+-------+
2. There is a limit (cap) on the total value across all coupons that can be used in a day
+----------+-----------+
| Cap Name | Cap Limit |
+----------+-----------+
| Cap 1 | 150 |
+----------+-----------+
| Cap 2 | 70 |
+----------+-----------+
3. Each coupon is subject to 1 or 2 caps. If it is subject to 2 caps, there is a specified sequence to apply caps.
+--------+--------------+----------+
| Coupon | Cap Sequence | Cap Name |
+--------+--------------+----------+
| A | 1 | Cap 1 |
+--------+--------------+----------+
| A | 2 | Cap 2 |
+--------+--------------+----------+
| B | 1 | Cap 2 |
+--------+--------------+----------+
| C | 1 | Cap 2 |
+--------+--------------+----------+
| C | 2 | Cap 1 |
+--------+--------------+----------+
| D | 1 | Cap 1 |
+--------+--------------+----------+
| E | 1 | Cap 1 |
+--------+--------------+----------+
| E | 2 | Cap 2 |
+--------+--------------+----------+
4. I have to now find how much coupon value could be utilized before my daily cap was reached. i.e. find "coupon usage" and “Cap Remaining” below.
So, If I join the tables above
+---+--------+-------+----------+--------------+-----------+--------------+---------------+
| # | Coupon | Value | Cap Name | Cap Sequence | Cap Limit | Coupon Usage | Cap Remaining |
+---+--------+-------+----------+--------------+-----------+--------------+---------------+
| 1 | A | 100 | Cap 1 | 1 | 150 | 100 | 50 |
+---+--------+-------+----------+--------------+-----------+--------------+---------------+
| 2 | A | 100 | Cap 2 | 2 | 70 | 0 | 70 |
+---+--------+-------+----------+--------------+-----------+--------------+---------------+
| 3 | B | 40 | Cap 2 | 1 | 70 | 40 | 30 |
+---+--------+-------+----------+--------------+-----------+--------------+---------------+
| 4 | C | 120 | Cap 2 | 1 | 70 | 30 | 0 |
+---+--------+-------+----------+--------------+-----------+--------------+---------------+
| 5 | C | 120 | Cap 1 | 2 | 150 | 50 | 0 |
+---+--------+-------+----------+--------------+-----------+--------------+---------------+
| 6 | D | 10 | Cap 1 | 1 | 150 | 0 | 0 |
+---+--------+-------+----------+--------------+-----------+--------------+---------------+
| 7 | E | 200 | Cap 1 | 1 | 150 | 0 | 0 |
+---+--------+-------+----------+--------------+-----------+--------------+---------------+
| 8 | E | 200 | Cap 2 | 2 | 70 | 0 | 0 |
+---+--------+-------+----------+--------------+-----------+--------------+---------------+
Explanation:
Row #1 : Coupon A has a value of 100 and that is less than the first cap i.e. Cap 1 with a limit of 150. So we use all of Coupon A. Cap 1 has a limit of 50 remaning.
Row #2: Coupon A was all used against Cap 1 and we did not have to consume Cap 2.
Row #3: Coupon B of value 40 and it is less than Cap 2 with a limit of 70. So we use all of Coupon B. Cap 2 has now 30 remaining.
Row #4: Coupon C has a value of 120. It is subject to Cap 2 first that has 30 remaining. So we use 30 and Cap 2 now has 0 remaining. Coupon C has (120 - 30) i.e. 90 residual value remaining.
Row #5: Coupon C has a 90 value remaning that is now capped against Cap 1 with a 50 limit. So we use 50 and Cap 1 becomes 0
Row #6 - #8: Caps have become 0. Nothing can be used.
I am able to make this work using analytical function with a single cap. The 2 caps with a possibly different usage sequence has me stuck.
with coupon_data(coupon, value)as
(
select 'A',100 from dual union all
select 'B',40 from dual union all
select 'C',120 from dual union all
select 'D',10 from dual union all
select 'E',200 from dual
)
, cap_data(cap_name, cap_limit) as
(
select 'Cap1', 150 from dual union all
select 'Cap2', 70 from dual
)
, coupon_cap_mapping(coupon, cap_sequence, cap_name) as
(
select 'A',1,'Cap1' from dual union all
select 'A',2,'Cap2' from dual union all
select 'B',1,'Cap2' from dual union all
select 'C',1,'Cap2' from dual union all
select 'C',2,'Cap1' from dual union all
select 'D',1,'Cap1' from dual union all
select 'E',1,'Cap1' from dual union all
select 'E',2,'Cap2' from dual
)
SELECT cd.coupon,
cd.value,
cap_d.cap_name,
ccm.cap_sequence,
cap_d.cap_limit
--, coupon_usage, cap_remaining
FROM coupon_data cd
JOIN coupon_cap_mapping ccm ON ( cd.coupon = ccm.coupon )
JOIN cap_data cap_d ON ( cap_d.cap_name = ccm.cap_name )
ORDER BY cd.coupon,
ccm.cap_sequence;
(
https://livesql.oracle.com/ords/livesql/s/da34i74lkmxt2mqrtp0k4xsk6 )
When I try providing the link to livesql, I get the "LiveSQL link must point to a valid shared script link" error.
I see you already have a solution on the forum. As discussed in this month's SQL Office Hours, a (pure) MATCH_RECOGNIZE solution performs much better than recursive WITH.
I can't find a way to calculate the used/remaining totals with pattern matching alone though. So this uses MODEL to calculate these values after using pattern matching to determine whether a coupon can be used. Perhaps someone in the community can find a way to simplify this into one pattern matching solution.
The basics of the pattern variables to see if there's remaining capacity is:
and sum ( limit1.value ) - value < limit1.cap_limit -- 1
and (
( coupon = prev ( coupon ) and sum ( limit2.value ) > limit2.cap_limit ) -- 2
or coupon <> nvl ( prev ( coupon ), 'x' ) -- 3
)
1 - Check the value running total minus the value for the current row is less than the limit (i.e. there's capacity remaining). When this is the case, use the spare capacity if either:
2 - The coupon is the same as the previous, and the total used for the other coupon is greater than it's limit (i.e. this coupon isn't completely used)
3 - It's a new coupon
Have two separate pattern variables for this, one for each cap. The pattern checks whether each row matches either of these or any row, zero or more times:
pattern ( ( limit1 | limit2 | other )* )
Follow this by a MODEL clause, which calculates the amount used and remaining for each capacity. The key here is the CV() function. This identifies the current row. In this case, these are increasing integers assigned by ROW_NUMBER. So:
least ( value [cv()], c1_remaining[cv()-1] )
Means return the smallest of the current value, or the amount remaining for the previous row.
A final case expression at the end combines these separate usage and remaining columns into one.
Complete solution below. I'm not sure how MATCH_RECOGNIZE + MODEL will perform compared to recursive WITH alone, but it's worth testing. Hopefully someone will be able to find a way to simplify this further!
create table coupon_data ( coupon, value ) as (
select 'A',100 from dual union all
select 'B',40 from dual union all
select 'C',120 from dual union all
select 'D',10 from dual union all
select 'E',200 from dual
);
create table cap_data ( cap_name, cap_limit ) as (
select 'Cap1', 150 from dual union all
select 'Cap2', 70 from dual
);
create table coupon_cap_mapping ( coupon, cap_sequence, cap_name ) as (
select 'A',1,'Cap1' from dual union all
select 'A',2,'Cap2' from dual union all
select 'B',1,'Cap2' from dual union all
select 'C',1,'Cap2' from dual union all
select 'C',2,'Cap1' from dual union all
select 'D',1,'Cap1' from dual union all
select 'E',1,'Cap1' from dual union all
select 'E',2,'Cap2' from dual
);
with coupon_caps as (
select cd.coupon,
cd.value,
cap_d.cap_name,
ccm.cap_sequence,
cap_d.cap_limit
from coupon_data cd
join coupon_cap_mapping ccm on ( cd.coupon = ccm.coupon )
join cap_data cap_d on ( cap_d.cap_name = ccm.cap_name )
), matches as (
select mr.*,
row_number () over ( order by coupon, cap_sequence ) as rn
from coupon_caps match_recognize (
order by coupon, cap_sequence
all rows per match
pattern ( ( limit1 | limit2 | other )* )
define
limit1 as
cap_name = 'Cap1'
and sum ( limit1.value ) - value < limit1.cap_limit
and (
( coupon = prev ( coupon ) and sum ( limit2.value ) > limit2.cap_limit )
or coupon <> nvl ( prev ( coupon ), 'x' )
),
limit2 as
cap_name = 'Cap2'
and sum ( limit2.value ) - value < limit2.cap_limit
and (
( coupon = prev ( coupon ) and sum ( limit1.value ) > limit1.cap_limit )
or coupon <> nvl ( prev ( coupon ) , 'x' )
)
) mr
), calcs as (
select *
from matches model
dimension by ( rn )
measures (
0 as c1_usage, 0 as c1_remaining,
0 as c2_usage, 0 as c2_remaining,
cap_limit, cap_name, coupon, cap_sequence, value
)
rules automatic order (
c1_usage [any] = case
when c1_remaining[cv()-1] = 0 then 0
when cv(rn) = 1 and cap_name[cv()] = 'Cap1' then least ( value[cv()], cap_limit[cv()] )
when cap_name[cv()] = 'Cap2' then c1_usage [cv()-1]
when coupon[cv()] = coupon[cv()-1] then least ( value[cv()] - c2_usage[cv()-1], c1_remaining[cv()-1] )
else least ( value [cv()], c1_remaining[cv()-1] )
end,
c1_remaining [any] = case
when c1_remaining[cv()-1] = 0 then 0
when cv(rn) = 1 and cap_name[cv()] = 'Cap1' then greatest ( cap_limit[cv()] - value[cv()], 0 )
when cap_name[cv()] = 'Cap2' then c1_remaining [cv()-1]
when coupon[cv()] = coupon[cv()-1] then greatest ( nvl ( c1_remaining[cv()-1], cap_limit[cv()] ) - least ( value[cv()] - c2_usage[cv()-1], value[cv()-1] ), 0 )
else c1_remaining[cv()-1] - c1_usage[cv()]
end,
c2_usage [any] = case
when c2_remaining[cv()-1] = 0 then 0
when cv(rn) = 1 and cap_name[cv()] = 'Cap2' then least ( value[cv()], cap_limit[cv()] )
when cap_name[cv()] = 'Cap1' then c2_usage [cv()-1]
when coupon[cv()] = coupon[cv()-1] then value[cv()] - c1_usage[cv()-1]
else least ( value [cv()], c2_remaining[cv()-1] )
end,
c2_remaining [any] = case
when c2_remaining[cv()-1] = 0 then 0
when cv(rn) = 1 and cap_name[cv()] = 'Cap2' then greatest ( cap_limit[cv()] - value[cv()], 0 )
when cap_name[cv()] = 'Cap1' then c2_remaining [cv()-1]
when coupon[cv()] = coupon[cv()-1] then greatest ( nvl ( c2_remaining[cv()-1], cap_limit[cv()] ) - least ( value[cv()] - c1_usage[cv()-1], value[cv()-1] ), 0 )
else c2_remaining[cv()-1] - c2_usage[cv()]
end
)
)
select coupon, value, cap_name, cap_sequence, cap_limit,
case cap_name
when 'Cap1' then c1_usage
when 'Cap2' then c2_usage
end usage, case cap_name
when 'Cap1' then c1_remaining
when 'Cap2' then c2_remaining
end remaining
from calcs c
order by coupon,
cap_sequence;
C VALUE CAP_ CAP_SEQUENCE CAP_LIMIT USAGE REMAINING
- ---------- ---- ------------ ---------- ---------- ----------
A 100 Cap1 1 150 100 50
A 100 Cap2 2 70 0 70
B 40 Cap2 1 70 40 30
C 120 Cap2 1 70 30 0
C 120 Cap1 2 150 50 0
D 10 Cap1 1 150 0 0
E 200 Cap1 1 150 0 0
E 200 Cap2 2 70 0 0