PostgreSQL Referencing Outer Query in Subquery - sql

I have two Postgres tables (really, more than that, but simplified for the purpose of the question) - one a record of products that have been ordered by customers, and another a historical record of prices per customer and a date they went into effect. Something like this:
'orders' table
customer_id | timestamp | quantity
------------+---------------------+---------
1 | 2015-09-29 16:01:01 | 5
1 | 2015-10-23 14:33:36 | 3
2 | 2015-10-19 09:43:02 | 7
1 | 2015-11-16 15:08:32 | 2
'prices' table
customer_id | effective_time | price
------------+---------------------+-------
1 | 2015-01-01 00:00:00 | 15.00
1 | 2015-10-01 00:00:00 | 12.00
2 | 2015-01-01 00:00:00 | 14.00
I'm trying to create a query that will return every order and its unit price for that customer at the time of the order, like this:
desired result
customer_id | quantity | price
------------+----------+------
1 | 5 | 15.00
1 | 3 | 12.00
2 | 7 | 14.00
1 | 2 | 12.00
This is essentially what I want, but I know that you can't reference an outer query inside an inner query, and I'm having trouble figuring out how to re-factor:
SELECT
o.customer_id,
o.quantity,
p.price
FROM orders o
INNER JOIN (
SELECT price
FROM prices x
WHERE x.customer_id = o.customer_id
AND x.effective_time <= o.timestamp
ORDER BY x.effective_time DESC
LIMIT 1
) p
;
Can anyone suggest the best way to make this work?

Instead of joining an inline view based on the prices table, you can perform a subquery in the SELECT list:
SELECT customer_id, quantity, (
SELECT price
FROM prices p
WHERE
p.customer_id = o.customer_id
AND p.effective_time <= o.timestamp
ORDER BY p.effective_time DESC
LIMIT 1
) AS price
FROM orders o
That does rely on a correlated subquery, which could be bad for performance, but with the way your data are structured I doubt there's a substantially better alternative.

You dont need the subquery, just a plain inner join will do (this assumes there are no duplicate effective_times per customer):
SELECT o.customer_id, o.quantity
,p.price
FROM orders o
JOIN prices p ON p.customer_id = o.customer_id
AND p.effective_time <= o.timestamp
AND NOT EXISTS ( SELECT * FROM prices nx
WHERE nx.customer_id = o.customer_id
AND nx.effective_time <= o.timestamp
AND nx.effective_time > p.effective_time
)
;

Related

Creating view from item table and price table

Let's say I have two tables. One is the orders table and the other is a price table giving the price of an item w.r.t. the number of items ordered.
The meaning of the test_price table is "up-to count items(inclusive) the cost per item is price". So 0-50 items cost 1.22 per item. 51-100 cost 1.20 per item.
table test_price
id | count | price
----+-------+-------
1 | 50 | 1.22
2 | 100 | 1.20
3 | 150 | 1.19
4 | 200 | 1.18
5 | 300 | 1.10
table test_orders
id | count
----+-------
1 | 12
2 | 50
3 | 65
4 | 155
5 | 400
So this means that order 1 for 12 items should be priced at 1.22 per item.
Order 5 should be priced at 1.10 per item.
I can get the price for a single order with
SELECT price FROM test_prices WHERE
count >= (SELECT count FROM test_orders WHERE id = 1)
ORDER BY count ASC LIMIT 1;
I would like to create a view that shows the orders with unit price and total price as columns
Join the tables on the count columns applying your conditions:
with cte as (select max(count) maxcount from test_price)
select
o.*, o.count * p.price total_price
from test_orders o inner join test_price p
on p.count = coalesce(
(select min(count) from test_price where o.count <= count),
(select maxcount from cte)
)
order by o.id;
See the demo.
Results:
| id | count | total_price |
| --- | ----- | ----------- |
| 1 | 12 | 14.64 |
| 2 | 50 | 61 |
| 3 | 65 | 78 |
| 4 | 155 | 182.9 |
| 5 | 400 | 440 |
I think your test_prices table is not quite right. I think it should be:
id | count | price
----+-------+-------
1 | 1 | 1.22
2 | 100 | 1.20
3 | 150 | 1.19
4 | 200 | 1.18
5 | 300 | 1.10
This says that for 1-99 quantity, the price is $1.22. For 100-149, the price $1.20, and so on.
With this structure, you can use a lateral join:
select o.*, p.price
from test_orders o left join lateral
(select p.*
from test_prices p
where p.count <= o.count
order by p.count desc
fetch first 1 row only
) p
on 1=1
The problem with your data is that price_table data is not all "up-to count", because the 5th row also means "up-to and beyond".
If you are able to add another row to price_table like (6, 10000, 1.10) then the solution is easy:
CREATE VIEW price_view AS
SELECT orders.id, orders.count, p.price, orders.count * p.price as total
FROM (
SELECT o.id, o.count, min(p.count) as pricebracket
FROM test_price p
LEFT JOIN test_orders o ON p.count >= o.count
GROUP BY o.id, o.count) orders
LEFT JOIN test_price p ON orders.pricebracket = p.count
ORDER BY orders.id
If you cannot get rid of this inconsistency in your data then you have to select maximum value first. So the query modifies like this:
CREATE VIEW price_view AS
WITH temptable as (SELECT max(p.count) as maxcount FROM test_price p)
SELECT orders.id, orders.count, p.price, orders.count * p.price as total
FROM (
SELECT o.id, o.count, min(p.count) as pricebracket
FROM test_price p
CROSS JOIN temptable
LEFT JOIN test_orders o ON p.count >=
(case when o.count > temptable.maxcount then temptable.maxcount else o.count end)
GROUP BY o.id, o.count) orders
LEFT JOIN test_price p ON orders.pricebracket = p.count
ORDER BY orders.id

sql date subquery get the last

I have an error "could not be bound" while trying this SQL
SELECT
promotions.id_product, price.value
FROM
promotions
LEFT OUTER JOIN
(SELECT TOP 1 id_product, date, value
WHERE date > promotions.date) AS price ON price.id_product = promotion.id_product
About the SQL... I have two tables and I need get the correct price while the promotions is running (not the last price)...
Table promotions
id_product | DATE | VALUE | Finish_date
1 | 2018-05-01 | 20 | 2018-06-03
1 | 2018-07-02 | 18 | 2018-08-01
Table prices
id_product | DATE | VALUE
1 | 2018-04-01 | 30
1 | 2018-06-02 | 25
You a subquery with join cannot be correlated to other tables in the from clause.
Instead, use outer apply:
SELECT p.id_product, pr.value
FROM promotions p OUTER APPLY
(SELECT TOP 1 pr.id_product, pr.date, pr.value
FROM prices pr
WHERE pr.id_produto = p.id_produto AND pr.date > p.date
ORDER BY pr.date DESC
) pr;
I added the ORDER BY. Presumably, you want the "next" price after the promotion date, not an arbitrary price afterwards.

Select that joins two tables Oracle PL/SQL

I've got two tables wchich I need to join with Select and I've got a problem.
The tables look like that:
table_price
Product_ID | Buy_date | Buy_price |
1 | 16.10.01 | 2.50 |
1 | 16.11.02 | 3.20 |
2 | 16.10.31 | 3.80 |
table expire_date
Product_ID | Count | Exp_date |
1 | 1000 | 17.10.01|
1 | 500 | 17.11.31|
2 | 500 | 17.11.01|
I need to write a select in Oracle PL/SQL wchich gives me following results:
Product_ID| Count | Exp_date| last_buy_price|
1 | 1000 | 17.10.01| 3.20 |
1 | 500 | 17.31.31| 3.20 |
2 | 500 | 17.11.01| 3.80 |
It means that it will give me every expire date with count of product from table expire_date and match it with last buy price from table_price with product_id (always with last buy price, ordered by column buy_date)
Please guys help me, I've tried so many codes and I still can't get satysfying results
A correlated subquery using keep is possibly the most performant method:
select ed.*,
(select max(p.buy_price) keep (dense_rank first order by p.buy_date desc)
from table_price p
where p.product_id = ed.product_id
) as last_buy_price
from expire_date ed;
You could, of course, also express this in the from clause:
select ed.*, p.last_buy_price
from expire_date ed left join
(select p.product_id,
max(p.buy_price) keep (dense_rank first order by p.buy_date desc) as last_buy_price
from table_price p
) p
on p.product_id = ed.product_id;
You can use ROW_NUMBER() :
SELECT ed.*,
tp.buy_price as last_buy_price
FROM expire_date ed
JOIN(SELECT s.*,
ROW_NUMBER() OVER(PARTITION BY s.product_id ORDER BY s.buy_date DESC) as rnk
FROM table_price s) tp
ON(ed.product_id = tp.product_id and tp.rnk = 1 )

SUM in multi-currency

I am trying to do SUM() in a multi-currency setup. The following will demonstrate the problem that I am facing:-
Customer
-------------------------
Id | Name
1 | Mr. A
2 | Mr. B
3 | Mr. C
4 | Mr. D
-------------------------
Item
-------------------------
Id | Name | Cost | Currency
1 | Item 1 | 5 | USD
2 | Item 2 | 2 | EUR
3 | Item 3 | 10 | GBP
4 | Item 4 | 5 | GBP
5 | Item 5 | 50 | AUD
6 | Item 6 | 20 | USD
7 | Item 3 | 10 | EUR
-------------------------
Order
-------------------------
User_Id | Product_Id
1 | 1
2 | 1
1 | 2
3 | 3
1 | 5
1 | 7
1 | 5
2 | 6
3 | 4
4 | 2
-------------------------
Now, I want the output of a SELECT query that lists the Customer Name and the total amount worth of products purchased as:-
Customer Name | Amount
Mr. A | Multiple-currencies
Mr. B | 25 USD
Mr. C | 15 GBP
Mr. D | 2 EUR
So basically, I am looking for a way to add the cost of multiple products under the same customer, if all of them have the same currency, else simply show 'multiple-currencies'. Running the following query will not help:-
SELECT Customer.Name, SUM(Item.Amount) FROM Customer
INNER JOIN Order ON Order.User_Id = Customer.Id
INNER JOIN Item ON Item.Id = Order.Product_Id
GROUP BY Customer.Name
What should my query be? I am using Sqlite
I would suggest two output columns, one for the currency and one for the amount:
SELECT c.Name,
(case when max(currency) = min(currency) then sum(amount)
end) as amount,
(case when max(currency) = min(currency) then max(currency)
else 'Multiple Currencies'
end) as currency
FROM Customer c INNER JOIN
Order o
ON o.User_Id = c.Id INNER JOIN
Item
ON i.Id = o.Product_Id
GROUP BY c.Name
If you want, you can concatenate these into a single string column. I just prefer to have the information in two different columns for something like this.
The above is standard SQL.
I think your query should looks like this
SELECT
Data.Name AS [Customer Name],
CASE WHEN Data.Count > 1 THEN "Multiple-currencies" ELSE CAST(Data.Amount AS NVARCHAR) END AS Amount
FROM
(SELECT
Customer.Name,
COUNT(Item.Currency) AS Count,
SUM(Item.Amount) AS Amount
FROM
Customer
INNER JOIN Order ON Order.User_Id = Customer.Id
INNER JOIN Item ON Item.Id = Order.Product_Id
GROUP BY
Customer.Name) AS Data
A subquery to get the count of currencies and then ask for them in the main query to show the total or the text "Multiple-currencies".
Sorry if there is any mistake or mistype but I don't have a database server to test it
Hope this helps.
IMO I would start by standardizing variable names. Why call ID in customer table USER_ID in order table? Just a pet peeve. Anyway, you should learn how to build queries.
start with joining the customer table to the order table on then join the result to the item table. The first join is on CUSTOMER_ID and the second join is on PRODUCT_ID. Once you have that working use SUM and GROUP BY
Ok, I managed to solve the problem this way:-
SELECT innerQuery.Name AS Name, (CASE WHEN innerQuery.Currencies=1 THEN (innerQuery.Amount || innerQuery.Currency) ELSE 'Mutliple-Currencies' END) AS Amount, FROM
(SELECT Customer.Name, SUM(Item.Amount), COUNT(DISTINCT Item.Currency) AS Currencies, Item.Currency AS Currency FROM Customer
INNER JOIN Order ON Order.User_Id = Customer.Id
INNER JOIN Item ON Item.Id = Order.Product_Id
GROUP BY Customer.Name)innerQuery

Multiple join statements not returning the expected result

I have to write a query with the following requirements:
The query should return a list of all
entry values for a customer named
“Steve” and for each date shown (if
available) the most recent status detail for
that date.
Customer Table
CustomerID | CustomerName
1 | Steve
2 | John
Entry Table
CustomerID | EntryDate | EntryValue
1 | 5/4/2010 | 200.0
1 | 4/4/2010 | 100.0
1 | 3/4/2010 | 150.0
1 | 2/4/2010 | 170.0
2 | 5/4/2010 | 220.0
Status Table
CustomerID | StatusDate | Detail
1 | 5/28/2010 | D
1 | 4/24/2010 | S
1 | 4/5/2010 | P
1 | 2/28/2010 | A
The expected output is:
CustomerName | Date | OrderCost | Detail
Steve | 5/4/2010 | 200.0 | S
Steve | 4/4/2010 | 100.0 | A
Steve | 3/4/2010 | 75.0 | A
Steve | 3/4/2010 | 75.0 | <null>
I think that the expected output may be wrong and it should actually be:
CustomerName | Date | OrderCost | Detail
Steve | 5/4/2010 | 200.0 | S
Steve | 4/4/2010 | 100.0 | A
Steve | 3/4/2010 | 150.0 | A
Steve | 2/4/2010 | 170.0 | <null>
Given the requirement, I don't understand why the 3/4/2010 date would occur twice and the second time it would have a Detail. I wrote the following query:
I wrote the following query:
SELECT Customers.CustomerName, Entries.EntryDate, Entries.EntryValue, Status.Detail
FROM Customers
INNER JOIN Entries ON Customers.CustomerID = Entries.CustomerID
LEFT OUTER JOIN Status ON Status.CustomerID = Customers.CustomersID AND Status.StatusDate <= Entries.EntryDate
WHERE (Customers.CustomerName = 'Steve')
The result of my query is this:
CustomerName| EntryDate | EntryValue | Detail
Steve | 5/4/2010 | 200.00 | S
Steve | 5/4/2010 | 200.00 | P
Steve | 5/4/2010 | 200.00 | A
Steve | 4/4/2010 | 100.00 | A
Steve | 3/4/2010 | 150.00 | A
Steve | 2/4/2010 | 170.00 | NULL
Any hints on what I'm doing wrong here? I can't figure it out...
Update
I've changed the order to an entry so it doesn't confuse us that much.
You are getting more results than you expect because the second JOIN condition is satisfied by many rows in the statuses table (e.g. There are 3 statusDates earlier than 5/4 so this date appears 3 times in the result set).
You need to JOIN statuses table, but get only one match (the latest). This can be done in several ways, AFAIK usually with a sub query. I think your case is rather complicated - I used a temp table. Hope it helps... (I currently don't have a DB to test this on, hopefully there are no silly syntax errors).
DROP TABLE IF EXISTS temp;
CREATE TABLE temp AS -- This temp table is basically the result set you got
(SELECT c.CustomerName, e.EntryDate, e.EntryValue, s.Detail, s.StatusDate
FROM Customers c
INNER JOIN Entires e ON c.CustomerID = e.CustomerID
LEFT OUTER JOIN Status s ON s.CustomerID = c.CustomersID
AND s.StatusDate <= e.EntryDate
WHERE (c.CustomerName = 'Steve')
);
SELECT t.CustomerName, t.EntryDate, t.EntryValue, t.Detail
FROM temp t
WHERE t.StatusDate = (SELECT MAX(t2.StatusDate)
FROM temp t2
WHERE t2.EntryDate = t.EntryDate);
To refrain from creating a temp table I believe this will work (please try and do let me know!)
SELECT t.CustomerName, t.EntryDate, t.EntryValue, t.Detail
FROM (SELECT c.CustomerName, e.EntryDate, e.EntryValue, s.Detail, s.StatusDate
FROM Customers c
INNER JOIN Entries e ON c.CustomerID = e.CustomerID
LEFT OUTER JOIN Status s ON s.CustomerID = c.CustomersID
AND s.StatusDate <= e.EntryDate
WHERE c.CustomerName = 'Steve') AS t
WHERE t.StatusDate = (SELECT MAX(t2.StatusDate)
FROM temp t2
WHERE t2.EntryDate = t.EntryDate);
You can use a subquery to get the status.
Use TOP 1 for SQL Server or LIMIT 1 for SQLite/MySQL
SQL Server / SyBase
SELECT Customers.CustomerName, Entries.EntryDate, Entries.EntryValue,
(SELECT top 1 Status.Detail From Status
where Status.CustomerID = Customers.CustomersID AND Status.StatusDate <= Entries.EntryDate
order by Status.StatusDate desc)
FROM Customers
INNER JOIN Entries ON Customers.CustomerID = Entries.CustomerID
WHERE (Customers.CustomerName = 'Steve')
MySQL / SQLite
SELECT Customers.CustomerName, Entries.EntryDate, Entries.EntryValue,
(SELECT Status.Detail From Status
where Status.CustomerID = Customers.CustomersID AND Status.StatusDate <= Entries.EntryDate
order by Status.StatusDate desc
limit 1)
FROM Customers
INNER JOIN Entries ON Customers.CustomerID = Entries.CustomerID
WHERE (Customers.CustomerName = 'Steve')
Shouldn't the status date come after the order date? Something like:
SELECT Customers.CustomerName, Orders.OrderDate, Orders.OrderCost, Status.Detail
FROM Customers
INNER JOIN Orders ON Customers.CustomerID = Orders.CustomerID
LEFT OUTER JOIN Status ON Status.CustomerID = Customers.CustomersID
WHERE Customers.CustomerName = 'Steve' AND Status.StatusDate >= Orders.OrderDate
Also, the CustomerID in the Status table seems a bit strange as it's usually orders that have a status, not the customer. Shouldn't the status table have an OrderID field?
The expected output is wrong. The last row should be for the date 2/4/2010. Also, their order costs are not right either. 2/4/2010 should be returning null, because there is no matching status.