MySQL - how to join row with minimal value only once - mysql

Im trying to get row with minimal value for each product. In a simple database. I will outline below. I studied a lot of answers and nothing works unfortunately.
Ive tried some ways of doing this, like self join and it works fine except on the same values it returns doubling values.
SELECT
products.id,
products.name,
prices.price,
prices.price_init
FROM products
LEFT JOIN prices ON prices.product_id = products.id
JOIN(
SELECT products.id, min(prices.price) as min_price FROM products
LEFT JOIN prices ON prices.product_id = products.id
GROUP BY products.id
) as min_prices
ON
min_prices.id = products.id
AND
min_prices.min_price = prices.price
This query works fine, but doubles the results.
link to sqlfiddle:
http://sqlfiddle.com/#!9/d106d9a/1
As you can see I need to take price_init field for minimal price.
Thanks.

I you want only one row per product -- even when there are multiple minimum prices -- you can filter by the id rather than the price.
This looks like
SELECT p.id, p.name, pr.price, pr.price_init
FROM products p LEFT JOIN
prices pr
ON pr.product_id = p.id AND
pr.id = (SELECT pr2.id
FROM prices pr2
WHERE pr2.product_id = pr.product_id
ORDER BY pr2.price
LIMIT 1
);
Here is a SQL Fiddle.
Note in MySQL 8+, you would instead use window functions:
SELECT p.id, p.name, pr.price, pr.price_init
FROM products p LEFT JOIN
(SELECT pr.*,
ROW_NUMBER() OVER (PARTITION BY pr.product_id ORDER BY pr.price) as seqnum
FROM prices pr
) pr
ON pr.product_id = p.id AND pr.seqnum = 1;

Related

subquery error in group by when converting from MySQL to PostgresSQL

I'm converting my database from MySQL to PostgresSQL, and I have this query which tries to sord the products by the cheapest price and the most popular in a given location. It works fine in MySQL, but in Postgres I'm running into problems with this query :
SELECT products.product_id,
suppliers.supplier_id,
product_code.desc_fa,
products.name_fa,
MIN(product_supplier.price) AS price,
SUM(COALESCE(orders.quantity, 0)) AS n_orders
FROM products
JOIN product_code ON product_code.code_id = products.code_id
JOIN product_supplier ON product_supplier.product_id = products.product_id
JOIN suppliers ON suppliers.supplier_id = product_supplier.supplier_id
JOIN product_tags ON product_tags.product_id = products.product_id
JOIN tags ON tags.tag_id = product_tags.tag_id
JOIN product_crop ON product_crop.product_id = products.product_id
JOIN crops ON crops.crops_id = product_crop.crop_id
LEFT JOIN orders
ON orders.product_id = products.product_id and orders.crop_id = product_crop.crop_id
LEFT JOIN user ON user.user_id = orders.user_id and user.location_id = 883
WHERE crops.crops_id = 1
AND product_supplier.quantity >= 3
AND tags.tag = 'علف کش'
GROUP BY products.name_fa
ORDER BY n_orders DESC
LIMIT 10;
It gives me this error :
column must appear in the GROUP BY clause or be used in an aggregate function
Any suggestions to how to work around this error ?
UPDATE :
According to the answers i was able to make it work by using this query:
WITH tem_1 AS (SELECT product_id, MIN(price) AS price FROM product_supplier GROUP BY product_id) ,
tem_2 AS (SELECT product_id, SUM(quantity) AS n_orders FROM orders Group by product_id)
SELECT products.product_id, suppliers.supplier_id, product_code.desc_fa, products.name_fa, tem_1.price,
products.telegraph, suppliers.location_id, COALESCE(tem_2.n_orders,0) AS quant FROM products
INNER JOIN product_supplier ON product_supplier.product_id = products.product_id
INNER JOIN suppliers ON suppliers.supplier_id = product_supplier.supplier_id
INNER JOIN product_code ON product_code.code_id = products.code_id
INNER JOIN product_crop ON product_crop.product_id = products.product_id
INNER JOIN crops ON crops.crops_id = product_crop.crop_id
INNER JOIN product_tags ON product_tags.product_id = products.product_id
INNER JOIN tags ON tags.tag_id = product_tags.tag_id
INNER JOIN tem_1 ON tem_1.price = product_supplier.price AND tem_1.product_id = products.product_id
LEFT JOIN tem_2 ON tem_2.product_id = products.product_id
WHERE crops.crops_id = 1 AND product_supplier.quantity >= 3 AND tags.tag = 'علف کش'
ORDER BY quant DESC
LIMIT 10;;
But since i'm fairly new to SQL, I wanted to know if my code is correct or is there a better way to implement it?
When you use aggregate function (as SUM, MIN and others) the other columns in your field list without aggregate function must be included in GROUP BY clause.
These fields:
products.product_id,
suppliers.supplier_id,
product_code.desc_fa,
products.name_fa
must be in GROUP BY.
Instead your GROUP BY clause has only the following field:
products.name_fa
You must add the other missing 3 fields
In MySql this error has turned off by default, so your query works fine, but in other DBMS you are in an error case.
You can see here how set MySql environment about the GROUP BY behaviour

Filter products by options

I have following database structure to store product options.
Now i have problem to filter out products that match only given options. First i did WHERE option_id IN (array of options), but that would give me products that match any of the options and that is not solution. User wants to filter out only products with given material, color, and size for instance. And if i do WHERE option_id = 4 AND option_id = 6 for instance i get nothing.
Here is my query:
SELECT DISTINCT p.id AS id,
...
FROM products p
LEFT JOIN product_categories pc ON p.id = pc.product_id
LEFT JOIN product_images pi ON p.id = pi.product_id
LEFT JOIN product_options po ON p.id = po.product_id
WHERE p.product_active = 1
AND po.option_id = 1 // only to get the idea
GROUP BY id
ORDER BY id DESC
LIMIT 0,
12
Just to mention it is PHP application , where user select options from select element with or without multiple attribute.
How to acomplish this?
You can use having:
SELECT p.id AS id, ...
FROM products p JOIN
product_categories pc
ON p.id = pc.product_id LEFT JOIN
product_images pi
ON p.id = pi.product_id JOIN
product_options po
ON p.id = po.product_id
WHERE p.product_active = 1 AND
po.option_id IN (4, 6)
GROUP BY p.id
HAVING COUNT(DISTINCT po.option_id) = 2
ORDER BY p.id DESC
LIMIT 0, 12;
The HAVING clause is specifying that a given id has two matching options. Because of the WHERE clause, these are the only two options that you care about.
I didn't change your approach (you didn't supply the complete query), but you are doing joins along different dimensions -- categories, images, and options. This creates a Cartesian product for each product, and that is often not the best approach to such a query.
There is no need for LEFT JOIN in the solution.
SELECT DISTINCT p.id AS id
FROM products p
JOIN product_options po ON p.id = po.product_id
WHERE p.product_active = 1
AND po.option_id IN (1, 2, 3)
GROUP BY p.id
HAVING COUNT(po.option_id) = 3
My solution keep only tables necessary to find the products with specified options.
In the case you want products having exactly this options and no others you can use NOT EXISTS:
SELECT DISTINCT p.id AS id
FROM products p
JOIN product_options po ON p.id = po.product_id
WHERE p.product_active = 1 AND
po.option_id IN (1, 2, 3) and
NOT EXISTS (
SELECT 1
FROM product_options po2
WHERE p.id = po2.product_id and po2.option_id NOT IN (1, 2, 3)
)
GROUP BY p.id
HAVING COUNT(po.option_id) = 3
If you want to select products accoding to the other conditions (like product categories and so on) then use IN in the WHERE clause. This approach avoids generating duplicate po.option_id and the outer query will still work correctly even without DISTINCT in COUNT.
SELECT DISTINCT p.id AS id
FROM products p
JOIN product_options po ON p.id = po.product_id
WHERE p.product_active = 1 AND
po.option_id IN (1, 2, 3) AND
-- use the following IN predicate to select products with specific features without introducing duplicates in your query
p.id IN (
select product_id FROM product_categories WHERE <your_condition>
)
GROUP BY p.id
HAVING COUNT(po.option_id) = 3
You select products with image lists. Something like:
select products.*, group_concat(product_images.id)
Additionally there may be options the product must all meet. This is criteria that belongs in the WHERE clause.
select
p.*,
(select group_concat(image) from product_images i where i.product_id = p.id) as images
from products p
where product_active = 1
and id in
(
select product_id
from product_options
where option_id in (1,3,55,97)
group by product_id
having count(*) = 4 -- four options in this example
);
Thanks guys, i've managed to return exactly what i wanted.
Now i just have problem with pagination query for the filtered products.
Final search query:
SELECT DISTINCT p.id AS id,
main_price,
promotion_price,
NEW,
sale,
recommended,
COUNT(pi.filename) AS image_count,
GROUP_CONCAT(DISTINCT pi.filename
ORDER BY pi.main_image DESC, pi.id ASC) AS images,
name_sr,
uri_sr,
description_sr
FROM products p
LEFT JOIN product_categories pc ON p.id = pc.product_id
LEFT JOIN product_images pi ON p.id = pi.product_id
LEFT JOIN product_options po ON p.id = po.product_id
WHERE p.product_active = 1
AND po.option_id IN(1)
AND p.main_price BETWEEN 5250.00 AND 14000.00
GROUP BY id
HAVING COUNT(DISTINCT po.option_id) = 1
ORDER BY id DESC
LIMIT 0,
12
Pagination query is something like this i modified it accorgin to new filter query:
SELECT COUNT(DISTINCT p.id) AS number
FROM products p
LEFT JOIN product_categories pc ON p.id = pc.product_id
LEFT JOIN product_images pi ON p.id = pi.product_id
LEFT JOIN product_options po ON p.id = po.product_id
WHERE p.product_active = 1
AND po.option_id IN(1)
AND p.main_price BETWEEN 5250.00 AND 14000.00
GROUP BY(p.id)
HAVING COUNT(DISTINCT po.option_id) = 1
If i leave out DISTINCT in SELECT COUNT i don't get filtered pagination, if i set DISTINCT i get number of rows that corespond to pagination. I suppose i could add another count(*) to all of this with subquery, but not sure if that is way to go and if there is more efficient and elegant way to do this.

Select from 3 tables with two order by before two group by

I try to get a list of products with each newest and lowest offer price
Table product:
id | name
Table offer:
id | product_id | price | created | dealer_id
Table invalids:
id | offer_id | status
I have tried:
SELECT * FROM product INNER JOIN
(
SELECT offer.product_id , offer.price
FROM offer
LEFT JOIN invalids
ON offer.id = invalids.offer_id
WHERE invalids.id IS NULL
GROUP BY offer.dealer_id
ORDER BY offer.created DESC
) o
ON o.product_id = product.id
ORDER BY product.name
I have tried an sqlfiddle http://sqlfiddle.com/#!9/32658/3 with this offer values:
(`id`, `price`, `dealer_id`, `product_id`, `created`)
(1,12.60,1,1,'2015-05-17 08:44:45'),
(2,13.00,1,1,'2015-08-17 08:44:45'),
(3,20.00,1,1,'2015-08-17 08:45:30'),
(4,10.00,1,1,'2015-08-17 08:45:46'),
(5,4.00,2,1,'2015-05-17 08:44:11'),
(6,11.00,2,1,'2015-08-17 08:44:46'),
(7,5.00,2,1,'2015-08-17 08:45:31'),
(9,110.00,2,2,'2015-08-17 08:46:58'),
(10,11.00,2,2,'2015-08-17 08:47:12');
Expected value for product ID 1 is offer ID 7 with price 5.
These steps I think I must realize:
Order offers by created and group by dealer_id to get newest entries
Take result from step 1 and order it by price to get smallest price.
Make this for all products
Maybe I must use a second SELECT FROM offer with GROUP BY and ORDER BY but how do I get I the product_id from the first (outer) select?
Well I would start by getting the latest date for each product offer like this:
SELECT product_id, MAX(created) AS latestOffer
FROM offer
GROUP BY product_id;
Once you have that, you can join it to the original table to get that offer:
SELECT o.*
FROM offer o
JOIN(
SELECT product_id, MAX(created) AS latestOffer
FROM offer
GROUP BY product_id) tmp ON tmp.product_id = o.product_id AND tmp.latestOffer = o.created;
Here is an SQL Fiddle example.
This query should help you:
SELECT *
FROM product
JOIN (
SELECT product_id, min(price) as minPrice, max(created) as newestOffer
FROM offer
WHERE id NOT IN (SELECT offer_id FROM invalids)
GROUP BY 1
) as b
ON product.id = b.product_id
A shot in the dark based on what I understand you to be after...
lots of nested subqueries.. keep thinking there's got to be a better way...
SELECT OO.ID, OO.Price, OO.Dealer_Id, OO.Product_ID, OO.created, P.name
FROM Offer OO
INNER JOIN (
SELECT Min(Price) as MinP
FROM offer O
INNER JOIN (
SELECT max(OI.created) as LatestOffer, OI.Dealer_ID, OI.Product_ID
FROM Offer OI
LEFT JOIN invalids I
on OI.Id = I.offer_Id
WHERE I.ID is null
GROUP BY OI.Dealer_Id, OI.Product_Id
) B
on O.Dealer_Id = B.Dealer_Id
and O.Product_Id = B.Product_Id
and O.Created = B.LatestOffer
) Z
on OO.Price = Z.MinP
INNER JOIN product P
on P.ID = OO.Product_ID
SQL FIDDLE

Select top sales products

I have three tables like this
orders(id, status, ...)
products(id, created_at, ...)
product_order(order_id, product_id, quantity)
I want to select the most sold products first then continue with latest products taking the quantity in consideration, Here's my try
SELECT products.* FROM products
LEFT JOIN product_order ON product_order.product_id = products.id
LEFT JOIN orders ON orders.id = product_order.order_id
WHERE orders.status != 'REJECTED'
GROUP BY product_order.product_id
ORDER BY COUNT(*) DESC, products.created_at
This statement returns the products that are not sold first because I am using left join and they count more than the sold ones.. also I don't know how to take the quantity in consideration
Thank you,
This should work :
SELECT p.*, sum(po.quantity) qty
FROM products p
LEFT OUTER JOIN product_order po ON po.product_id = p.id
LEFT OUTER JOIN orders o ON o.id = po.order_id
WHERE o.status != 'REJECTED'
GROUP BY po.product_id
ORDER BY qty DESC, p.created_at
If you want the most sold products you could add
AND products.quantity = SELECT max(quantity) from products
after your WHERE statement

MySQL LEFT JOIN, GROUP BY and ORDER BY not working as required

I have a table
'products' => ('product_id', 'name', 'description')
and a table
'product_price' => ('product_price_id', 'product_id', 'price', 'date_updated')
I want to perform a query something like
SELECT `p`.*, `pp`.`price`
FROM `products` `p`
LEFT JOIN `product_price` `pp` ON `pp`.`product_id` = `p`.`product_id`
GROUP BY `p`.`product_id`
ORDER BY `pp`.`date_updated` DESC
As you can probably guess the price changes often and I need to pull out the latest one. The trouble is I cannot work out how to order the LEFT JOINed table. I tried using some of the GROUP BY functions like MAX() but that would only pull out the column not the row.
Thanks.
It appears that it is impossible to use an ORDER BY on a GROUP BY summarisation. My fundamental logic is flawed. I will need to run the following subquery.
SELECT `p`.*, `pp`.`price` FROM `products` `p`
LEFT JOIN (
SELECT `price` FROM `product_price` ORDER BY `date_updated` DESC
) `pp`
ON `p`.`product_id` = `pp`.`product_id`
GROUP BY `p`.`product_id`;
This will take a performance hit but as it is the same subquery for each row it shouldn't be too bad.
You need to set aliases properly I think and also set what you are joining on:
SELECT p.*, pp.price
FROM products AS p
LEFT JOIN product_price AS pp
ON pp.product_id = p.product_id
GROUP BY p.product_id
ORDER BY pp.date_updated DESC
This will give you the last updated price:
select
p.*, pp.price
from
products p,
-- left join this if products may not have an entry in prodcuts_price
-- and you would like to see a null price with the product
join
(
select
product_price_id,
max(date_updated)
from products_price
group by product_price_id
) as pp_max
on p.product_id = pp.product_id
join products_price pp on
pp_max.prodcuts_price_id = pp.products_price_id
Mysqlism:
SELECT p.*, MAX(pp.date_updated), pp.price
FROM products p
LEFT JOIN product_price pp ON pp.product_id = p.product_id
GROUP BY p.product_id
Will work on some RDBMS:
SELECT p.*, pp.date_updated, pp.price
FROM products p
LEFT JOIN product_price pp ON pp.product_id = p.product_id
WHERE (p.product_id, pp.date_updated)
in (select product_id, max(date_updated)
from product_price
group by product_id)
Will work on most RDBMS:
SELECT p.*, pp.date_updated, pp.price
FROM products p
LEFT JOIN product_price pp ON pp.product_id = p.product_id
WHERE EXISTS
(
select null -- inspired by Linq-to-SQL style :-)
from product_price
WHERE product_id = p.product_id
group by product_id
HAVING max(date_updated) = pp.date_updated
)
Will work on all RDBMS:
SELECT p.*, pp.date_updated, pp.price
FROM products p
LEFT JOIN product_price pp ON pp.product_id = p.product_id
LEFT JOIN
(
select product_id, max(date_updated) as recent
from product_price
group by product_id
) AS latest
ON latest.product_id = p.product_id AND latest.recent = pp.date_updated
And if nate c's code intent is to just get one row from product_price, no need to table-derive (i.e. join (select product_price_id, max(date_updated) from products_price) as pp_max), he might as well just simplify(i.e. no need to use the product_price_id surrogate primary key) it like the following:
SELECT p.*, pp.date_updated, pp.price
FROM products p
LEFT JOIN product_price pp ON pp.product_id = p.product_id
WHERE pp.date_updated = (select max(date_updated) from product_price)