Build SQL query with JOIN and limits - sql

Help me please build PostgreSQL query.
There are 2 tables: products(id, title) and prices(id, product_id, price_type, moment, value)
moment - timestamp, can be in past or future
Assume that price_type has only two option: retail or purchase
But one product may has many retail prices with different moments.
I need select all products with actual retail and purchase prices, where moment less than now.
It's I can done
SELECT
products.id,
products.title_translations AS title,
retail_prices.moment AS ret_moment,
pur_prices.value AS purchase,
retail_prices.value AS retail
FROM products
LEFT OUTER JOIN prices AS pur_prices ON products.id=pur_prices.product_id AND pur_prices.price_type='purchase' AND pur_prices.moment<current_timestamp
LEFT OUTER JOIN prices AS retail_prices ON products.id=retail_prices.product_id AND retail_prices.price_type='retail' AND retail_prices.moment<current_timestamp
ORDER BY products.id;
It works, but returns
product with all prices, but I need only last prices(by moment).

Just use ROW_NUMBER to find what is the last price before current time
with last_prices as (
SELECT
products.id,
products.title_translations AS title,
prices.moment,
prices.value,
prices.price_type,
ROW_NUMBER() OVER (PARTITION BY product_id, price_type
ORDER BY moment DESC) as rn
FROM products
LEFT JOIN prices
ON products.id = prices.product_id
WHERE moment < now()
)
SELECT id, title,
MAX(CASE WHEN price_type = 'retail'
THEN moment
END) as retail_moment,
MAX(CASE WHEN price_type = 'retail'
THEN value
END) as retail_price,
MAX(CASE WHEN price_type = 'purchase'
THEN moment
END) as purchase_moment,
MAX(CASE WHEN price_type = 'purchase'
THEN value
END) as purchase_price
FROM last_prices
WHERE rn = 1
GROUP BY id, title
ORDER BY id

To keep things organized, and straight in my mind, I'd use CTEs to generate two subsets of price data, one for purchase one for retail and assign a row number in ascending sequence with the lowest number having the most recent moment less than the currenttimestmap. And then when we join to these ctes, we only return the lowest number assigned.
With Pur_prices as (SELECT P.*, row_Number() over (partition by product_ID order by moment desc) RN
FROM prices P
WHERE price_Type = 'purchase'
and p.moment < current_timestamp)
, Retail_prices as (SELECT P.*, row_Number() over (partition by product_ID order by moment desc) RN
FROM prices P
WHERE price_Type = 'retail'
and p.moment < current_timestamp)
SELECT
p.id,
p.title_translations AS title,
rp.moment AS ret_moment,
rp.value AS retail,
pp.moment AS Pur_moment,
pp.value AS purchase
FROM products p
LEFT JOIN pur_prices pp
ON p.id=pp.product_id
AND pp.RN = 1 --Only show the most recent price less than current time
LEFT JOIN retail_prices rp
ON p.id=rp.product_id
AND RP.RN = 1 --Only show the most recent price less than current time
ORDER BY p.id;
The end result should be all products regardless if they have a retail or purchase price; but if they do show the retail/purchase pricing for the most recent moment before now. My only concern is this implies all pricing has a moment they start (no null values allowed!)

You may be wanting it be ordered with respect to moment in descending order.
Change
ORDER BY products.id;
to
ORDER BY product.id ASC, moment DESC;

Related

SQL get top 3 values / bottom 3 values with group by and sum

I am working on a restaurant management system. There I have two tables
order_details(orderId,dishId,createdAt)
dishes(id,name,imageUrl)
My customer wants to see a report top 3 selling items / least selling 3 items by the month
For the moment I did something like this
SELECT
*
FROM
(SELECT
SUM(qty) AS qty,
order_details.dishId,
MONTHNAME(order_details.createdAt) AS mon,
dishes.name,
dishes.imageUrl
FROM
rms.order_details
INNER JOIN dishes ON order_details.dishId = dishes.id
GROUP BY order_details.dishId , MONTHNAME(order_details.createdAt)) t
ORDER BY t.qty
This gives me all the dishes sold count order by qty.
I have to manually filter max 3 records and reject the rest. There should be a SQL way of doing this. How do I do this in SQL?
You would use row_number() for this purpose. You don't specify the database you are using, so I am guessing at the appropriate date functions. I also assume that you mean a month within a year, so you need to take the year into account as well:
SELECT ym.*
FROM (SELECT YEAR(od.CreatedAt) as yyyy,
MONTH(od.createdAt) as mm,
SUM(qty) AS qty,
od.dishId, d.name, d.imageUrl,
ROW_NUMBER() OVER (PARTITION BY YEAR(od.CreatedAt), MONTH(od.createdAt) ORDER BY SUM(qty) DESC) as seqnum_desc,
ROW_NUMBER() OVER (PARTITION BY YEAR(od.CreatedAt), MONTH(od.createdAt) ORDER BY SUM(qty) DESC) as seqnum_asc
FROM rms.order_details od INNER JOIN
dishes d
ON od.dishId = d.id
GROUP BY YEAR(od.CreatedAt), MONTH(od.CreatedAt), od.dishId
) ym
WHERE seqnum_asc <= 3 OR
seqnum_desc <= 3;
Using the above info i used i combination of group by, order by and limit
as shown below. I hope this is what you are looking for
SELECT
t.qty,
t.dishId,
t.month,
d.name,
d.mageUrl
from
(
SELECT
od.dishId,
count(od.dishId) AS 'qty',
date_format(od.createdAt,'%Y-%m') as 'month'
FROM
rms.order_details od
group by date_format(od.createdAt,'%Y-%m'),od.dishId
order by qty desc
limit 3) t
join rms.dishes d on (t.dishId = d.id)

SQL Server query returns incorrect results

I have a database to use in SQL Server. The tables are:
Price (prodID, from, price)
Product (prodID, name, quantity)
PO (prodID, orderID, amount)
Order (orderID, date, address, status, trackingNumber, custID, shipID)
Shipping (shipID, company, time, price)
Customer (custID, name)
Address (addrID, custID, address)
I am trying to find the total price of each order (which is the total price of each product ordered by the date attribute in the Order table plus the total shipping price for that order), but the catch is that each product has multiple prices associated with it depending on the date it was ordered aka the from attribute in the Price table.
My current query is this:
select o.orderID, sum(p.price+s.price) as total_cost
from [Order] as o
inner join PO as po on o.orderId = po.orderId
inner join Price as p on p.prodId = po.prodId
inner join Shipping as s on o.shipId = s.shipId
group by o.orderId
order by o.orderId;
This query returns the correct orderIDs but returns higher than expected total_cost
I know that I need subqueries to extract the correct prices for each product but I am unsure of where or how to use them in this case. I am also not sure my main query is even close to what I want.
A piece of sample data from my query has orderID 1 having a total cost of $17971.15 while the expected result for ID 1 is $388.68.
My goal is to have the orderID and the total price for each order which also includes the shipping price.
The key issue starts with the price table - from your question as
Price (prodID, from, price)
You really want this to be in the format of a date range e.g., prodID, dateFrom, dateTo, price.
You can do this with a LEAD function (getting the next 'from', and calling it 'dateto')
SELECT prod_ID, [from] as datefrom, LEAD([from],1) OVER (PARTITION BY prodID ORDER BY [from]) AS dateto, [price]
FROM Price
To get the relevant price for a given order date, use the above as a virtual table rather than the price table and find the price as at the relevant date (between the datefrom and dateto, with the link to dateto being a 'less than' rather than 'less than or equal to') e.g.,
select o.orderID, s.price AS Shipping_price, sum(PO.amount * p.price) as item_Price
from [Order] as o
inner join PO as po on o.orderId = po.orderId
inner join Shipping as s on o.shipId = s.shipId
inner join
(SELECT prod_ID, [from] as datefrom, LEAD([from],1) OVER (PARTITION BY prodID ORDER BY [from]) AS dateto, [price]
FROM Price
) as p on p.prodId = po.prodId AND o.[date] >= p.[datefrom] AND o.[date] < [dateto]
group by o.orderId, s.price
order by o.orderId;
Note in the above
Total cost for items is calculated as item price * number of items (not just the base price for the item)
I have separated shipping cost in the above - I imagine that it is by order (rather than by item) whereas your question includes the shipping once per item.
Note that #Dale's comments are important - the above code is completely untested and may have SQL errors etc. As we don't have sample data etc, I cannot run it to ensure it works. But hopefully it sets you on the correct path.
I do suggest you run a logic check over your result when finished e.g., if you order 5 bananas # $1.50 each, with shipping $8, your total cost should be $15.50

Use last value when current row is null , for PostgreSQL timeseries table

I come across a problem that I could not find an optimal solution. So the idea is to get the price at each given time for a list of products from a list of shops but because the price are registered at different time I get some nulls when grouping by time and also an array o values. Therefore it requires to couple of steps in order to obtain what I need. I am wondering if someone know a better, faster way to achieve this. Bellow is my initial PostgreSQL table of course this is just a snippet of it to get the idea:
Initial Table
Desired results (intermediate table and final one)
And bellow is the PostgreSQL sql code that give the result I want but it seems very costly:
SELECT times,
first_value(price_yami_egg) OVER (PARTITION BY partition_price_yami_egg order by time) as price_yami_egg
first_value(price_yami_salt) OVER (PARTITION BY partition_price_yami_salt order by time) as price_yami_salt
first_value(price_dobl_egg) OVER (PARTITION BY partition_price_dobl_egg order by time) as price_dobl_egg
first_value(price_dobl_salt) OVER (PARTITION BY partition_price_dobl_salt order by time) as price_dobl_salt
FROM(
SELECT time,
min(price_yami_egg) as price_yami_egg,
sum(case when min(price_yami_egg) is not null then 1 end) over (order by times) as partition_price_yami_egg
min(price_yami_salt) as price_yami_salt,
sum(case when min(price_yami_salt) is not null then 1 end) over (order by times) as partition_price_yami_salt
min(price_dobl_egg) as price_dobl_egg,
sum(case when min(price_dobl_egg) is not null then 1 end) over (order by times) as partition_price_dobl_egg
min(price_dobl_salt) as price_dobl_salt,
sum(case when min(price_dobl_salt) is not null then 1 end) over (order by times) as partition_price_dobl_salt
FROM (
SELECT "time" AS times,
CASE WHEN shop_name::text = 'yami'::text AND product_name::text = 'egg'::text THEN price END AS price_yami_egg
CASE WHEN shop_name::text = 'yami'::text AND product_name::text = 'salt'::text THEN price END AS price_yami_salt
CASE WHEN shop_name::text = 'dobl'::text AND product_name::text = 'egg'::text THEN price END AS price_dobl_egg
CASE WHEN shop_name::text = 'dobl'::text AND product_name::text = 'salt'::text THEN price END AS price_dobl_salt
FROM shop sh
) S
GROUP BY time
ORDER BY time) SS
Do you just want aggregation?
select time,
min(price) filter (where shop_name = 'Yami' and product_name = 'EGG'),
min(price) filter (where shop_name = 'Yami' and product_name = 'SALT'),
min(price) filter (where shop_name = 'Dobl' and product_name = 'EGG'),
min(price) filter (where shop_name = 'Dobl' and product_name = 'SALT')
from shop s
group by time;
If. your concern is NULL values in the result, then you can fill them in. This is a little tricky, but the idea is:
with t as (
select time,
min(price) filter (where shop_name = 'Yami' and product_name = 'EGG') as yami_egg,
min(price) filter (where shop_name = 'Yami' and product_name = 'SALT') as yami_salt,
min(price) filter (where shop_name = 'Dobl' and product_name = 'EGG') as dobl_egg,
min(price) filter (where shop_name = 'Dobl' and product_name = 'SALT') as dobl_salt
from shop s
group by time
)
select s.*,
max(yaml_egg) over (yaml_egg_grp) as imputed_yaml_egg,
max(yaml_salt) over (yaml_egg_grp) as imputed_yaml_salt,
max(dobl_egg) over (yaml_egg_grp) as imputed_dobl_egg,
max(dobl_salt) over (yaml_egg_grp) as imputed_dobl_salt
from (select s.*,
count(yaml_egg) over (order by time) as yaml_egg_grp,
count(yaml_salt) over (order by time) as yaml_egg_grp,
count(dobl_egg) over (order by time) as dobl_egg_grp,
count(dobl_salt) over (order by time) as dobl_salt_grp
from s
) s

SQLite print out history of daily price changes of a stock

I have to output price change for each stock based on the dates that are held at the table pricedates.
What I have currently come up with is to group prices by id_stock and order by price_date in ASC order, then subtract from every row.
It should be separately done for buy_price and sell_price.
The logic should be like this
stock_id prices history
1 price 1 price 1
price 60 price 60-price1
price 98 price98-price60
2 price 34 price34
price 67 price67-price34
I am really struggling with putting everything together, here is something that I have tried, most probably it's a total mess:
select p.id_price
(select min(pd.price_date) from pricedates pd as po where po.price_date > pd.price_date) as next_id_price,
(po.buy_price - pd.buy_price) buy
(po.sell_price - pd.sell_price) sell
from prices p
left join pricedates pd on pd.id_date=p.id_price_date
group by p.id_stock
order by pd.price_date asc;
If you are running SQLite >= 3.25, you can use window function lag() to access the previous record of the same stock_id:
select
p.stock_id,
p.buy_price,
p.buy_price - lag(p.buy_price, 1, 0)
over(partition by p.stock_id order by pd.price_date) buy_history,
p.sell_price,
p.sell_price - lag(p.sell_price, 1, 0)
over(partition by p.stock_id order by pd.price_date) sell_history
from prices p
inner join pricedates pd
on pd.id_date = p.id_price_date
order by pd.price_date asc;
In earlier vesions of SQLite, where lag() is not available, one solution is to self-join the table with a correlated subquery that locates the previous record:
select
p.stock_id,
p.buy_price,
p.buy_price - coalesce(p1.buy_price, 0) buy_history,
p.sell_price,
p.sell_price - coalesce(p1.sell_price, 0) sell_history
from prices p
inner join pricedates pd
on pd.id_date = p.id_price_date
left join prices p1
on p1.stock_id = p.stock_id
and p1.id_date = (
select pd1.id_date
from pricedates pd1
where pd1.price_date > pd.price_date
order by pd1.price_date
limit 1
)
order by pd.price_date asc;
You would use lag() -- which is available in the more recent versions of SQLite:
select p.*,
(price -
lag(p.price, 1, 0) over (partition by stock_id order by id_price_date)
) as diff
from price p;

SQL Select only products and prices that have changed in price since a specified date

The tables look like this:
tblProducts (around 200k records)
SKU,Title,CategoryID
100,Apple,0
101,Orange,0
102,Carrot,1
tblCategories
CategoryID,CategoryName
0,Fruit
1,Vegetables
tblPrices (around 10m records)
SKU,BuyPrice,SellPrice,Timestamp
100,1,2,2013-1-1 23:04
100,3,6,2013-1-2 19:04
100,4,8,2013-1-3 21:04
100,4,8,2013-1-4 20:04
100,4,8,2013-1-5 22:04
I need to get the current BuyPrice of all products (the most recent one from tblPrices) and compare it to the latest BuyPrice at the time of X days ago from NOW(). I need only the products that changed in BuyPrice.
This is so I can answer the question, 'what products changed in price over the last X days?'. Given the small set of data above, I would get an empty table for 1 days or 2 days, but for 3 days, I would want to retrieve:
SKU,Title,CategoryName,OldBuyPrice,OldSellPrice,NewBuyPrice,NewSellPrice, NBP/OBP
100,Apple,Fruit, 3, 6, 4, 8, 2.00
and for 4 days:
SKU,Title,CategoryName,OldBuyPrice,OldSellPrice,NewBuyPrice,NewSellPrice, NBP/OBP
100,Apple,Fruit, 1, 2, 4, 8, 4.00
I've been searching for similar solutions on the net, but haven't found one. Any ordering is fine. Thanks in advance!
Sure, this is doable. There's a decent windowing-function version, although there may still be better ways to do this:
WITH Current_Price (sku, buyPrice, sellPrice) as
(SELECT sku, buyPrice, sellPrice
FROM (SELECT sku, buyPrice, sellPrice,
ROW_NUMBER() OVER(PARTITION BY sku
ORDER BY timestamp DESC) as rownum
FROM price) t
WHERE rownum = 1),
Price_Back_Previous_Days (sku, buyPrice, sellPrice) as
(SELECT sku, buyPrice, sellPrice
FROM (SELECT sku, buyPrice, sellPrice,
ROW_NUMBER() OVER(PARTITION BY sku
ORDER BY timestamp DESC) as rownum
FROM price
WHERE timestamp < DATEADD(DAY, -3, CONVERT(DATE, GETDATE()))) t
WHERE rownum = 1)
SELECT p.sku, p.title, c.categoryName,
prev.buyPrice as oldBuyPrice, prev.sellPrice as oldSellPrice,
curr.buyPrice as newBuyPrice, curr.sellPrice as newSellPrice,
CASE WHEN prev.buyPrice = 0
THEN curr.buyPrice
ELSE 1.0 * curr.buyPrice / prev.buyPrice END as 'NBP/OBP'
FROM Product p
JOIN Category c
ON c.categoryId = p.categoryId
JOIN Current_Price curr
ON curr.sku = p.sku
JOIN Price_Back_Previous_Days prev
ON prev.sku = p.sku
AND (prev.buyPrice <> curr.buyPrice
OR prev.sellPrice <> curr.sellPrice)
Which yields the expected
SKU TITLE CATEGORYNAME OLDBUYPRICE OLDSELLPRICE NEWBUYPRICE NEWSELLPRICE NBP/OBP
100 Apple Fruit 1 2 4 8 4
(Have a working SQL Fiddle Example, with an specific date substituted for GETDATE() for future-reasons.)
You'll notice that I'm using -3 (and not -4, as might be expected), because I'm attempting to retrieve the same value for 'as of this date', regardless of when (during the day) the query is run. Obviously the 'current price' can still change (although adding a timestamp parameter there as well could fix that); however, this should make sure that you're looking at a consistent price throughout a given day.
Try this: SQL Fiddle
Select
tt.SKU,tt.BuyPrice,tt.SellPrice,tr.BuyPrice As NewBuyPrice,tr.SellPrice As NewSellPrice, tr.BuyPrice/tt.BuyPrice NNBP
From
(
select SKU, Max(Timestamps) timestamps
from t
Group by t.SKU
) t
Join t tr on t.SKU = tr.SKU AND t.timestamps = tr.Timestamps
Join t tt ON t.SKU = tt.SKU
AND DATEDIFF(D, tt.timestamps, t.timestamps) = 4
AND tt.BuyPrice <> tr.BuyPrice
I tried to create the table oldtpc which will get the products which has maximun of date after X number of days. And another newtpc which has prices with most recent date. And in the on condition between 'oldtpc' and and 'newtpc' I am checking that those dates do not match
select tp.SKU, tp.Title, tc.CategoryName, oldtpc.BuyPrice, oldtpc.Sellprice, newtpc.buyprice, newtpc.Sellprice
from tblProducts tp
join tblCategories tc
on tp.CategoryId= tc.CateogryId
join (select SKU, BuyPrice, SellPrice, max(TimeStamp) as TimeStamp
from tblPrices new
where DATEDIFF ( dd, timestamp, getdate()) < #xdays
group by SKU, BuyPrice, SellPrice ) as newtpcnewtpc
on tp.SKU = newtpc .sku
join (select SKU, BuyPrice, SellPrice, max(TimeStamp) as TimeStamp
from tblPrices old
where DATEDIFF ( dd, timestamp, getdate()) >= #xdays
group by SKU, BuyPrice, SellPrice ) as oldtpc
on oldtpc.SKU = tp.SKU and oldtpc.timestamp <> newtpc.timestamp
PS: some syntax might be wrong, but I think the general idea should work fine