How to create BigQuery this query in retail dataset - sql

I have a table with user retail transactions. It includes sales and cancels. If Qty is positive - it sells, if negative - cancels. I want to attach cancels to the most appropriate sell. So, I have tables likes that:
| CustomerId | StockId | Qty | Date |
|--------------+-----------+-------+------------|
| 1 | 100 | 50 | 2020-01-01 |
| 1 | 100 | -10 | 2020-01-10 |
| 1 | 100 | 60 | 2020-02-10 |
| 1 | 100 | -20 | 2020-02-10 |
| 1 | 100 | 200 | 2020-03-01 |
| 1 | 100 | 10 | 2020-03-05 |
| 1 | 100 | -90 | 2020-03-10 |
User with ID 1 has the following actions: buy 50 -> return 10 -> buy 60 -> return 20 -> buy 200 -> buy 10 - return 90. For each cancel row (with negative Qty) I find the previous row (by Date) with positive Qty and greater than cancel Qty.
So I need to create BigQuery queries to create table likes this:
| CustomerId | StockId | Qty | Date | CancelQty |
|--------------+-----------+-------+------------+-------------|
| 1 | 100 | 50 | 2020-01-01 | -10 |
| 1 | 100 | 60 | 2020-02-10 | -20 |
| 1 | 100 | 200 | 2020-03-01 | -90 |
| 1 | 100 | 10 | 2020-03-05 | 0 |
Does anybody help me with these queries? I have created one candidate query (split cancel and sales, join them, and do some staff for removing), but it works incorrectly in the above case.
I use BigQuery, so any BQ SQL features could be applied.
Any ideas will be helpful.

You can use the following query.
;WITH result AS (
select t1.*,t2.Qty as cQty,t2.Date as Date_t2 from
(select *,ROW_NUMBER() OVER (ORDER BY qty DESC) AS [ROW NUMBER] from Test) t1
join
(select *,ROW_NUMBER() OVER (ORDER BY qty) AS [ROW NUMBER] from Test) t2
on t1.[ROW NUMBER] = t2.[ROW NUMBER]
)
select CustomerId,StockId,Qty,Date,ISNULL(cQty, 0) As CancelQty,Date_t2
from (select CustomerId,StockId,Qty,Date,case
when cQty < 0 then cQty
else NULL
end AS cQty,
case
when cQty < 0 then Date_t2
else NULL
end AS Date_t2 from result) t
where qty > 0
order by cQty desc
result: https://dbfiddle.uk

You can do this as a gaps-and-islands problem. Basically, add a grouping column to the rows based on a cumulative reverse count of negative values. Then within each group, choose the first row where the sum is positive. So:
select t.* (except cancelqty, grp),
(case when min(case when cancelqty + qty >= 0 then date end) over (partition by customerid grp) = date
then cancelqty
else 0
end) as cancelqty
from (select t.*,
min(cancelqty) over (partition by customerid, grp) as cancelqty
from (select t.*,
countif(qty < 0) over (partition by customerid order by date desc) as grp
from transactions t
) t
from t
) t;
Note: This works for the data you have provided. However, there may be complicated scenarios where this does not work. In fact, I don't think there is a simple optimal solution assuming that the returns are not connected to the original sales. I would suggest that you fix the data model so you record where the returns come from.

The below query seems to satisfy the conditions and the output mentioned.The solution is based on mapping the base table (t) and having the corresponding canceled qty row alongside from same table(t1)
First, a self join based on the customer and StockId is done since they need to correspond to the same customer and product.
Additionally, we are bringing in the canceled transactions t1 that happened after the base row in table t t.Dt<=t1.Dt and to ensure this is a negative qty t1.Qty<0 clause is added
Further we cannot attribute the canceled qty if they are less than the Original Qty. Therefore I am checking if the positive is greater than the canceled qty. This is done by adding a '-' sign to the cancel qty so that they can be compared easily. -(t1.Qty)<=t.Qty
After the Join, we are interested only in the positive qty, so adding a where clause to filter the other rows from the base table t with canceled quantities t.Qty>0.
Now we have the table joined to every other canceled qty row which is less than the transaction date. For example, the Qty 50 can have all the canceled qty mapped to it but we are interested only in the immediate one came after. So we first group all the base quantity values and then choose the date of the canceled Qty that came in first in the Having clause condition HAVING IFNULL(t1.dt, '0')=MIN(IFNULL(t1.dt, '0'))
Finally we get the rows we need and we can exclude the last column if required using an outer select query
SELECT t.CustomerId,t.StockId,t.Qty,t.Dt,IFNULL(t1.Qty, 0) CancelQty
,t1.dt dt_t1
FROM tbl t
LEFT JOIN tbl t1 ON t.CustomerId=t1.CustomerId AND
t.StockId=t1.StockId
AND t.Dt<=t1.Dt AND t1.Qty<0 AND -(t1.Qty)<=t.Qty
WHERE t.Qty>0
GROUP BY 1,2,3,4
HAVING IFNULL(t1.dt, '0')=MIN(IFNULL(t1.dt, '0'))
ORDER BY 1,2,4,3
fiddle

Consider below approach
with sales as (
select * from `project.dataset.table` where Qty > 0
), cancels as (
select * from `project.dataset.table` where Qty < 0
)
select any_value(s).*,
ifnull(array_agg(c.Qty order by c.Date limit 1)[offset(0)], 0) as CancelQty
from sales s
left join cancels c
on s.CustomerId = c.CustomerId
and s.StockId = c.StockId
and s.Date <= c.Date
and s.Qty > abs(c.Qty)
group by format('%t', s)
if applied to sample data in your question - output is

Related

SQL logic to determine unsold inventory and corresponding available dates (Available to sell)

I am looking for advice on how to generate SQL to be used in SQL Server that will show available inventory to sell and the corresponding date that said inventory will be available. I am easily able to determine if we have inventory that is available immediately but can't wrap my head around what logic would be needed to determine future available quantities.
In the below table. The +/- column represents the weekly inbound vs outbound and the quantity available is a rolling SUM OVER PARTITION BY of the +/- column. I was able to get the immediate quantity available through this simple logic:
Case when Min(X.Qty_Available) > 0 Then Min(X.Qty_Available) else 0 END
AS Immediate_available_Qty
Table:
+-------------+---------------+---------------+------+---------------+
| Item Number | Item Name | week_end_date | +/- | Qty_Available |
+-------------+---------------+---------------+------+---------------+
| 123456 | Fidget Widget | 7/13/2019 | 117 | 117 |
| 123456 | Fidget Widget | 7/20/2019 | 49 | 166 |
| 123456 | Fidget Widget | 7/27/2019 | -7 | 159 |
| 123456 | Fidget Widget | 8/3/2019 | -12 | 147 |
| 123456 | Fidget Widget | 8/10/2019 | -1 | 146 |
| 123456 | Fidget Widget | 8/17/2019 | 45 | 191 |
| 123456 | Fidget Widget | 8/24/2019 | -1 | 190 |
| 123456 | Fidget Widget | 8/31/2019 | -1 | 189 |
| 123456 | Fidget Widget | 9/7/2019 | 50 | 239 |
+-------------+---------------+---------------+------+---------------+
My desired results of this query would be as follows:
+-----------+-----+
| Output | Qty |
+-----------+-----+
| 7/13/2019 | 117 |
| 7/20/2019 | 29 |
| 8/17/2019 | 43 |
+-----------+-----+
the second availability is determined by taking the first available quantity of 117 out of each line in Qty_Available column and finding the new minimum. If the new min is Zero, find the next continuously positive string of data (that runs all the way to the end of the data). Repeat for the third_available quantity and then stop.
I was on the thought train of pursuing RCTE logic but don't want to dive into that rabbit hole if there is a better way to tackle this issue and I'm not even sure the RCTE work for this problem?
This should return your expected result:
SELECT Item_Number, Min(week_end_date), Sum("+/-")
FROM
(
SELECT *
-- put a positive value plus all following negative values in the same group
-- using a Cumulative Sum over 0/1
,Sum(CASE WHEN "+/-" > 0 THEN 1 ELSE 0 end)
Over (PARTITION BY Item_Number
ORDER BY week_end_date
ROWS UNBOUNDED PRECEDING) AS grp
FROM my_table
) AS dt
WHERE grp <= 3 -- only the 1st 3 groups
GROUP BY Item_Number, grp
So here's what I came up with. I know this is poor, I didn't want to leave this thread high and dry and maybe I can get more insight on a better path. Please know that I've never had any real training so I don't know what I don't know.
I ended up running this into a temp table and altering the commented out section in table "A". then re-running that into a temp table.
Select
F.Upc,
F.name,
F.Week_end_date as First_Available_Date,
E.Qty_Available_1
From
(
Select Distinct
D.Upc,
D.name,
Case When Min(D.Rolling_Qty_Available) Over ( PARTITION BY D.upc) < 1 then 0 else
Min(D.Rolling_Qty_Available) Over ( PARTITION BY D.upc) END as Qty_Available_1,
Case When Max(D.Look_up_Ref) Over ( PARTITION BY D.upc) = 0 then '-1000' else
Max(D.Look_up_Ref) Over ( PARTITION BY D.upc) END as Look_up_Ref_1
From
(
Select
A.Upc,
A.name,
A.Week_end_Date,
A.Rolling_Qty_Available,
CASE WHEN
C.Max_Row = A.Row_num and A.[Rolling_Qty_Available] >1 THEN 1
ELSE
CASE WHEN
Sum(A.Calc_Row_Thing) OVER (Partition by A.UPC Order by A.Row_Num DESC
ROWS BETWEEN UNBOUNDED PRECEDING
AND Current ROW
) = (C.Max_Row - A.Row_num + 1)
THEN
C.Max_Row - A.Row_num + 1
ELSE 0 END
END as Look_up_Ref
FROM (
Select
G.Upc,
G.Name,
G.Week_End_Date,
G.Row_num,
G.Calc_Row_Thing,
G.Rolling_Qty_Available
--CASE When (G.Rolling_Qty_Available -
--isnull(H.Qty_Available_1,0)) > 0 then 1 else - 0 END as
--Calc_Row_Thing,
From [dbo].[ATS_item_detail_USA_vw] as G
--Left Join [dbo].[tmp_ats_usa_qty_1] as H on G.upc = H.upc
) AS A --Need to subtract QTY 1 out of here and below
join (
SELECT
B.upc,
Max(Row_num) AS Max_Row
FROM [dbo].[ATS_item_detail_USA_vw] AS B
GROUP BY B.upc
) as C on A.upc = C.upc
) as D
GROUP BY
D.Upc,
D.name,
D.Rolling_Qty_Available,
D.Look_up_Ref
HAVING Max(D.Look_up_Ref) > 1
) as E
Left join
(
SELECT
A.Upc,
A.name,
A.Week_end_Date,
A.Rolling_Qty_Available,
CASE WHEN
C.Max_Row = A.Row_num and A.[Rolling_Qty_Available] >1 THEN 1
ELSE
CASE WHEN
Sum(A.Calc_Row_Thing) OVER (Partition by A.UPC Order by A.Row_Num DESC
ROWS BETWEEN UNBOUNDED PRECEDING
AND Current ROW
) = (C.Max_Row - A.Row_num + 1)
THEN
C.Max_Row - A.Row_num + 1
ELSE 0 END
END as Look_up_Ref
From (
Select
G.Upc,
G.Name,
G.Week_End_Date,
G.Row_num,
G.Calc_Row_Thing,
G.Rolling_Qty_Available
--CASE When (G.Rolling_Qty_Available -
--isnull(H.Qty_Available_1,0)) > 0 then 1 else - 0 END as
--Calc_Row_Thing,
From [dbo].[ATS_item_detail_USA_vw] as G
--Left Join [dbo].[tmp_ats_usa_qty_1] as H on G.upc = H.upc
) as A --subtract qty_1 out the start qty 2 calc
join (
SELECT
B.upc,
Max(Row_num) as Max_Row
FROM [dbo].[ATS_item_detail_USA_vw] as B
GROUP BY B.upc
) AS C ON A.upc = C.upc
) AS F ON E.upc = F.upc and E.Look_up_Ref_1 = F.Look_up_Ref

Select only 1 payment from a table with customers with multiple payments

I have a table called "payments" where I store all the payments of my costumers and I need to do a select to calculate the non-payment rate in a given month.
The costumers can have multiples payments in that month, but I should count him only once: 1 if any of the payments is done and 0 if any of the payment was made.
Example:
+----+------------+--------+
| ID | DATEDUE | AMOUNT |
+----+------------+--------+
| 1 | 2016-11-01 | 0 |
| 1 | 2016-11-15 | 20.00 |
| 2 | 2016-11-10 | 0 |
+----+------------+--------+
The result I expect is from the rate of november:
+----+------------+--------+
| ID | DATEDUE | AMOUNT |
+----+------------+--------+
| 1 | 2016-11-15 | 20.00 |
| 2 | 2016-11-10 | 0 |
+----+------------+--------+
So the rate will be 50%.
But if the select is:
SELECT * FROM payment WHERE DATEDUE BETWEEN '2016-11-01' AND '2016-11-30'
It will return me 3 rows and the rate will be 66%, witch is wrong. Ideas?
PS: This is a simpler example of the real table. The real query have a lot of columns, subselects, etc.
It sounds like you need to partition your results per customer.
SELECT TOP 1 WITH TIES
ID,
DATEDUE,
AMOUNT
ORDER BY ROW_NUMBER() OVER (PARTITION BY ID ORDER BY AMOUNT DESC)
WHERE DATEDUE BETWEEN '2016-11-01' AND '2016-11-30'
PS: The BETWEEN operator is frowned upon by some people. For clarity it might be better to avoid it:
What do BETWEEN and the devil have in common?
Try this
SELECT
id
, SUM(AMOUNT) AS AMOUNT
FROM
Payment
GROUP BY
id;
This might help if you want other columns.
WITH cte (
SELECT
id
, ROW_NUMBER() OVER (PARTITION BY ID ORDER BY AMOUNT DESC ) AS RowNum
-- other row
)
SELECT *
FROM
cte
WHERE
RowNum = 1;
To calculate the rate, you can use explicit division:
select 1 - count(distinct case when amount > 0 then id end) / count(*)
from payment
where . . .;
Or, in a way that is perhaps easier to follow:
select avg(flag * 1.0)
from (select id, (case when max(amount) > 0 then 0 else 1 end) as flag
from payment
where . . .
group by id
) i

How to return all rows with MAX value meeting a condition of another field in SQL?

I have the following costs table:
+--------+------+-----------+
| Year | ID | Amount |
+--------+------+-----------+
| 1960 | 1 | 100 |
| 1960 | 2 | 200 |
| 1960 | 3 | 200 |
| 1960 | 4 | 150 |
| 1961 | 1 | 300 |
| 1961 | 2 | 200 |
| 1961 | 3 | 100 |
| 1961 | 4 | 300 |
+---------+------+----------+
I want all ID’s having the MAX Amount by Year. For example, for 1960, I want rows with ID's 2 and 3. For 1961, I want rows with ID's 1 and 4.
SELECT Year, ID, Amount FROM costs WHERE Amount = (SELECT MAX(Amount) FROM costs);
The above gets me all MAX values across all Years. But I want a condition that only gets me the max Amount values per year. How do I add an condition to only select records with Year = 1960?
Please try this with below query.This is tested. Its working fine.
By clicking on the below link you can see your expected result in live which you want.
SQL Fiddle Live Demo
SELECT
t1.*
FROM
costs t1
WHERE
t1.amount = (
SELECT
MAX(t2.amount)
FROM
costs t2
WHERE
t2. `year` = t1. `year`
);
Try this....It should work
SELECT
*
FROM
costs
WHERE
(YEAR, amount) IN (
SELECT
YEAR,
max(amount)
FROM
costs
GROUP BY
YEAR
);
One option which should run on all major databases is to use a subquery which finds the max amounts for each year to select the records you want:
SELECT c1.*
FROM costs c1
INNER JOIN
(
SELECT Year, MAX(Amount) AS MaxAmount
FROM costs
GROUP BY Year
) c2
ON c1.Year = c2.Year AND
c1.Amount = c2.MaxAmount
Another way to do this would be to use a correlated subquery:
SELECT c1.*
FROM costs c1
WHERE c1.Amount = (SELECT MAX(c2.Amount) FROM costs c2 WHERE c2.Year = c1.Year)
I expect that joining (the first option) would be the fastest method for larger tables, especially if you have proper indices would could be used.
SELECT Year , ID , Amount
FROM #Table T1
JOIN
(
SELECT MAX(Amount) Amount,Year
FROM #Table
GROUP BY Year
) A ON A.Year = T1.Year AND A.Amount = T1.Amount

SQL Aggregate Sum to Only Net Out Negative Rows

I'm trying to roll up product values based on dates. The example below starts out with 20,000, adds 5,000, and then subtracts 7,000. The result should be eating through the entire 5,000 and then into the prior positive row. This would remove the 5,000 row.
I think this would be as simple as doing a sum window function ordered by date descending. However, as you can see below, I want to stop summing at any row that remains positive and then move to the next.
I cannot figure out the logic in SQL to make this work. In my head, it should be:
SUM(Value) OVER (PARTITION BY Product, (positive valued rows) ORDER BY Date DESC)
But there could be multiple positive valued rows in a row where a negative valued row could eat through all of them, or there could be multiple negative values in a row.
This post seemed promising, but I don't think the logic would work for if a negative value would be larger than the positive value.
HAVE:
+------------+----------------+-------+
| Date | Product | Value |
+------------+----------------+-------+
| 01/13/2015 | Prod1 | 20000 |
| 08/13/2015 | Prod1Addition1 | 5000 |
| 12/13/2015 | Prod1Removal | -7000 |
| 02/13/2016 | Prod1Addition2 | 2000 |
| 03/13/2016 | Prod1Addition3 | 1000 |
| 04/13/2016 | Prod1Removal | -1500 |
+------------+----------------+-------+
WANT:
+------------+----------------+-------+
| Date | Product | Value |
+------------+----------------+-------+
| 01/13/2015 | Prod1 | 18000 |
| 02/13/2016 | Prod1Addition2 | 1500 |
+------------+----------------+-------+
i can only think of a recursive cte solution
; with
cte as
(
select Date, Product, Value, rn = row_number() over (order by Date)
from yourtable
),
rcte as
(
select Date, Product, Value, rn, grp = 1
from cte
where rn = 1
union all
select Date = case when r.Value < 0 then c.Date else r.Date end,
Product = case when r.Value < 0 then c.Product else r.Product end,
c.Value,
c.rn,
grp = case when r.Value < 0 then r.grp + 1 else r.grp end
from rcte r
inner join cte c on r.rn = c.rn - 1
)
select Date, Product, Value = sum(Value)
from rcte
group by Date, Product, grp
order by Date
I think that you want this:
select Date,
Product,
Sum(Value) As Value
From TABLE_NAME
Group By Date, Product
Order by Date, Product;
thats correct?

Stock calculation in Postgres

I have a table p1 with transactions in Postgres like this:
| id | product_id | transaction_date | quantity |
|----|------------|------------------|----------|
| 1 | 1 | 2015-01-01 | 1 |
| 2 | 1 | 2015-01-02 | 2 |
| 3 | 1 | 2015-01-03 | 3 |
and p2 table with products like this:
| id | product | stock |
|----|--------------|-------|
| 1 | Product A | 15 |
stock in p2' has been be reduced for every new record in p1.
How to reconstruct previous states to get this result?
| product | first_stock | quantity | last_stock |
|-----------|-------------|----------|------------|
| Product A | 21 | 1 | 20 |
| Product A | 20 | 2 | 18 |
| Product A | 18 | 3 | 15 |
I have tried using lead() to get the quantity after the current row.
SELECT p2.product, p1.quantity, lead(p1.quantity) OVER(ORDER BY p1.id DESC)
FROM p1 INNER JOIN p2 ON p1.product_id = p2.id;
But how to calculate leading rows from the current stock?
You don't just need lead() you need the running sum over all rows in between to reconstruct previous states from transaction data:
SELECT p2.product
, p2.stock + px.sum_quantity AS first_stock
, px.quantity
, p2.stock + px.sum_quantity - quantity AS last_stock
FROM p2
JOIN (
SELECT product_id, quantity, transaction_date
, sum(quantity) OVER (PARTITION BY product_id
ORDER BY transaction_date DESC) AS sum_quantity
FROM p1
) px ON px.product_id = p2.id
ORDER BY px.transaction_date;
Assuming the course of events actually indicated by transaction_date.
Use the aggregate function sum() as window-aggregate function to get the running sum. Use a subquery, since we use the running sum of quantities (sum_quantity) multiple times.
For last_stock subtract quantity of the current row (after adding it redundantly).
Nitpick
Theoretically, it would be cheaper to use a custom frame definition for the window frame to only sum quantities up to preceding row, so we don't add and subtract the quantity of the current row redundantly. But that's more complex and hardly faster in reality:
SELECT p2.id, p2.product, px.transaction_date -- plus id and date for context
, p2.stock + COALESCE(px.pre_sum_q + px.quantity, 0) AS first_stock
, px.quantity
, p2.stock + COALESCE(px.pre_sum_q, 0) AS last_stock
FROM p2
LEFT JOIN (
SELECT id, product_id, transaction_date
, quantity
, sum(quantity) OVER (PARTITION BY product_id
ORDER BY transaction_date DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) AS pre_sum_q
FROM p1
) px ON px.product_id = p2.id
ORDER BY px.transaction_date, px.id;
Explanation for the frame definition in this related answer:
Grouping based on sequence of rows
While being at it, also demonstrating how to prevent missing rows and NUll values with LEFT JOIN and COALESCE for products that don't have any related rows in p1, and a stable sort order if there are multiple transactions for the same product on the same day.
Still assuming all columns to be defined NOT NULL, or you need to do some more for corner cases with NULL values.