How to distribute sales with partitions - sql

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;

Related

How To Calculate Running balance using SQL

If I have total qty = 100. and it has been shipped in 4 phases line 40, 10, 25, 25 that equals to 100. when I am running this query:
Someone Helped me with this query. I want the same runnable for DB2.
SET totalQty = -1;
SELECT
IF(#totalQty<0, pl.quantity, #totalQty) AS totalQty,
pr.invoiceqty,
#totalQty:=(#totalQty - pr.invoiceqty) AS balance
FROM
purchaseorderline pl, replenishmentrequisition pr
I am getting result like this :
--total qty-- --invoice qty-- --balance qty--
100 40 60
100 10 90
100 25 75
100 25 70
The result I want :
--total qty-- --invoice qty-- --balance qty--
100 40 60
60 10 50
50 25 25
25 25 00
It would be good enough, if you provided some sample data in a table form and not just what you get on it.
WITH MYTAB (PHASE_ID, QTY) AS
(
-- Your initial data as the result of
-- your base SELECT statement
VALUES
(1, 40)
, (2, 10)
, (3, 25)
, (4, 25)
)
SELECT
QTY + QTY_TOT - QTY_RTOT AS "total qty"
, QTY AS "invoice qty"
, QTY_TOT - QTY_RTOT AS "balance qty"
FROM
(
SELECT
PHASE_ID
, QTY
-- Running total sum
, SUM (QTY) OVER (ORDER BY PHASE_ID) AS QTY_RTOT
-- Total sum
, SUM (QTY) OVER () AS QTY_TOT
FROM MYTAB
)
ORDER BY PHASE_ID
total qty
invoice qty
balance qty
100
40
60
60
10
50
50
25
25
25
25
0
A variation of Marks answer is:
WITH MYTAB (PHASE_ID, QTY) AS
(
-- Your initial data as the result of
-- your base SELECT statement
VALUES (1, 40)
, (2, 10)
, (3, 25)
, (4, 25)
)
SELECT QTY_TOT AS "total qty"
, QTY AS "invoice qty"
, coalesce(lead(QTY_TOT) over (order by phase_id),0) AS "balance qty"
FROM
( SELECT PHASE_ID
, QTY
-- Running total sum
, SUM (QTY) OVER (ORDER BY PHASE_ID desc) AS qty_tot
FROM MYTAB
)
ORDER BY PHASE_ID
It uses lead at the outer level instead of sum over the entire window at the inner level
Fiddle

Generate billing balance depending on the number of payments made

Below is the table I have created and inserted values in it:
CREATE TABLE Invoices
(
InvID int,
InvAmount int
)
GO
INSERT INTO Invoices
VALUES (1, 543), (2, 749)
CREATE TABLE payments
(
PayID int IDENTITY (1, 1),
InvID int,
PayAmount int,
PayDate date
)
INSERT INTO payments
VALUES (1, 20, '2016-01-01'),
(1, 35, '2016-01-07'),
(1, 78, '2016-01-13'),
(1, 52, '2016-01-25'),
(2, 40, '2016-01-03'),
(2, 54, '2016-01-15'),
(2, 63, '2016-01-17'),
(2, 59, '2016-01-28')
SELECT * FROM Invoices
SELECT * FROM payments
As shown in the screenshot above, the Invoice table specifies various customer billings (the first billing totals 543, the second billing totals 749).
As shown in the screenshot above, the payments table specifies the various payments the customer made for each of the billings. For example, one can see that on January 1st the customer paid 20 USD out of billing no. 1 (which totals 543 USD), and on January 3rd the customer paid 40 USD out of billing no. 2, (which totals 749 USD).
Now the question is:
Write a query that displays the billing balance, based on the number of payments made so far.
The query result should exactly look like the screenshot below:
This is what I have tried:
SELECT
payments.InvID,
InvAmount - SUM(PayAmount) OVER (PARTITION BY payments.InvID ORDER BY PayID
ROWS BETWEEN UNBOUNDED PRECEDING AND 0 FOLLOWING) AS 'InvAmount',
PayDate, PayAmount,
InvAmount - SUM(PayAmount) OVER (PARTITION BY payments.InvID ORDER BY PayID) AS 'Balance'
FROM
Invoices
JOIN
payments ON payments.InvID = Invoices.InvID
After running the query, I got the following result which is shown below:
As you can see from the screenshot above, I nearly got the result I wanted.
The only problem is that InvAmount is exactly returning the same row values as Balance. I am not able to retain the starting row values of InvAmount which are 543 (InvID = 1) and 749 (InvID = 2) respectively.
How can this issue be solved?
You can add back the PayAmount in the calculation
InvAmount
+ PayAmount
- SUM(PayAmount) OVER (PARTITION BY payments.InvID
ORDER BY PayID
ROWS BETWEEN UNBOUNDED PRECEDING
AND 0 FOLLOWING) AS InvAmount
Or use BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING. But you need to handle NULL value for the very first row
InvAmount
- ISNULL(SUM(PayAmount) OVER (PARTITION BY payments.InvID
ORDER BY PayID
ROWS BETWEEN UNBOUNDED PRECEDING
AND 1 PRECEDING), 0) AS InvAmount
db>fiddle demo
You can use PARTITION BY clause for invoice identifier and then deduct the pay amount from the invoice amount as given below
;WITH CTE_Balance as
(
SELECT i.InvID, i.InvAmount, p.PayAmount, p.PayDate
, InvAmount - sum(payamount) over (partition by p.invid order by paydate rows between unbounded preceding and current row) as balance
, ROW_NUMBER() over(partition by p.invid order by p.paydate) as rnk
FROM payments as p
inner join Invoices as i
on i.InvID = p.InvID
)
SELECT invid, case when rnk =1 then invamount else lag(balance) over(partition by invid order by paydate) end as invamount
,payAmount, paydate, balance
FROM CTE_Balance
invid
invamount
payAmount
paydate
balance
1
543
20
2016-01-01
523
1
523
35
2016-01-07
488
1
488
78
2016-01-13
410
1
410
52
2016-01-25
358
2
749
40
2016-01-03
709
2
709
54
2016-01-15
655
2
655
63
2016-01-17
592
2
592
59
2016-01-28
533

Subquery Factoring recursive 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.

Table with daily historical stock prices. How to pull stocks where the price reached a certain number for the first time

I have a table with historical stocks prices for hundreds of stocks. I need to extract only those stocks that reached $10 or greater for the first time.
Stock
Price
Date
AAA
9
2021-10-01
AAA
10
2021-10-02
AAA
8
2021-10-03
AAA
10
2021-10-04
BBB
9
2021-10-01
BBB
11
2021-10-02
BBB
12
2021-10-03
Is there a way to count how many times each stock hit >= 10 in order to pull only those where count = 1 (in this case it would be stock BBB considering it never reached 10 in the past)?
Since I couldn't figure how to create count I've tried the below manipulations with min/max dates but this looks like a bit awkward approach. Any idea of a simpler solution?
with query1 as (
select Stock, min(date) as min_greater10_dt
from t
where Price >= 10
group by Stock
), query2 as (
select Stock, max(date) as max_greater10_dt
from t
where Price >= 10
group by Stock
)
select Stock
from t a
join query1 b on b.Stock = a.Stock
join query2 c on c.Stock = a.Stock
where not(a.Price < 10 and a.Date between b.min_greater10_dt and c.max_greater10_dt)
This is a type of gaps-and-islands problem which can be solved as follows:
detect the change from < 10 to >= 10 using a lagged price
count the number of such changes
filter in only stock where this has happened exactly once
and take the first row since you only want the stock (you could group by here but a row number allows you to select the entire row should you wish to).
declare #Table table (Stock varchar(3), Price money, [Date] date);
insert into #Table (Stock, Price, [Date])
values
('AAA', 9, '2021-10-01'),
('AAA', 10, '2021-10-02'),
('AAA', 8, '2021-10-03'),
('AAA', 10, '2021-10-04'),
('BBB', 9, '2021-10-01'),
('BBB', 11, '2021-10-02'),
('BBB', 12, '2021-10-03');
with cte1 as (
select Stock, Price, [Date]
, row_number() over (partition by Stock, case when Price >= 10 then 1 else 0 end order by [Date] asc) rn
, lag(Price,1,0) over (partition by Stock order by [Date] asc) LaggedStock
from #Table
), cte2 as (
select Stock, Price, [Date], rn, LaggedStock
, sum(case when Price >= 10 and LaggedStock < 10 then 1 else 0 end) over (partition by Stock) StockOver10
from cte1
)
select Stock
--, Price, [Date], rn, LaggedStock, StockOver10 -- debug
from cte2
where Price >= 10
and StockOver10 = 1 and rn = 1;
Returns:
Stock
BBB
Note: providing DDL+DML as show above makes it much easier of people to assist.

Get value based on date

I am trying to get the price point of a product based on future pricing. I can't really do it by Max (Expiration Date) since the price points are different. And I also can't do a Max(Price) since the high price might be the one expiring or it could be the new one. My data would look like this:
Supplier
Product
Price
Effective Date
Expiration Date
Supplier 1
A
800
04-01-2121
12-31-2023
Supplier 1
A
1000
01-01-2121
03-31-2023
Supplier 1
B
500
04-01-2121
12-31-2023
Supplier 1
B
400
01-01-2121
03-31-2023
Supplier 2
D
200
01-01-2121
12-31-2023
Supplier 2
C
600
01-01-2121
12-31-2023
The result I am trying to get is below:
Supplier
Product
Price
Effective Date
Expiration Date
Supplier 1
A
800
04-01-2121
12-31-2023
Supplier 1
B
500
04-01-2121
12-31-2023
Supplier 2
D
200
01-01-2121
12-31-2023
Supplier 2
C
600
01-01-2121
12-31-2023
Any ideas?
To have the product information when expirationdate is the greatest we can product wise group the rows in descending order of expirationdate and find the row with lowest serial number for each product or we can simply use subquery to select the information where expirationdate=max(expirationdate) within the product group.
First approach will be more efficient. But if your dbms doesn't support row_number() then you can use second approach.
Schema:
create table mydata(Supplier varchar(30),Product varchar(30), Price int,EffectiveDate date, ExpirationDate date);
insert into mydata values('Supplier 1', 'A', 800 ,'04-01-2121', '12-31-2023');
insert into mydata values('Supplier 1', 'A', 1000 ,'01-01-2121', '03-31-2023');
insert into mydata values('Supplier 1', 'B', 500 ,'04-01-2121', '12-31-2023');
insert into mydata values('Supplier 1', 'B', 400 ,'01-01-2121', '03-31-2023');
insert into mydata values('Supplier 2', 'D', 200 ,'01-01-2121', '12-31-2023');
insert into mydata values('Supplier 2', 'C', 600 ,'01-01-2121', '12-31-2023');
Query#1
WITH cte
AS (SELECT supplier,
product,
price,
effectivedate,
expirationdate,
ROW_NUMBER ()
OVER (PARTITION BY product
ORDER BY expirationdate DESC)
rn
FROM mydata)
SELECT supplier,
product,
price,
effectivedate,
expirationdate
FROM cte
WHERE rn = 1
Output:
supplier
product
price
effectivedate
expirationdate
Supplier 1
A
800
2121-04-01
2023-12-31
Supplier 1
B
500
2121-04-01
2023-12-31
Supplier 2
C
600
2121-01-01
2023-12-31
Supplier 2
D
200
2121-01-01
2023-12-31
Query#2 for older version of DBMS:
SELECT supplier,
product,
price,
effectivedate,
expirationdate
FROM mydata m
where effectivedate =
(select max(effectivedate) from mydata md where m.product=md.product)
Output:
supplier
product
price
effectivedate
expirationdate
Supplier 2
D
200
2121-01-01
2023-12-31
Supplier 2
C
600
2121-01-01
2023-12-31
Supplier 1
B
500
2121-04-01
2023-12-31
Supplier 1
A
800
2121-04-01
2023-12-31
db<>fiddle here
It looks you can accomplish what you need with a simple row_number by numbering the rows decending ordered against your ExpirationDate and filtering for the first row in each group.
select Supplier, Product, Price, EffectiveDate, ExpirationDate from (
select Supplier, Product, Price, EffectiveDate, ExpirationDate, row_number() over (partition by Product order by ExpirationDate desc)Seq
from table
)t
where Seq=1