SQL query find supplier with lowest price for each part - mysql

I need to find the supplier with the lowest price for each part.
Tables: suppliers(sid, sname, address), parts(pid, pname, colour), catalog(sid, pid, cost)
This works:
SELECT
sname, pid
FROM
(SELECT
*
FROM
suppliers
NATURAL JOIN catalog
NATURAL JOIN (SELECT
pid, MIN(cost) AS min_cost
FROM
catalog
GROUP BY (pid)) AS m
HAVING cost = min_cost) AS n
But when I try to shorten it to the following I get an error that there is an unknown cost in the having clause:
SELECT
sname, pid
FROM
suppliers
NATURAL JOIN
catalog
NATURAL JOIN
(SELECT
pid, MIN(cost) AS min_cost
FROM
catalog
GROUP BY (pid)) AS m
HAVING cost = min_cost
Why can't it find the cost? Isn't the cost in the table because I've joined the subquery to catalog?
EDIT
I changed it to use INNER JOIN instead of NATURAL JOIN as per suggestions, but I'm still getting the same error. New query:
SELECT
s.sname, m.pid
FROM
suppliers s
INNER JOIN
catalog c ON s.sid = c.sid
INNER JOIN
(SELECT
pid, MIN(cost) AS min_cost
FROM
catalog
GROUP BY (pid)) AS m ON c.pid = m.pid
HAVING cost = min_cost
EDIT_2
The problem was not the JOIN but the HAVING, which should actually be WHERE, as shown by bbrumm's answer.

I would suggest a query like this:
SELECT
supplier.sname,
catalog.pid
FROM suppliers
INNER JOIN catalog ON suppliers.supplier_id = catalog.supplier_id
INNER JOIN
(SELECT
pid, MIN(cost) AS min_cost
FROM catalog
GROUP BY (pid)) AS m
ON catalog.pid = m.pid
WHERE catalog.cost = m.min_cost;
I've made a few assumptions on your column names (e.g. supplier_id) that you may need to change. A point could be made that the "cost=min_cost" is part of the JOIN so it could go there as well. I've also not included table aliases as while it's best practice, it's not required.

Related

Left join issue using having clause with sum

I have two tables, products(id, name) and products_cost(id, sid, cost).
I'm trying to get a list of products whose total cost is less than 1000 including products that has no cost.
I've tried this:
SELECT a.name, SUM(b.cost) AS price
FROM products a
LEFT JOIN products_cost b
ON a.id = b.sid
GROUP BY a.name
HAVING SUM(b.cost)<1000;
The above SQL only gives the list of product name that includes cost and I want the output list to include product name that doesn't include cost as well.
I think you need to check for null values in the having clause:
SELECT p.name, SUM(pc.cost) AS price
FROM products p LEFT JOIN
products_cost pc
ON p.id = b.sid
GROUP BY p.name
HAVING SUM(pc.cost) < 1000 OR SUM(pc.cost) IS NULL;
Note that I also fixed the logic (salary doesn't makes sense in the SUM()). And I introduced meaningful table aliases -- abbreviations for the table names.
You can also add an IFNULL expression in your HAVING clause
SELECT a.name, SUM(b.cost) AS price
FROM products a
LEFT JOIN products_cost b
ON a.id = b.sid
GROUP BY a.name
HAVING SUM(IFNULL,b.cost,0)<1000;
use subquery and join
select p.*,a.s as cost from product p left join (
select pid,sum(cost) as s
from products_cost
group by pid
having s<1000 ) a on p.id=a.pid

MySQL: Ranking from multiple tables, sub queries?

This is a MySQL question. I have three tables with the following columns:
transactions (table): transact_id, customer_id, transact_amt, product_id,
products (table): product_id, product_cost, product_name, product_category
customers (table): customer_id, joined_at, last_login_at, state, name, email
I'd like a query that finds out the most popular item in every state and the state. One of the tricky parts is that some product_name have multiple product_id. Therefore I though joining the three tables that generate an output with two columns: state and product_name. Until here that worked fine doing this:
SELECT p.product_name, c.state
FROM products p
INNER JOIN transactions t
ON p.product_id = t.product_id
INNER JOIN customers c
ON c.customer_id = t.customer_id
This selects all the products, and the states from where the customer is. The problem is that I can't find the way to rank the mos popular product per state. I tried different group by, order by and using subqueries without success. I suspect I need to do subqueries, but I can't find the way to resolve it. The expected outcome should look like this:
most_popular_product | state
Bamboo | WA
Walnut | MO
Any help will be greatly appreciated.
Thank you!
You need a subquery that gets the count of transactions for each product in each state.
SELECT p.product_name, c.state, COUNT(*) AS count
FROM products p
INNER JOIN transactions t
ON p.product_id = t.product_id
INNER JOIN customers c
ON c.customer_id = t.customer_id
GROUP BY p.product_name, c.state
Then write another query that has this as a subquery, and gets the highest count for each state.
SELECT state, MAX(count) AS maxcount
FROM (
SELECT p.product_name, c.state, COUNT(*) AS count
FROM products p
INNER JOIN transactions t
ON p.product_id = t.product_id
INNER JOIN customers c
ON c.customer_id = t.customer_id
GROUP BY p.product_name, c.state
) AS t
GROUP BY state
Finally, join them together:
SELECT t1.product_name AS most_popular_product, t1.state
FROM (
SELECT p.product_name, c.state, COUNT(*) AS count
FROM products p
INNER JOIN transactions t
ON p.product_id = t.product_id
INNER JOIN customers c
ON c.customer_id = t.customer_id
GROUP BY p.product_name, c.state
) AS t1
JOIN (
SELECT state, MAX(count) AS maxcount
FROM (
SELECT p.product_name, c.state, COUNT(*) AS count
FROM products p
INNER JOIN transactions t
ON p.product_id = t.product_id
INNER JOIN customers c
ON c.customer_id = t.customer_id
GROUP BY p.product_name, c.state
) AS t
GROUP BY state
) AS t2 ON t1.state = t2.state AND t1.count = t2.maxcount
This is basically the same pattern as SQL select only rows with max value on a column, just using the first grouped query as the table you're trying to group.

Rewriting a query that has two sub queries using no sub queries

Given the database schema:
Part( PID, PName, Producer, Year, Price)
Customer( CID, CName, Province)
Supply(SID, PID, CID, Quantity, Amount, Date)
And the query:
Select cname, Province
From Customer c
Where exists (
Select *
from Supply s
join Part p on p.pId = s.pId
Where CId = c.CId
and p.Producer = 'Apple'
)
and Not exists (
Select *
from Supply n
join Part nap on nap.pId = n.pId
Where CId = c.CId
and nap.Producer != 'Apple'
)
How would I go about rewriting this query without the two sub queries?
You can use the LEFT JOIN/NULL pattern to find customers who haven't bought any non-Apple products. Then you can do this all with just joins. You'll have to join with Supply and Parts twice, once for finding Apple products, then again for excluding non-Apple products.
SELECT distinct c.name, c.province
FROM Customer AS c
JOIN Supply AS s1 ON s1.cid = c.cid
JOIN Parts AS p1 ON p1.pid = s1.pid
LEFT JOIN Supply AS s2 ON s2.cid = c.cid
LEFT JOIN Parts AS p2 ON p2.pid = s2.pid AND p2.producer != 'Apple'
WHERE p1.producer = 'Apple' AND p2.pid IS NULL
Notice that in the LEFT JOIN you put restrictions of the second table in the ON clause, not the WHERE clause. See Return row only if value doesn't exist for more about this part of the query.
You want customer who only bought Apple products?
One possible solution is based on conditional aggregation:
Select c.cname, c.Province
From Customer c
join
( -- this is not a Subquery, it's a Derived Table
Select s.CId -- assuming there's a CId in Supply
from Supply s
join Part p
on p.pId = s.pId
group by s.CId
-- when there's any other supplier this will return 1
having max(case when p.Producer = 'Apple' then 0 else 1 end) = 0
) as p
on p.CId = c.CId

relational division

I'm supposed to write a query for this statement:
List the names of customers, and album titles, for cases where the customer has bought the entire album (i.e. all tracks in the album)
I know that I should use division.
Here is my answer but I get some weird syntax errors that I can't resolve.
SELECT
R1.FirstName
,R1.LastName
,R1.Title
FROM (Customer C, Invoice I, InvoiceLine IL, Track T, Album Al) AS R1
WHERE
C.CustomerId=I.CustomerId
AND I.InvoiceId=IL.InvoiceId
AND T.TrackId=IL.TrackId
AND Al.AlbumId=T.AlbumId
AND NOT EXISTS (
SELECT
R2.Title
FROM (Album Al, Track T) AS R2
WHERE
T.AlbumId=Al.AlbumId
AND R2.Title NOT IN (
SELECT R3.Title
FROM (Album Al, Track T) AS R3
WHERE
COUNT(R1.TrackId)=COUNT(R3.TrackId)
)
);
ERROR: misuse of aggregate function COUNT()
You can find the schema for the database here
You cannot alias a table list such as (Album Al, Track T) which is an out-dated syntax for (Album Al CROSS JOIN Track T). You can either alias a table, e.g. Album Al or a subquery, e.g. (SELECT * FROM Album CROSS JOIN Track) AS R2.
So first of all you should get your joins straight. I don't assume that you are being taught those old comma-separated joins, but got them from some old book or Website? Use proper explicit joins instead.
Then you cannot use WHERE COUNT(R1.TrackId) = COUNT(R3.TrackId). COUNT is an aggregate function and aggregation is done after WHERE.
As to the query: It's a good idea to compare track counts. So let's do that step by step.
Query to get the track count per album:
select albumid, count(*)
from track
group by albumid;
Query to get the track count per customer and album:
select i.customerid, t.albumid, count(distinct t.trackid)
from track t
join invoiceline il on il.trackid = t.trackid
join invoice i on i.invoiceid = il.invoiceid
group by i.customerid, t.albumid;
Complete query:
select c.firstname, c.lastname, a.title
from
(
select i.customerid, t.albumid, count(distinct t.trackid) as cnt
from track t
join invoiceline il on il.trackid = t.trackid
join invoice i on i.invoiceid = il.invoiceid
group by i.customerid, t.albumid
) bought
join
(
select albumid, count(*) as cnt
from track
group by albumid
) complete on complete.albumid = bought.albumid and complete.cnt = bought.cnt
join customer c on c.customerid = bought.customerid
join album a on a.albumid = bought.albumid;
Seems you are using count in the wrong place
use having for aggregate function
SELECT R3.Title
FROM (Album Al, Track T) AS R3
HAVING COUNT(R1.TrackId)=COUNT(R3.TrackId))
but be sure of alias because in some database the alias in not available in subquery ..
You should simplify your query. Take a look at this:
SELECT FirstName
, LastName
, Title
FROM (
SELECT C.FirstName
, C.LastName
, A.AlbumID
, A.Title
, COUNT(DISTINCT TrackID) as TracksInvoiced
FROM Customer C
INNER JOIN Invoice I
ON I.CustomerId = C.CustomerId
INNER JOIN InvoiceLine IL
ON I.InvoiceId = IL.InvoiceId
INNER JOIN Track T
ON T.TrackID = I
INNER JOIN Album A
ON A.AlbumID = T.AlbumID
GROUP BY C.FirstName, C.LastName, A.AlbumID, A.Title
) C
INNER JOIN (
SELECT AlbumID
, COUNT(TrackID) as TotalTracks
FROM Track
GROUP BY AlbumID
) A
ON C.AlbumID = A.AlbumID
AND TracksInvoiced = TotalTracks
I used two subselects, the first one counts invoiced tracks per customer and album and joins it with another subselect for each album and amount of tracks on it, only where the two counts are equal.
This one seems to be a little less complicated:
SELECT r.FirstName, r.LastName, r.Title FROM
(
SELECT C.FirstName as FirstName,
C.LastName as LastName,
A.Title as Title,
A.AlbumId as AlbumId,
COUNT(*) as count
FROM Customer C, Invoice I, InvoiceLine IL, Track T, Album A
WHERE C.CustomerId=I.CustomerId
AND I.InvoiceId = IL.InvoiceId
AND T.TrackId = IL.TrackId
AND A.AlbumId = T.AlbumId
GROUP BY C.CustomerId, A.AlbumId
) AS r
WHERE r.count IS IN
(
SELECT COUNT(*) FROM Track T
WHERE T.AlbumId = r.AlbumId
)
Tested the idea on a simpler basis and extended to your example so I don't give a guarantee that you can copy and paste and its working immediately...

MySQL Join Query (possible two inner joins)

I currently have the following:
Table Town:
id
name
region
Table Supplier:
id
name
town_id
The below query returns the number of suppliers for each town:
SELECT t.id, t.name, count(s.id) as NumSupplier
FROM Town t
INNER JOIN Suppliers s ON s.town_id = t.id
GROUP BY t.id, t.name
I now wish to introduce another table in to the query, Supplier_vehicles. A supplier can have many vehicles:
Table Supplier_vehicles:
id
supplier_id
vehicle_id
Now, the NumSupplier field needs to return the number of suppliers for each town that have any of the given vehicle_id (IN condition):
The following query will simply bring back the suppliers that have any of the given vehicle_id:
SELECT * FROM Supplier s, Supplier_vehicles v WHERE s.id = v.supplier_id AND v.vehicle_id IN (1, 4, 6)
I need to integrate this in to the first query so that it returns the number of suppliers that have any of the given vehicle_id.
SELECT t.id, t.name, count(s.id) as NumSupplier
FROM Town t
INNER JOIN Suppliers s ON s.town_id = t.id
WHERE s.id IN (SELECT sv.supplier_id
FROM supplier_vehicles sv
WHERE sv.vehicle_id IN (1,4,6))
GROUP BY t.id, t.name
Or you could do an INNER JOIN (as your supplier join is INNER, but this will remove towns with no suppliers with those vehicles) and change the COUNT(s.id) TO COUNT(DISTINCT s.id)
If I remember correctly, you can put your second query inside the LEFT OUTER JOIN condition.
So for example, you can do something like
...
LEFT OUTER JOIN (SELECT * FROM Suppler s, Supplier_vehicles ......) s ON s.town_id=t.id
In that way you are "integrating" or combining the two queries into one. Let me know if this works.
SELECT t.name, count(s.id) as NumSupplier
FROM Town t
LEFT OUTER JOIN Suppliers s ON t.id = s.town_id
LEFT OUTER JOIN Supplier_vehicles v ON s.id = v.supplier_id
WHERE v.vehicle_id IN (1,4,6)
GROUP BY t.name