MySQL: Get the full row with min value - mysql

I am trying to get the complete row with the lowest price, not just the field with the lowest price.
Create table:
CREATE TABLE `Products` (
`SubProduct` varchar(100),
`Product` varchar(100),
`Feature1` varchar(100),
`Feature2` varchar(100),
`Feature3` varchar(100),
`Price1` float,
`Price2` float,
`Price3` float,
`Supplier` varchar(100)
);
Insert:
INSERT INTO
`Products` (`SubProduct`, `Product`, `Feature1`, `Feature2`, `Feature3`, `Price1`, `Price2`, `Price3`, `Supplier`)
VALUES
('Awesome', 'Product', 'foo', 'foo', 'foor', '1.50', '1.50', '0', 'supplier1'),
('Awesome', 'Product', 'bar', 'foo', 'bar', '1.25', '1.75', '0', 'supplier2');
Select:
SELECT
`SubProduct`,
`Product`,
`Feature1`,
`Feature2`,
`Feature3`,
MIN(`Price1`),
`Price2`,
`Price3`,
`Supplier`
FROM `Products`
GROUP BY `SubProduct`, `Product`
ORDER BY `SubProduct`, `Product`;
You can see that at http://sqlfiddle.com/#!2/c0543/1/0
I get the frist inserted row with the content of the column price1 from the second inserted row.
I expect to get the complete row with the right features, supplier and other columns. In this example it should be the complete second inserted row, because it has the lowest price in column price1.

You need to get the MIN price rows and then JOIN those rows with the main table, like this:
SELECT
P.`SubProduct`,
P.`Product`,
P.`Feature1`,
P.`Feature2`,
P.`Feature3`,
`Price` AS Price1,
P.`Price2`,
P.`Price3`,
P.`Supplier`
FROM `Products` AS P JOIN (
SELECT `SubProduct`, `Product`, MIN(`Price1`) AS Price
FROM `Products`
GROUP BY `SubProduct`, `Product`
) AS `MinPriceRows`
ON P.`SubProduct` = MinPriceRows.`SubProduct`
AND P.`Product` = MinPriceRows.`Product`
AND P.Price1 = MinPriceRows.Price
ORDER BY P.`SubProduct`, P.`Product`;
Working Demo: http://sqlfiddle.com/#!2/c0543/20
Here what I have done is to get a temporary recordset as MinPriceRows table which will give you MIN price per SubProduct and Product. Then I am joining these rows with the main table so that main table rows can be reduced to only those rows which contain MIN price per SubProduct and Product.

Try with this:
SELECT
`p`.`SubProduct`,
`p`.`Product`,
`p`.`Feature1`,
`p`.`Feature2`,
`p`.`Feature3`,
`p`.`Price1`,
`p`.`Price2`,
`p`.`Price3`,
`p`.`Supplier`
FROM `Products` `p`
inner join (select MIN(`Price1`)as `Price1`
From `Products`
) `a` on `a`.`Price1` = `p`.`Price1`
ORDER BY `p`.`SubProduct`, `p`.`Product`;
demo: http://sqlfiddle.com/#!2/c0543/24

This works...
SELECT
P1.`SubProduct`,
P1.`Product`,
P1.`Feature1`,
P1.`Feature2`,
P1.`Feature3`,
P1.`Price1`,
P1.`Price2`,
P1.`Price3`,
P1.`Supplier`
FROM `Products` P1
INNER JOIN `Products` P2 ON P1.SubProduct = P2.SubProduct AND P1.Product = P2.Product
WHERE P1.Price1 < P2.Price1

Related

MySQL w/ Multiple Associations Through a Single Cross Reference Table

Short version: when I set a condition on a join table it is "contaminating" another join that uses the same table with a different alias, giving me unexpected results.
Long version: I can demonstrate the behavior with the following three tables:
# TABLE 1
CREATE TABLE `students` (
`id` int(10) NOT NULL,
`name` varchar(10) NOT NULL
);
INSERT INTO `students` (`id`, `name`) VALUES
(1, 'Sophia');
# TABLE 2
CREATE TABLE `classes` (
`id` int(10) NOT NULL,
`subject` varchar(10) NOT NULL,
`year` varchar(4) NOT NULL
);
INSERT INTO `classes` (`id`, `subject`, `year`) VALUES
(1, 'Math', '2019'),
(2, 'English', '2020'),
(3, 'German', '2020');
# TABLE 3 - A CROSS REFERENCE
CREATE TABLE `classes_students` (
`id` int(10) NOT NULL,
`student_id` int(10) NOT NULL,
`class_id` int(10) NOT NULL
);
INSERT INTO `classes_students` (`id`, `student_id`, `class_id`) VALUES
(1, 1, 1),
(2, 1, 2),
(3, 1, 3);
If I select from the Students table with multiple joins on the Classes table, it might look like this:
SELECT S.id, S.name, GROUP_CONCAT(C20.subject), GROUP_CONCAT(C19.subject)
FROM students S
LEFT JOIN classes_students CS ON CS.student_id = S.id
LEFT JOIN classes AS C20 ON CS.class_id = C20.id AND C20.year = '2020'
LEFT JOIN classes AS C19 ON CS.class_id = C19.id AND C19.year = '2019'
WHERE 1 = 1
GROUP BY S.id
As expected, this returns all of Sophia's classes:
id name GROUP_CONCAT(C20.subject) GROUP_CONCAT(C19.subject)
1 Sophia English,German Math
However, if I want to get every student who took English in 2020 and Math in 2019, I try adding conditions using both of the joins (e.g. WHERE C20.subject = 'English' AND C19.subject = 'Math') and the query fails to return anything.
What is going on here? In addition to having a workaround, I'd love to better understand why this doesn't work! <3
UPDATED
As suggested in the comments, I've tried modifying the JOIN ON statement and adding a HAVING statement. The following query works as expected:
SELECT S.id, S.name, GROUP_CONCAT(C20.subject), GROUP_CONCAT(C19.subject)
FROM students S
LEFT JOIN classes_students CS ON CS.student_id = S.id
LEFT JOIN classes AS C20 ON CS.class_id = C20.id AND C20.year = '2020' AND C20.subject = 'English'
LEFT JOIN classes AS C19 ON CS.class_id = C19.id AND C19.year = '2019' AND C19.subject = 'Math'
WHERE 1=1
GROUP BY S.id
HAVING COUNT(C20.subject) > 0 AND COUNT(C19.subject) > 0
But this strategy performs really poorly in my actual application, which is more complex and has 16k "students". (Query time is quadruple that of a similar query with WHERE statements.)

MySQL: Update with join in subquery

I got a table with products and a table with reviews of the products. The products-table has the parent- and child-products. The parent-products should get all reviews from the child-products. I did:
DROP TABLE IF EXISTS products;
CREATE TABLE products (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`parent` int(10) unsigned DEFAULT NULL,
`review` decimal(3,2) DEFAULT NULL,
PRIMARY KEY(id)
);
DROP TABLE IF EXISTS reviews;
CREATE TABLE reviews (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`product` int(10) unsigned NOT NULL,
`review` decimal(3,2) DEFAULT NULL,
PRIMARY KEY(id)
);
INSERT INTO products SET id=1, parent=null;
INSERT INTO products SET id=2, parent=1;
INSERT INTO products SET id=3, parent=1;
INSERT INTO reviews SET product=2, review=5;
INSERT INTO reviews SET product=3, review=5;
INSERT INTO reviews SET product=3, review=4;
INSERT INTO products SET id=4, parent=null;
INSERT INTO products SET id=5, parent=4;
INSERT INTO reviews SET product=5, review=4;
INSERT INTO reviews SET product=5, review=2;
UPDATE products
SET products.review=
(SELECT SUM(reviews.review)/COUNT(reviews.review) FROM reviews
LEFT JOIN products p ON p.parent = products.id
)
WHERE products.parent IS NULL;
But with that I'm surprised I'm getting an error:
ERROR 1054 (42S22): Unknown column 'products.id' in 'on clause'
Any suggestions on how to do it correctly? The idea is that product 1 should get a review of 14/3 = 4.66 and product 4 should get a review of 6/2 = 3.
The products is not visible in the subquery. Use following syntax instead:
UPDATE products pp
LEFT JOIN (
SELECT pc.parent, SUM(r.review)/COUNT(r.review) as 'rev'
FROM reviews r
LEFT JOIN products pc on r.product = pc.id
GROUP BY pc.parent
) pcc ON pcc.parent = pp.id
SET pp.review=pcc.rev
WHERE pp.parent IS NULL;
Since you've declared p as an alias for the products table, you need to use it throughout the query. So, in your LEFT JOIN clause just use p.parent instead of products.parent.
UPDATE products
SET products.review=
(SELECT SUM(reviews.review)/COUNT(reviews.review) FROM reviews
LEFT JOIN products p ON p.parent = p.id
)
WHERE products.parent IS NULL;
At its heart, you appear to be looking for this value:
SELECT SUM(r.review)/(SELECT COUNT(*) FROM products) n FROM reviews r;
+----------+
| n |
+----------+
| 4.666667 |
+----------+
So, something like...
UPDATE products x
JOIN (SELECT SUM(r.review)/(SELECT COUNT(*) FROM products) n FROM reviews r) y
SET x.review = y.n
WHERE x.review IS NULL;

MySQL MAX() GROUP BY 3 tables

I have 3 tables:
CREATE TABLE IF NOT EXISTS sportsman (
sportsman_id int NOT NULL AUTO_INCREMENT,
sportsman_name varchar(255) NOT NULL,
PRIMARY KEY (sportsman_id)
);
CREATE TABLE IF NOT EXISTS competition (
competition_id int NOT NULL AUTO_INCREMENT,
competition_name varchar(255) NOT NULL,
PRIMARY KEY (competition_id)
);
CREATE TABLE IF NOT EXISTS results (
competition_id int,
sportsman_id int,
result float,
FOREIGN KEY (sportsman_id) REFERENCES sportsman(sportsman_id),
FOREIGN KEY (competition_id) REFERENCES competition(competition_id)
);
Here is the sample data:
INSERT INTO `sportsman` (`sportsman_name`) VALUES ('sportsman1');
INSERT INTO `sportsman` (`sportsman_name`) VALUES ('sportsman2');
INSERT INTO `competition` (`competition_name`) VALUES ('competition1');
INSERT INTO `competition` (`competition_name`) VALUES ('competition2');
INSERT INTO `results` (`competition_id`, `sportsman_id`, `result`) VALUES ('1', '1', '20');
INSERT INTO `results` (`competition_id`, `sportsman_id`, `result`) VALUES ('1', '2', '25');
INSERT INTO `results` (`competition_id`, `sportsman_id`, `result`) VALUES ('2', '1', '18');
INSERT INTO `results` (`competition_id`, `sportsman_id`, `result`) VALUES ('2', '2', '23');
I need to get competition_name, max result, sportsman_name.
My query is:
SELECT
c.`competition_name`,
MAX(r.`result`),
s.`sportsman_name`
FROM `competition` c
INNER JOIN `results` r ON c.`competition_id` = r.`competition_id`
INNER JOIN `sportsman` s ON s.`sportsman_id` = r.`sportsman_id`
GROUP BY c.`competition_name`;
It groups by competition_name, it finds max result. But it picks first sportsman_name.
Any suggestions?
GROUP BY the sportsman_name also to get the MAX(result) for every competitor in every competition.
SELECT
c.`competition_name`,
MAX(r.`result`),
s.`sportsman_name`
FROM `competition` c
INNER JOIN `results` r ON c.`competition_id` = r.`competition_id`
INNER JOIN `sportsman` s ON s.`sportsman_id` = r.`sportsman_id`
GROUP BY c.`competition_name`, s.`sportsman_name`;
Link to a live demo to try it out
And if you want to only show the name of the highest scorer:
SELECT
c.`competition_name`, result, r.sportsman_id, sportsman_name
FROM
`competition` c
INNER JOIN
`results` r ON c.`competition_id` = r.`competition_id`
AND r.`sportsman_id` = (SELECT
rs.`sportsman_id`
FROM
results rs
WHERE
rs.`competition_id` = r.`competition_id`
ORDER BY rs.`result` DESC
LIMIT 1)
INNER JOIN
`sportsman` s ON s.sportsman_id = r.sportsman_id
GROUP BY c.`competition_name`;
Fiddle
The simplest method in MySQL is to use group_concat()/substring_index():
SELECT c.`competition_name`, MAX(r.`result`),
SUBSTRING_INDEX(GROUP_CONCAT(s.`sportsman_name` ORDER BY r.result DESC), ',', 1) as sportspersons_name
FROM `competition` c INNER JOIN
`results` r
ON c.`competition_id` = r.`competition_id` INNER JOIN
`sportsman` s
ON s.`sportsman_id` = r.`sportsman_id`
GROUP BY c.`competition_name`;
This has some limitations. First, if the competitor's name can have a comma, then you need another separator (such as '|'); that is a minor adjustment to the query.
Second, the internal buffer for GROUP_CONCAT() has a default maximum length of about 1,000 characters. For your sample data, this is unlikely to be a problem. But the limit is good to know about (it can be increased).
Two alternative methods get around this. One uses an additional query to get the maximum result and "join"s that back in. The second uses variables. In most databases, you would simply use ROW_NUMBER(), but MySQL does not support that ANSI-standard function.
My colleague sent me another solution to my problem. Maybe it will help somebody
SELECT
c.competition_name,
r.result,
s.sportsman_name
FROM competition c
JOIN (SELECT
MAX(result) result, competition_id
FROM results
GROUP BY competition_id
) AS temp ON temp.competition_id = c.competition_id
JOIN results r ON r.result = temp.result AND r.competition_id = temp.competition_id
JOIN sportsman s ON s.sportsman_id = r.sportsman_id
WHERE temp.competition_id IS NOT NULL;

MySQL: Multiple Joins to Same Table & then to Another Table in Same Row

I am working in LAMP environment.
In MySQL I have 3 tables & I want to create the desired report as mentioned in below attached image.
How I can achieve this.
Because of table ProductMaster allowing non-ID integers in its FK_ProductTag_# fields, a special join is needed for each Attribute (assuming each FK_ProductTag_# can have a "0" value. Here is the requested query:
select a.ProductName as 'Product Name',
a.Attr1 as 'Atrribute-1',
b.Attr2 as 'Attribute-2',
c.Attr3 as 'Atrribute-3' from
(select m.ProductName as 'ProductName',
concat_ws(': ', tagtype.Description, tag1.Description) as 'Attr1'
from ProductMaster m
left join ProductTag tag1 on m.FK_ProductTag_1 = tag1.ID
left join ProductTagType tagtype on tag1.FK_ProductTagType = tagtype.ID) as a
join
(select m.ProductName as 'ProductName',
concat_ws(': ', tagtype.Description, tag2.Description) as 'Attr2'
from ProductMaster m
left join ProductTag tag2 on m.FK_ProductTag_2 = tag2.ID
left join ProductTagType tagtype on tag2.FK_ProductTagType = tagtype.ID) as b
on a.ProductName = b.ProductName
join
(select m.ProductName as 'ProductName',
concat_ws(': ', tagtype.Description, tag3.Description) as 'Attr3'
from ProductMaster m
left join ProductTag tag3 on m.FK_ProductTag_3 = tag3.ID
left join ProductTagType tagtype on tag3.FK_ProductTagType = tagtype.ID) as c
on a.ProductName = c.ProductName
order by a.ProductName asc
See this SQLFiddle for a demo.
SQLFiddle was acting up during testing, so copy the above query and the below table schema into SQLTest for a demo:
create table ProductTagType (ID int not null auto_increment, Description varchar(20), primary key (ID));
create table ProductTag (ID int not null auto_increment, Description varchar(20), FK_ProductTagType int(1), primary key (ID));
create table ProductMaster (ID int not null auto_increment, ProductName varchar(20), FK_ProductTag_1 int(1), FK_ProductTag_2 int(1), FK_ProductTag_3 int(1), primary key (ID));
insert into ProductTagType (Description)
values ('Imported'), ('Local'), ('HomeMade');
insert into ProductTag (Description, FK_ProductTagType)
values ('Wood', 2), ('Plastic', 2), ('Steel', 1), ('Aluminum', 3);
insert into ProductMaster (ProductName, FK_ProductTag_1, FK_ProductTag_2, FK_ProductTag_3)
values ('Chair', 1, 2, 3), ('Table', 0, 3, 4);

MySQL returning 1 image for each product

This is very similar to Limit results from joined table to one row, but I'm struggling to get it to work exactly as I need...
The table structure is very similar:
CREATE TABLE tblProducts (
intProductID int(11) NULL AUTO_INCREMENT,
strProductName varchar(255) NULL,
PRIMARY KEY (intProductID)
);
CREATE TABLE tblProductImages (
intImageID int(11) NULL AUTO_INCREMENT,
intProductID int(11) NULL,
strImageName varchar(255) NULL,
intOrder int(11) NULL,
PRIMARY KEY (intImageID)
);
Populate the tables with:
INSERT INTO tblProducts (strProductName)
VALUES
('Product #1'), ('Product #2'), ('Product #3');
INSERT INTO tblProductImages (intProductID, strImageName, intOrder)
VALUES
(1, 'image_for_1.jpg', 1),
(2, '1st_image_for_2.jpg', 1),
(2, '2nd_image_for_2.jpg', 2);
I want to do a SELECT that returns all 3 products, with appropriate images. Similar to:
intProductID, strProductName, strImageName
1, Product #1, image_for_1.jpg
2, Product #2, 1st_image_for_2.jpg
3, Product #3, NULL
A simple solution would be:
SELECT intProductID, strProductName,
(SELECT strImageName
FROM tblProductImages
WHERE tblProductImages.intProductID = tblProducts.intProductID
ORDER BY intOrder
LIMIT 1)
FROM tblProducts
But this fails if I want to add & return another field from the tblProductImages (AltText for example)
I've setup a Fiddle at http://sqlfiddle.com/#!2/883c5/1
Thanks
This one will work, but is quite ugly
select p.intProductId, p.strProductName, pi.strImageName
from tblProducts p
inner join tblProductImages pi on pi.intProductID = p.intProductId
inner JOIN (select min(intOrder) minOrder, intProductID
FROM tblProductImages
GROUP BY intProductID) s
on s.intProductID = p.intProductID and s.minOrder = pi.intOrder
union
select p.intProductId, p.strProductName, pi.strImageName
from tblProducts p
left join tblProductImages pi on pi.intProductID = p.intProductId
where pi.intProductID is null;
alternative :
select p.intProductId, p.strProductName, pi.strImageName
from tblProducts p
left join tblProductImages pi on pi.intProductID = p.intProductId
where pi.intProductId is null or pi.IntOrder = (select min(intOrder)
from tblProductImages
where intProductId = pi.intProductId);
Sqlfiddle
You need to use aggregation (specifically GROUP BY). You want to GROUP BY the product name and use a LEFT JOIN so that the image does not need to exist.
SELECT strProductName, strImageName
FROM tblProducts LEFT JOIN tblProductImages USING (intProductID)
GROUP BY strProductName
Note that the image that is selected in this case is random (though usually it will be in intImageID order). This assumes that the actual image retrieved is not important as long as it is associated with the given product.
http://sqlfiddle.com/#!2/1391e6/1
For that exact output, you might need to modify your insert statement for table tblProductImages as:
INSERT INTO tblProductImages (intProductID, strImageName, intOrder) VALUES
(1, 'image_for_1.jpg', 1), (2, '1st_image_for_2.jpg', 1), (3, '2nd_image_for_3.jpg', 2);
And use the below Select statement:
SELECT tP.intProductID, tp.strProductName, tPI.strImageName
FROM tblProducts AS tP
INNER JOIN tblProductImages AS tPI ON tP.intProductID = tPI.intProductID;