Subquery Factoring recursive sql - sql

Im having an issue where im using recursive subquery factoring to use the previous rows values as my next rows values. Problem is i need to stop using the previous rows values if my product_key changes.
CREATE TABLE MAKE_IT_WORK
(
PRODUCT_KEY NUMBER,
WEEK NUMBER,
OPENING_STOCK NUMBER,
INTAKE NUMBER,
SALES NUMBER,
CLOSING_STOCK NUMBER,
FORWARD_COVER NUMBER
);
Insert into MAKE_IT_WORK (PRODUCT_KEY, WEEK)
Values (1, 1);
Insert into MAKE_IT_WORK (PRODUCT_KEY, WEEK, INTAKE, SALES)
Values (1, 2, 1000, 80);
Insert into MAKE_IT_WORK (PRODUCT_KEY, WEEK, SALES)
Values (1, 3, 70);
Insert into MAKE_IT_WORK (PRODUCT_KEY, WEEK, SALES)
Values (1, 4, 90);
Insert into MAKE_IT_WORK (PRODUCT_KEY, WEEK, SALES)
Values (2, 1, 0);
Insert into MAKE_IT_WORK (PRODUCT_KEY, WEEK, INTAKE, SALES)
Values (2, 2, 6000, 500);
Insert into MAKE_IT_WORK (PRODUCT_KEY, WEEK, SALES)
Values (2, 3, 70);
Insert into MAKE_IT_WORK (PRODUCT_KEY, WEEK, SALES)
Values (2, 4, 350);
CURRENT QUERY
with master
as(select product_key,week,opening_stock ,intake,sales,closing_stock,forward_cover,row_number()over( order by 1) lvl,product_key-1 pkey
from make_it_work),
bdw_knows_best(product_key,week,opening_stock,intake,sales,closing_stock,forward_cover,lvl,pkey) as
(select product_key
,week
,opening_stock
,nvl(intake,0)intake
,sales
,closing_stock
,forward_cover
,lvl
,pkey
from master
where lvl = 1
union all
select a.product_key
,a.week
,case when b.closing_stock < 0 then 0
else b.closing_stock
end opening_stock
,nvl(a.intake,0)intake
,nvl(a.sales,0) sales
,case when nvl(b.closing_stock,0) + nvl(a.intake,0) - nvl(a.sales,0) < 0 THEN 0
else nvl(b.closing_stock,0) + nvl(a.intake,0) - nvl(a.sales,0)
end closing_stock
,a.forward_cover
,b.lvl +1
,a.pkey pkey
from master a,
bdw_knows_best b
where a.lvl = b.lvl +1
)
select product_key,week,opening_stock,intake,sales,closing_stock,forward_cover,lvl,pkey from bdw_knows_best;
REQUIRED
When the product key changes from 1 to 2, I need to use the values from Product_Key 2 and not the last records from Product_Key 1. I need to somehow group the by Product_Key buckets(so to speak).
Any help or ideas would be highly appreaciated

You don't need a recursive CTE. Window functions (the OVER clause) will produce the result you want. For example:
select product_key, week, opening_stock, intake, sales,
coalesce(opening_stock, 0)
+ sum(intake) over(partition by product_key order by week)
- sum(sales) over(partition by product_key order by week)
as closing_stock
from make_it_work
order by product_key, week;
Result:
PRODUCT_KEY WEEK OPENING_STOCK INTAKE SALES CLOSING_STOCK
------------ ----- -------------- ------- ------ -------------
1 1
1 2 1000 80 920
1 3 70 850
1 4 90 760
2 1 0
2 2 6000 500 5500
2 3 70 5430
2 4 350 5080
See running example at db<>fiddle.

Related

Filter Table results Self Join

Imagine a large table that contains receipt information. Since it holds so much data, you are required to return a subset of the data, excluding or consolidating rows where possible.
Here is the SQL and results table showing how the data should be returned.
create table table1
(RecieptNo smallint, Customer varchar(10), ReceiptDate date,
ItemDesc varchar(10), Amount smallint)
insert into table1 values
(100, 'Matt','2022-01-05','Ball', 10),
(101, 'Mark','2022-01-07','Hat', 20),
(101, 'Mark','2022-01-07','Jumper', -20),
(101, 'Mark','2022-01-14','Spoon', 30),
(102, 'Luke','2022-01-15','Fork', 15),
(102, 'Luke','2022-01-17','Spork', -10),
(103, 'John','2022-01-20','Orange', 13),
(103, 'John','2022-01-25','Pear', 12)
If there are rows on the same receipt where the negative and positive values cancel out, do not return either row.
If there is a receipt with a negative amount not exceeding positive amount, the negative amount should be deducted from positive line.
RecieptNo
Customer
ReceiptDate
ItemDesc
Amount
100
Matt
2022-01-05
Ball
10
101
Mark
2022-01-14
Spoon
30
102
Luke
2022-01-15
Fork
5
103
John
2022-01-20
Orange
13
103
John
2022-01-25
Pear
12
This is proving tricky, any ideas?
Based on table you provided, I suppose you want only row with the earliest date when you have multiple rows with same receipts which bring positive Amount after deduction.
;WITH cte AS (
SELECT *
, SUM( amount) OVER (PARTITION BY RecieptNo ORDER BY RecieptNo, ReceiptDate ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS ActualAmount
, ROW_NUMBER() OVER (PARTITION BY RecieptNo ORDER BY RecieptNo, ReceiptDate) AS rn
FROM table1)
SELECT RecieptNo, Customer, ReceiptDate, ItemDesc, ActualAmount
FROM cte
WHERE ActualAmount > 0 AND rn = 1
Read about window functions and cte's though.

How to distribute sales with partitions

I have 2 tables:
1st table columns: ItemCode int, Amount float (I have over 1000 ItemCodes)
2nd table columns: ItemCode int, SoldAmount float, Price float (I have over 10000 sale rows for different items)
Example:
ItemId 1528's Amount in 1st table is 244. That items sales in the 2nd table is as below:
Sale 1 Amount = 120, Price = 10
Sale 2 Amount = 120, Price = 30
Sale 3 Amount = 100, Price = 20
Sale 4 Amount = 10, Price = 25
ItemCode
Amount
1528
244
1530
150
ItemCode
Date
Amount
Price
1528
2021.11.01
120
10
1530
2021.10.01
120
30
1528
2021.09.01
100
20
1530
2021.08.01
10
25
Tried cursor and loop , but no desired output.
The desired outcome is to distribute that 100 amount with the sales above like following:
Sale 1 Amount 60: 100 - 60 = 40 with price 5 --- So we continue to the next row and subtract whatever is left
Sale 2 Amount 30: 40 - 30 = 10 with price 6 --- So we continue to the next row and subtract whatever is left
Sale 3 Amount 20: 10 - 20 = -10 with price 7 --- So we stop here as the amount is equal to 0 or below .
As the result we should get this:
60 * 5 = 300
30 * 6 = 180
10 * 7 = 70 (that 10 is derived from whatever could be subtracted before it hits 0)
Desired table as below
ItemCode
Date
Amount
Price
1528
2021.11.01
120
10
1528
2021.10.01
120
30
1528
2021.09.01
4
20
My last attempt was as below
WITH CTE AS (
SELECT ItemCode, SUM(Amount) AS Amount
FROM table 1
GROUP BY STOCKREF )
SELECT *,
IIF(LAG(table1.Amount - table2.amount) OVER (PARTITION BY table1.Amount ORDER BY Date DESC) IS NULL, table1.Amount - table2.amount,
LAG(table1.Amount - table2.amount) OVER (PARTITION BY CTE.ItemCode ORDER BY Date DESC) - table2.AMOUNT) AS COL
FROM CTE JOIN (SELECT ItemCode, DATE_, AMOUNT, PRICE FROM table2) table 2 ON table1.ItemCode = table2.Amount
Hopefully this addresses the right question - if you're trying to create a running total per item_code, deducting the sale quantity from starting inventory from first-to-last sale, maybe this would work:
CREATE TABLE #items (item_code INT,
item_amount INT);
INSERT INTO #items (item_code, item_amount)
VALUES (1528, 244);
INSERT INTO #items (item_code, item_amount)
VALUES (1529, 240);
CREATE TABLE #sales (item_code INT,
sale_date DATE,
sale_amount INT,
sale_price DECIMAL(12,2));
INSERT INTO #sales (item_code, sale_date, sale_amount, sale_price)
VALUES (1528, '2021-12-01', 50, 5);
INSERT INTO #sales (item_code, sale_date, sale_amount, sale_price)
VALUES (1528, '2021-11-29', 120, 6.76292);
INSERT INTO #sales (item_code, sale_date, sale_amount, sale_price)
VALUES (1528, '2021-11-15', 120, 6.6453);
INSERT INTO #sales (item_code, sale_date, sale_amount, sale_price)
VALUES (1528, '2021-11-01', 100, 6.96875);
INSERT INTO #sales (item_code, sale_date, sale_amount, sale_price)
VALUES (1529, '2021-11-30', 48, 7.2);
INSERT INTO #sales (item_code, sale_date, sale_amount, sale_price)
VALUES (1529, '2021-11-18', 48, 3.5);
INSERT INTO #sales (item_code, sale_date, sale_amount, sale_price)
VALUES (1529, '2021-11-09', 96, 3.9);
INSERT INTO #sales (item_code, sale_date, sale_amount, sale_price)
VALUES (1529, '2021-11-05', 96, 3.75);
;WITH all_sales_with_running_totals AS ( --Calculate the running total of each item, deducting sale amount from total starting amount, in order of first sale to last
SELECT s.item_code,
s.sale_date,
s.sale_price,
i.item_amount AS starting_amount,
s.sale_amount,
i.item_amount - SUM(sale_amount) OVER(PARTITION BY s.item_code
ORDER BY s.sale_date
ROWS UNBOUNDED PRECEDING
) AS running_sale_amount
FROM #sales AS s
JOIN #items AS i ON s.item_code = i.item_code
),
sales_with_prev_running_total AS ( --Add the previous rows' running total, to assist with the final calculation
SELECT item_code,
sale_date,
sale_price,
starting_amount,
sale_amount,
running_sale_amount,
LAG(running_sale_amount, 1, NULL) OVER(PARTITION BY item_code
ORDER BY sale_date
)AS prev_running_sale_amount
FROM all_sales_with_running_totals
)
SELECT item_code, --Return the final running sale amount for each sale - if the inventory has already run out, return null. If there is insufficient inventory to fill the order, fill it with the qty remaining. Otherwise, fill the entire order.
sale_date,
sale_price,
starting_amount,
sale_amount,
running_sale_amount,
prev_running_sale_amount,
CASE WHEN prev_running_sale_amount <= 0
THEN NULL
WHEN running_sale_amount < 0
THEN prev_running_sale_amount
ELSE sale_amount
END AS result_sale_amount
FROM sales_with_prev_running_total;

Group price with start and end date

I have a table
Recordid Price Start date end date
-----------------------------------------
1 20 2017-10-01 2017-10-02
2 20 2017-10-03 2017-10-04
3 30 2017-10-05 2017-10-05
4 20 2017-10-06 2017-10-07
I want to get every price when it started and when it ended so my result set would be
20. 2017-10-01. 2017-10-04
30. 2017-10-05. 2017-10-05
20. 2017-10-06. 2017-10-07
I'm having problems to figure it out
It's an Oracle database
i figured it out with the code below
SELECT distinct price
, case when start_dt is null then lag(start_dt) over (order by start_date)
else start_dt end realstart
, case when end_dt is null then lead(end_dt) over (order by end_date)
else end_dt end realend
FROM (SELECT case when nvl(lag(price) over (order by start_date),-1) <> price then start_date end start_dt
, case when nvl(lead(price) over (order by end_date),-1) <>price then end_date end end_dt
, price
, start_date
, end_date
FROM t) main
WHERE start_dt is not null
or end_dt is not null
From your sample data I think you want to have start date and end date whenever the price has been changed in order of the record id.
The following query may contain more sub queries than neccessary, because readability. The very inner select determines when the price has been changed here called group changes. The Next level froms the group by a rolling sum. This is possible, because the only the group change contains values > 0. The rest is obvious.
SELECT GRP,
PRICE,
MIN("Start date") AS "Start date",
MAX("end date") AS "end date"
FROM ( SELECT sub.*,
SUM(GROUP_CHANGE) OVER (ORDER BY RECORDID) AS GRP
FROM ( SELECT t.*,
CASE
WHEN RECORDID = LAG(t.RECORDID) OVER (ORDER BY t.PRICE, t.RECORDID) + 1
THEN 0
ELSE RECORDID
END AS GROUP_CHANGE
FROM t ) sub ) fin
GROUP BY GRP, PRICE
ORDER BY GRP
GRP PRICE Start date end date
---------- ---------- ---------- --------
1 20 01.10.17 04.10.17
4 30 05.10.17 05.10.17
8 20 06.10.17 11.10.17
Tested with following data (Note that I have add some record to your deliverd sample data as I wanted to have a group with three records)
CREATE TABLE t (
Recordid INT,
Price INT,
"Start date" DATE,
"end date" DATE
);
INSERT INTO t VALUES (1, 20, TO_DATE('2017-10-01', 'YYYY-MM-DD'), TO_DATE('2017-10-02', 'YYYY-MM-DD'));
INSERT INTO t VALUES (2, 20, TO_DATE('2017-10-03', 'YYYY-MM-DD'), TO_DATE('2017-10-04', 'YYYY-MM-DD'));
INSERT INTO t VALUES (3, 30, TO_DATE('2017-10-05', 'YYYY-MM-DD'), TO_DATE('2017-10-05', 'YYYY-MM-DD'));
INSERT INTO t VALUES (4, 20, TO_DATE('2017-10-06', 'YYYY-MM-DD'), TO_DATE('2017-10-07', 'YYYY-MM-DD'));
INSERT INTO t VALUES (5, 20, TO_DATE('2017-10-08', 'YYYY-MM-DD'), TO_DATE('2017-10-09', 'YYYY-MM-DD'));
INSERT INTO t VALUES (6, 20, TO_DATE('2017-10-10', 'YYYY-MM-DD'), TO_DATE('2017-10-11', 'YYYY-MM-DD'));
Here is one method that might work in your case:
select price, min(start_date), max(end_date)
from (select t.*,
sum(case when prev_price = price and prev_end_date = start_date - 1
then 0 else 1
end) over (order by t.start_date) as grp
from (select t.*,
lag(t.end_date) over (order by t.start_date) as prev_end_date,
lag(t.price) over (order by t.start_date) as prev_price
from t
) t
) t
group by price, grp

Performing a COUNT on the MAX number of occurences

My scenario is that person A can sells product A each month of the year. From that information I had to calculate in what month they sold the most of product A for the current year.
Should they sell 10 of product A in January, 6 in August and 10 October, i take the info for the latest month (in this case October).
However, i want to include some sort of tracker that says if the current MAX for the year has been equaled by person A at an earlier point in the year i want to COUNT the number of occurencies. Should person A go on to sell 15 in Novemeber, the counter should restart.
Current data =
EMP PRODUCT MONTH VOLUME
---------------------------------------------------
A A 1 10
A A 8 6
A A 10 10
AIM=
EMP PRODUCT MAX(VOLUME) COUNT
---------------------------------------------------
A A 10 2
Any suggestions as to the most efficient way of resolving this would be great!
CREATE TABLE MY_TABLE (EMP VARCHAR2(10), PRODUCT VARCHAR2(10), MONTH NUMBER, VOLUME NUMBER);
INSERT INTO MY_TABLE VALUES ('A', 'A', 1, 10);
INSERT INTO MY_TABLE VALUES ('A', 'A', 8, 6);
INSERT INTO MY_TABLE VALUES ('A', 'A', 10, 10);
COMMIT;
--EMP PRODUCT MONTH VOLUME
-----------------------------------------------------
--A A 1 10
--A A 8 6
--A A 10 10
SELECT EMP,
PRODUCT,
VOLUME,
MY_COUNT
FROM ( SELECT EMP,
PRODUCT,
VOLUME,
COUNT (MY_RANK) MY_COUNT,
RANK () OVER (PARTITION BY EMP, PRODUCT ORDER BY VOLUME DESC)
MY_SECOND_RANK
FROM (SELECT EMP,
PRODUCT,
volume,
RANK ()
OVER (PARTITION BY EMP, PRODUCT
ORDER BY VOLUME DESC, MONTH DESC)
MY_RANK
FROM MY_TABLE)
GROUP BY EMP, PRODUCT, VOLUME)
WHERE MY_SECOND_RANK = 1;

Oracle SQL - Sum of distinct value belong year, for the first time

sorry for the title but is a little difficult to explain the topic in a sigle row..
I have a table like this and i want to know (for each month in the year) the number of employee who have received bonus for the first time.
EMPLOYEE_NAME MONTH BONUS_RECEIVED
AAA 1 1
BBB 1 1
CCC 2 1
AAA 2 1
DDD 2 1
AAA 3 1
BBB 3 1
XXX 3 1
So, the result should be
MONTH TOTAL_BONUS
1 2
2 2
3 1
Month 1, employee AAA and BBB receive the bonus (so the result is 2)
Month 2, employee CCC and DD receive bonus (AAA already received across the year), so the result is 2
Month 3, only employee XXX has received bonus, because AAA and BBB has already received it across the year
A double aggregation solves your problem:
select month, count(1) as total_bonus
from (
select employee_name, min(month) as month
from table_like_this
where bonus_received = 1
group by employee_name
)
group by month;
First, for each employee you find the first month he/she received a bonus. Then, you count the number of employees per all "first bonus-received-month found".
U CAN ALSO USE RANK()
SELECT MONTH
,COUNT(BONUS) AS BONUS
FROM (
SELECT EMPLOYEE
,MONTH
,BONUS
,RANK() OVER (
PARTITION BY EMPLOYEE ORDER BY MONTH
) AS RN
FROM TBTEST
)
WHERE RN = 1
GROUP BY MONTH
You could use ROW_NUMBER() analytic function.
For example,
Setup
CREATE TABLE t
(EMPLOYEE_NAME varchar2(3), MONTH number, BONUS_RECEIVED number);
INSERT ALL
INTO t (EMPLOYEE_NAME, MONTH, BONUS_RECEIVED)
VALUES ('AAA', 1, 1)
INTO t (EMPLOYEE_NAME, MONTH, BONUS_RECEIVED)
VALUES ('BBB', 1, 1)
INTO t (EMPLOYEE_NAME, MONTH, BONUS_RECEIVED)
VALUES ('CCC', 2, 1)
INTO t (EMPLOYEE_NAME, MONTH, BONUS_RECEIVED)
VALUES ('AAA', 2, 1)
INTO t (EMPLOYEE_NAME, MONTH, BONUS_RECEIVED)
VALUES ('DDD', 2, 1)
INTO t (EMPLOYEE_NAME, MONTH, BONUS_RECEIVED)
VALUES ('AAA', 3, 1)
INTO t (EMPLOYEE_NAME, MONTH, BONUS_RECEIVED)
VALUES ('BBB', 3, 1)
INTO t (EMPLOYEE_NAME, MONTH, BONUS_RECEIVED)
VALUES ('XXX', 3, 1)
SELECT * FROM dual;
Query
SQL> SELECT MONTH,
2 COUNT(rn) total_bonus
3 FROM
4 (SELECT t.*,
5 row_number() OVER(PARTITION BY employee_name ORDER BY MONTH) rn
6 FROM t
7 WHERE BONUS_RECEIVED = 1
8 )
9 WHERE rn = 1
10 GROUP BY MONTH;
MONTH TOTAL_BONUS
---------- -----------
1 2
2 2
3 1