MySQL Query to retrieve maximum value among certain criteria - mysql

I have products with different rankings. The products may be members of a supergroup (like Cream).
product_id | supergroup | rank | other_info
1 | Cream | 3 | Eric
2 | Zep | 1 | Jimmy
3 | Zep | 4 | Jon Paul
4 | Cream | 3 | Jack
5 | Cream | 4 | Ginger
6 | Who | 4 | Roger
7 | Who | 5 | John
8 | Who | 3 | Pete
I want to get the max product rank from each group, along with other info for that product id. Ranks are not meant for intragroup ranks. They are ranks that work across all products in the system. So more than one product may have the same rank, even in the same group.
EDIT: fixed "other_info". I had some gibberish there. Also added a row. Results should be from highest rank to lowest. But they also should only include the highest ranking product_id from each supergroup, along with matching other_info.
product_id | supergroup | rank | other_info
2 | Zep | 1 | Jimmy
8 | Who | 3 | Pete
1 | Cream | 3 | Eric
Can I do that with a simple query? The existing system's query already involves a GROUP BY statement on the supergroup, and no aggregators in the SELECT. That results in a random, but coherent row from within the group. What is the simplest way to modify the query to get a complete row, but always of the highest-ranked member of each super group.
If there is no way, what about this: Is this possible without GROUP BY?

SELECT t.*
FROM your_table t
JOIN (
SELECT MIN(product_id) as product_id #if there are multiple products with the same (min) rank in the same supergroup - get the one with lowest product_id
FROM your_table tt
JOIN (
SELECT supergroup, MIN(rank) as min_rank
FROM your_table
GROUP BY supergroup
) mr ON mr.supergroup = tt.supergroup AND mr.min_rank = tt.rank
GROUP BY tt.supergroup, tt.rank
) as mid ON mid.id.product_id = t.product_id
ORDER BY whatever_you_need_to
You need an index on (supergroup,rank) for this to run efficiently.

Related

Different price for additional members of the same group

I have a simple table like this:
group | name | price
1 | john |
2 | mike |
3 | paul |
1 | sean |
4 | jack |
2 | brad |
5 | mick |
1 | bill |
4 | chad |
I have two different price values where 100EUR is for a first member of a group and 50EUR is for all additional members of that same group.
Detailed explanation. If a group has only one member, that member gets a price of 100EUR. If a group has multiple members, the first member gets a price of 100EUR, and all additional members of that same group get a price of 50EUR. There can be unlimited number of groups that will be added additionally.
The result should be like this:
group | name | price
1 | john | 100
2 | mike | 100
3 | paul | 100
1 | sean | 50
4 | jack | 100
2 | brad | 50
5 | mick | 100
1 | bill | 50
4 | chad | 50
I'd need a query which would be able to INSERT/UPDATE all missing price fields whenever I manually run it.
Thank you in advance for looking into that matter.
After a lot of trial and error I found a perfect fully functional solution, based on daviid's clever method. The issue with mysql is that by it's structure won't update tables with select methods as subquery. However, self-join (join or inner join) methods can be used instead in this case. I also had to add auto-incremental id to that table, so the final table structure is:
id | group_id | name | price
1 | 1 | john |
2 | 2 | mike |
3 | 3 | paul |
4 | 1 | sean |
5 | 4 | jack |
6 | 2 | brad |
7 | 5 | mick |
8 | 1 | bill |
9 | 4 | chad |
---
SET SQL_SAFE_UPDATES=0;
UPDATE table_name
SET price = 50;
UPDATE table_name AS a
JOIN
( SELECT id
FROM table_name
GROUP BY group_id
HAVING COUNT(*) >= 1
) AS b
ON a.id = b.id
SET a.price = 100;
Thanks also to Cody and Barmar for usable hints...
A partial answer: you can GROUP BY your "group" field and tack on a HAVING COUNT(group) > 1 to determine if that group has more than 1 member.
That is, to see all groups with more than one member it would look like:
SELECT
group
FROM table
GROUP BY group
HAVING COUNT(group) > 1
That will just tell you which groups have multiple members. Without another way to ensure ordering you cannot tell which member is "first" in their group and thus should be priced at 100 and all others priced at 50.
The following queries are not tested and might contain syntax errors. But they are good enough to understand the principle. There are many possible ways to achieve your result.
Here is my take: I would make use of one query to UPDATE the price on every row and set it to 50 whether it is the first group member or not. >table_name<, of course, needs to be changed to the name of your mentioned table.
UPDATE >table_name<
SET price = 50;
Then I would take care of each individual group and the respective first member by running the following query. Adapt the query to each group by changing the >groupId<.
UPDATE >table_name<
SET price = 100
WHERE id = (
SELECT id
FROM >table_name<
WHERE group = >groupId<
ORDER BY id
LIMIT 1
);
Take a look a the nested query: It queries the table for all members of only one group, orders them in ascending order and only returns an id per member. By applying LIMIT to the query, the result will just be the first group member's id. The resulting id can then be used in the other query to update the price and set it to 100.
But be careful: If you insert/delete (new) members with an id that is not just counting up, this query might select a "new first member".

Mysql query to get max age by section and if two or more has same age return student with smallest id

I have a table of students with temporary test values like this:
Table students
+----+-------------+-------+-----------+
| id | section_id | age | name |
+----+-------------+-------+-----------+
| 1 | 1 | 18 | Justin |
+----+-------------+-------+-----------+
| 2 | 2 | 14 | Jillian |
+----+-------------+-------+-----------+
| 3 | 2 | 16 | Cherry |
+----+-------------+-------+-----------+
| 4 | 3 | 19 | Ronald |
+----+-------------+-------+-----------+
| 5 | 3 | 21 | Marie |
+----+-------------+-------+-----------+
| 6 | 3 | 21 | Arthur |
+----+-------------+-------+-----------+
I want to query the table such that I want to get all the maximum ages of each section. However, if two students have the same age, the table produced will return the student with smallest id.
Return:
+----+------------+-----+--------+
| id | section_id | age | name |
+----+------------+-----+--------+
| 1 | 1 | 18 | Justin |
+----+------------+-----+--------+
| 3 | 2 | 16 | Cherry |
+----+------------+-----+--------+
| 5 | 3 | 21 | Marie |
+----+------------+-----+--------+
I tried this query:
SELECT ANY_VALUE(id), ANY_VALUE(section_id), MAX(age), ANY_VALUE(name) FROM
(SELECT id, section_id, age, name FROM students ORDER BY id) as X
GROUP BY section_id
Unfortunately, there are instances that id does not match the age and name.
I have on my end:
sql_mode = only_full_group_by
and I don't have a privilege to edit that, hence the any_value function but I have no idea how to use it.
This will do what you want.
It starts by finding the maximum age per section (including duplicates).
Then it joins those results with the minimum id per section (to eliminate duplicates).
And finally, select all fields for the matching id and section combinations.
SELECT s3.*
FROM students s3
INNER JOIN (
SELECT MIN(s2.id) AS id, s2.section_id
FROM students s2
INNER JOIN (
SELECT s1.section_id, MAX(s1.age) AS age
FROM students s1
GROUP BY s1.section_id
) s1 USING (section_id, age)
GROUP BY s2.section_id
) s2 USING (id, section_id);
Working SQL fiddle: https://www.db-fiddle.com/f/aezgAYM6A5KnXykceB7At1/0
I would simply use a correlated subquery:
select s.*
from students s
where s.id = (select s2.id
from students s2
where s2.section_id = s.section_id
order by s2.age desc, s2.id asc
limit 1
);
This is pretty much the simplest way to express the logic. And with an index on students(section, age, id), it should be the most performant as well.

mysql sum and count per category append to matching category rows

I am trying to derive a MySQL query that turns this:
product | sold
milk | 6
milk | 4
bread | 3
bread | 2
bread | 2
to this;
product | sold | total order | Total sold
milk | 2 | 2 | 6
milk | 4 | 2 | 6
bread | 3 | 3 | 7
bread | 2 | 3 | 7
bread | 2 | 3 | 7
I've been able to get the queries for sums and counts no problem, but I cannot seem to get it to join as a new column matching the product. Is this even possible? I've tried WITH ROLLUP but it just creates another row, not exactly what I want.
You could join a simple query on this table with an aggregate query on:
SELECT a.product, a.sold, b.total_order, b.total_sold
FROM mytable a
JOIN (SELECT product, COUNT(*) AS total_order, SUM(sold) AS total_sold
FROM mytable
GROUP BY product) b ON a.product = b.product

MYSQL show all entries sorted by 2 columns random on one column

We are looking to return rows of a query as groups and displaying all entries of the group in the sort order. Randomly based on the set_id... and then in order by the sort_id.
So, randomly it will show:
Carl,
Phil,
Wendy,
Tina,
Rick,
Joe
or
Tina,
Rick,
Joe,
Carl,
Phil,
Wendy
This query is always showing Tina/Rick/Joe first
SELECT * FROM products ORDER BY set_id, rand()
Any help would be appreciated
+---------+--------+-------+----------+
| id | set_id | name | sort_id |
+---------+--------+-------+----------+
| 1 | AA |Rick | 2 |
| 2 | BB |Carl | 1 |
| 3 | AA |Joe | 3 |
| 4 | AA |Tina | 1 |
| 5 | BB |Phil | 2 |
| 6 | BB |Wendy | 3 |
+---------+--------+-------+----------+
if you need a random comma separated name list this will do the trick.
This will keep the groups and the correct sorting within the group.
Query
SELECT
GROUP_CONCAT(Table_names_rand.names) as names
FROM (
SELECT
*
FROM (
SELECT
GROUP_CONCAT(name ORDER BY sort_id) as names
FROM
Table1
GROUP BY
set_id
)
AS Table1_names
ORDER BY
RAND()
)
AS Table_names_rand
Result
| names |
|-------------------------------|
| Carl,Phil,Wendy,Tina,Rick,Joe |
or
| names |
|-------------------------------|
| Tina,Rick,Joe,Carl,Phil,Wendy |
demo http://www.sqlfiddle.com/#!9/487ac9/9
if you need random names as records output.
Query
SELECT
Table1.name
FROM
Table1
CROSS JOIN (
SELECT
GROUP_CONCAT(Table_names_rand.names) as names
FROM (
SELECT
*
FROM (
SELECT
GROUP_CONCAT(name ORDER BY sort_id) as names
FROM
Table1
GROUP BY
set_id
)
AS Table1_names
ORDER BY
RAND()
)
AS Table_names_rand
)
AS Table_names_rand
ORDER BY
FIND_IN_SET(name, Table_names_rand.names)
Result
| name |
|-------|
| Carl |
| Phil |
| Wendy |
| Tina |
| Rick |
| Joe |
or
| name |
|-------|
| Tina |
| Rick |
| Joe |
| Carl |
| Phil |
| Wendy |
demo http://www.sqlfiddle.com/#!9/487ac9/28
If we strip away the randomness of the gorup ordering, your query would look like this:
SELECT
*
FROM
products
ORDER BY
set_id,
sort_id;
The ordering by set_id is necessary to "group" the results, without really grouping them. You do not want to group them, because then the rows with the same group would be aggregated, meaning that only one row per group would be put out.
Since you only want to randomize the groups, you need to write another query that assigns a random number to each group, like the one below:
SELECT
set_id,
RAND() as 'rnd'
FROM
products
GROUP BY
set_id
The GROUP BY clause makes sure, that each group is only selected once. The resultset will look like this:
| set_id | priority |
+--------+---------+
| AA | 0.21 |
| BB | 0.1 |
With that result we can then randomize the output, by combining both queries with a JOIN on the set_id field. This will add the randomly generated number from the second query to the result set of the first query and therefore extend the static set_id with the randomized, but still for all group members equal, rnd:
SELECT
products.*
FROM
products
JOIN (
SELECT
set_id,
RAND() as 'rnd'
FROM
products
GROUP BY
set_id
) as rnd ON rnd.set_id = products.set_id
ORDER BY
rnd.rnd,
products.set_id,
products.sort_id;
Keep in mind, that it is important to still group on products.set_id, because it may be possible that two groups get the same random number assigned. If the result would not be ordered by products.set_id those groups members would then be merged.

SQL alternative to double subquery

I have a table MyTable with values that look like this:
| id | name | type | category |
---------------------------------------------------
| 1 | Rob | Red | Rock |
| 2 | Rob | Blue | Rap |
| 2 | Rob | Blue | Rock |
| 3 | Jane | Green | Country |
| 3 | Jane | Green | Rap |
| 4 | Meg | Yellow | Rock |
| 5 | Jane | Blue | Rap |
| 5 | Jane | Blue | Rock |
| 6 | Jane | Red | Country |
| 6 | Jane | Red | Rock |
| 7 | Rob | Red | Rap |
| 7 | Rob | Red | Country |
| 8 | Meg | Green | Country |
| 9 | Meg | Blue | Rap |
Now, my issue resides in the fact that (as the data is given to me), there are duplicate ids. Each id stands for a report, so id of 1 is for report 1. In this report, there is a name, type, and category. The report can only have one type, but as many categories as it likes. Hence, the duplicate ids come from there being different categories, each constituting a new row. The end result i wish to achieve it to list the names in one row, along with all the types + count (where count is the count of the distinct types, as in one type per report) in the next row. It would look like such:
| Rob | Red(2), Blue(1) |
| Jane | Green(1), Blue(1), Red(1) |
| Meg | Yellow(1), Green(1), Blue(1)|
Now, I've developed a query that actually uses two subqueries and successfully achieves this result. It goes
select name as firstCol, group_concat(type_counts order by countName desc separator ', ') as secondCol from
(select name, concat(type,' (',count(name),')') as type_counts, count(name) as countName from
(select name, type from
some join stuff
where (date_reported between '2014-11-01' and '2014-11-31')
group by id order by type, name) a
group by name, type order by name, count(name) desc) a
group by name;
This query essentially groups by id first, to remove the duplicate ids and disregard the split due to differing categories. The query wrapping it then groups by name and traffic type, concatenating the traffic type and the count of names together as "type (count)". The third groups solely by name and group concats all of the types to have a column for the name, and then a second column with all of the type+counts listed and separated by commas. I was just wondering...is there a query that could make it faster, by not having to use so many subqueries and such? Thanks.
The end result i wish to achieve it to list the names in one row, along with all the types + count (where count is the count of the distinct types, as in one type per report) in the next row.
Use a subquery as an expression in the SELECT clause and another in the WHERE clause. For example:
SELECT B.Name, B.UserId AS [User Link], (SELECT Count(B2.Name) FROM Badges B2 WHERE B2.Name = B.Name) as [Total Gold, Silver, and Bronze Badges Awarded for this Tag]
FROM Badges B
WHERE Class = 2
AND TagBased = 1
AND (SELECT Count(B2.Name)
FROM Badges B2
WHERE B2.Name = B.Name
AND B2.Class = 2
AND B2.TagBased = 1) = 1
GROUP BY B.Name, B.UserId
ORDER BY B.Name
References
Users with their own Silver Tag Badges - Stack Exchange Data Explorer
Relational Algebra and SQL (pdf)
A Gentle Introduction to SQL