Multi-event tournament standings (with arbitrary number of entries) - mysql

Suppose you have a a multi-event competition where competitors can attempt any event an arbitrary number of times. (weird, I know.)
How do pull out a desired player's best time for each event,
and assign it a placing? (1st 2nd 3rd...)
Data example: Desired output:
Name | Event | Score Name | Event | Score | Rank
-------------------- ----------------------------
Bob 1 50 Given input: "Bob"
Bob 1 100 Bob 1 100 1
Bob 2 75 Bob 2 75 3
Bob 3 80 Bob 3 80 2
Bob 3 65
Given input: "Jill"
Jill 2 75 Jill 2 90 1
Jill 2 90 Jill 3 60 3
Jill 3 60
Given input: "Chris"
Chris 1 70 Chris 1 70 2
Chris 2 50 Chris 2 85 2
Chris 2 85 Chris 3 100 1
Chris 3 100
This is a build up of my previous question:
Multi-event tournament standings
I feel understand that problem much better (Thanks!), but I cannot bridge the gap to this version of the problem.
I have SQL 5.x so I cant use stuff like Rank(). This will also be crunching many thousands of scores.

Desired output can be acheaved with this query:
select
IF(event is NULL, CONCAT('Given input: "', name,'"'), name) as name,
IF(event is NULL, '', event) as event,
IF(event is NULL, '', max(score)) as score,
IF(event is NULL, '', (
select count(s2.name) + 1
from (
select name, max(score) as score
from scores es
where es.event = s.event
group by es.name
order by score desc
) s2
where s2.score > max(s.score)
)) as `rank`
from scores s
group by name, event with rollup
having name is not NULL
order by name, event;
And output (if run query in mysql cli):
+----------------------+-------+-------+------+
| name | event | score | rank |
+----------------------+-------+-------+------+
| Given input: "Bob" | | | |
| Bob | 1 | 100 | 1 |
| Bob | 2 | 75 | 3 |
| Bob | 3 | 80 | 2 |
| Given input: "Chris" | | | |
| Chris | 1 | 70 | 2 |
| Chris | 2 | 85 | 2 |
| Chris | 3 | 100 | 1 |
| Given input: "Jill" | | | |
| Jill | 2 | 90 | 1 |
| Jill | 3 | 60 | 3 |
+----------------------+-------+-------+------+
11 rows in set, 3 warnings (0.00 sec)
Should work on any Mysql 5.

You can get the highest score per event by an aggregation by event taking the max(). To simulate a dense_rank() you can use a subquery counting the scores higher than or equal to the current score per event.
For a particular contestant (here Bob) that makes:
SELECT d1.name,
d1.event,
max(d1.score) score,
(SELECT count(*)
FROM (SELECT d2.event,
max(d2.score) score
FROM data d2
GROUP BY d2.event,
d2.name) x1
WHERE x1.score >= max(d1.score)
AND x1.event = d1.event) rank
FROM data d1
WHERE d1.name = 'Bob'
GROUP BY d1.event
ORDER BY d1.event;
And for all of them at once:
SELECT d1.name,
d1.event,
max(d1.score) score,
(SELECT count(*)
FROM (SELECT d2.event,
max(d2.score) score
FROM data d2
GROUP BY d2.event,
d2.name) x1
WHERE x1.score >= max(d1.score)
AND x1.event = d1.event) rank
FROM data d1
GROUP BY d1.name,
d1.event
ORDER BY d1.name,
d1.event;
db<>fiddle

E.g.:
DROP TABLE IF EXISTS my_table;
CREATE TABLE my_table
(id SERIAL PRIMARY KEY
,name VARCHAR(12) NOT NULL
,event INT NOT NULL
,score INT NOT NULL
);
INSERT INTO my_table (name,event,score) VALUES
('Bob' ,1, 50),
('Bob' ,1,100),
('Bob' ,2, 75),
('Bob' ,3, 80),
('Bob' ,3, 65),
('Jill' ,2, 75),
('Jill' ,2, 90),
('Jill' ,3, 60),
('Chris',1, 70),
('Chris',2, 50),
('Chris',2, 85),
('Chris',3,100);
SELECT a.*
, FIND_IN_SET(a.score,b.scores) my_rank
FROM my_table a -- it's possible that this really needs to be a repeat of the subquery below, so
-- ( SELECT m.* FROM my_table m JOIN (SELECT name,event,MAX(score) score FROM my_table
-- GROUP BY name, event) n ON n.name = m.name AND n.event = m.event AND n.score = m.score) AS a
JOIN
(
SELECT x.event
, GROUP_CONCAT(DISTINCT x.score ORDER BY x.score DESC) scores
FROM my_table x
JOIN
( SELECT name
, event
, MAX(score) score
FROM my_table
GROUP
BY name
, event
) y
ON y.name = x.name
AND y.event = x.event
AND y.score = x.score
GROUP
BY x.event
) b
ON b.event = a.event
WHERE FIND_IN_SET(a.score,b.scores) >0;
+----+-------+-------+-------+------+
| id | name | event | score | rank |
+----+-------+-------+-------+------+
| 2 | Bob | 1 | 100 | 1 |
| 3 | Bob | 2 | 75 | 3 |
| 4 | Bob | 3 | 80 | 2 |
| 6 | Jill | 2 | 75 | 3 |
| 7 | Jill | 2 | 90 | 1 |
| 8 | Jill | 3 | 60 | 3 |
| 9 | Chris | 1 | 70 | 2 |
| 11 | Chris | 2 | 85 | 2 |
| 12 | Chris | 3 | 100 | 1 |
+----+-------+-------+-------+------+

Related

An interesting SQL query CHALLENGE - list the AVERAGE value of the top 3 scores of each athlete

An interesting SQL query CHALLENGE:
A table named athelets consisting of id, ath_id, name, score, date.
+----+--------+-----------------+--------+------------+
| id | ath_id | name | record | r_date |
+----+--------+-----------------+--------+------------+
| 1 | 2 | John Wayne | 79 | 2010-07-08 |
| 2 | 7 | Ronald Regan | 51 | 2000-03-22 |
| 3 | 1 | Ford Harrison | 85 | 2009-11-13 |
| 4 | 2 | John Wayne | 69 | 2017-01-01 |
Please write a sql query to list the average value of the top three scores of each athlete, something like:
ath_id: 1, the arithmetic mean of his/her top 3 records: 77
ath_id: 2, the arithmetic mean of his/her top 3 records: 73
ath_id: 3, the arithmetic mean of his/her top 3 records: 47
select ath_id, avg(record)
from
(select ath_id, record
from atheletes as t1
where
(select count(*) from atheletes where t1.ath_id=ath_id and record > t1.record) < 3) as d
group by ath_id;
The above query should works as expected.
Assuming combinations of athletes and records are unique...
SELECT ath_id
, ROUND(AVG(record),2) top3_avg
FROM
( SELECT x.*
FROM athletes x
JOIN athletes y
ON y.ath_id = x.ath_id
AND y.record >= x.record
GROUP
BY x.id
HAVING COUNT(*) <=3
) a
GROUP
BY ath_id;

MySQL - How to select rows with max value of a field

I have a table of users with their scores for each level of a game:
id | user_id | level | score
1 | David | 1 | 20
2 | John | 1 | 40
3 | John | 2 | 30
4 | Mark | 1 | 60
5 | David | 2 | 10
6 | David | 3 | 80
7 | Mark | 2 | 20
8 | John | 3 | 70
9 | David | 4 | 50
10 | John | 4 | 30
What is the SQL query needed to get for each level, who has the highest score?
The result should be:
id | user_id | level | score
4 | Mark | 1 | 60
3 | John | 2 | 30
6 | David | 3 | 80
9 | David | 4 | 50
Thank you
If you want to get ties, then you can do something like this:
select s.*
from scores s
where s.score = (select max(s2.score) from scores s2 where s2.level = s.level);
You could get one row per level by aggregating this:
select s.level, s.score, group_concat(s.user_id)
from scores s
where s.score = (select max(s2.score) from scores s2 where s2.level = s.level)
group by s.level, s.score;
This combines the users (if there is more than one) into a single field.
order by score desc in sub query, then select max(score) group by level.
select id, user_id , level , max(score) as score
from
(select * from scores order by score desc)A
group by level
If you only want the user, who reached the highest score first (no ties per level):
select *
from users u1
where id = (
select id
from users u2
where u2.level = u1.level
order by score desc, id asc
limit 1
)
You should have indexes (id) and (level, score, id)

How to find top 3 topper of each subject in given table

id - Name - Subject - Marks
1 - ABC - MAT - 90
2 - ABC - SCI - 80
3 - ABC - ENG - 90
4 - ABC - HIS - 96
5 - ABC - PHY - 70
6 - ABC - CHE - 43
7 - XYZ - MAT - 90
8 - XYZ - SCI - 80
9 - XYZ - ENG - 90
10 - XYZ - HIS - 96
11 - XYZ - PHY - 70
13 - XYZ - CHE - 43
etc .....
Just want to show 3 topper of each subject
ABC - MATH - 90
XYZ - MATH - 90
DEF - MATH - 80
etc
You can do this using variables.
select t.*
from (select t.*,
(#rn := if(#s = subject, #rn + 1,
if(#s := subject, 1, 1)
)
) as rn
from t cross join
(select #rn := 0, #s := '') params
order by subject, marks desc
) t
where rn <= 3
order by t.subject, t.rn;
With your data I create a SqlFiddleDemo.
I include two addtional student so query return only 3 from 4.
CREATE TABLE Courses
(`id` int, `Name` varchar(3), `Subject` varchar(3), `Marks` int);
And create this derivated to simplify the next code. The idea is create an unique code with 3 number 000-100 + Name so I can sorted during left outer join using AND L.comb <= R.comb.
Take note because this sorting in case of tie the latest name alphabetic will be showing first.
CREATE TABLE s_course AS
SELECT `id`, `Name`, `Subject`, `Marks`, concat(LPAD(`Marks`, 3, '0'), `Name`) as comb
FROM Courses;
Now the SELECT, if you run the inner select will see the 4 result and check how the ties are resolve.
SELECT *
FROM (
SELECT L.Subject, L.Marks, L.Name, count(*) as rn
FROM s_course L
left outer join s_course R
ON L.Subject = R.Subject
AND L.comb <= R.comb
GROUP BY L.Subject, L.comb
ORDER BY L.Subject, L.comb
) t
WHERE rn <= 3
ORDER BY Subject, rn
OR maybe you can ORDER BY Subject, Marks DESC, Name
This query exploit one issue of MySQL where you dont need put the same fields on select and group by
OUTPUT
| Subject | Marks | Name | rn |
|---------|-------|------|----|
| CHE | 48 | PQR | 1 |
| CHE | 48 | FGH | 2 |
| CHE | 43 | XYZ | 3 |
|---------|-------|------|----|
| ENG | 95 | PQR | 1 |
| ENG | 92 | FGH | 2 |
| ENG | 90 | XYZ | 3 |
|---------|-------|------|----|
| HIS | 96 | XYZ | 1 |
| HIS | 96 | ACB | 2 |
| HIS | 91 | PQR | 3 |
|---------|-------|------|----|
| MAT | 95 | PQR | 1 |
| MAT | 95 | FGH | 2 |
| MAT | 90 | XYZ | 3 |
|---------|-------|------|----|
| PHY | 75 | PQR | 1 |
| PHY | 70 | XYZ | 2 |
| PHY | 70 | ACB | 3 |
|---------|-------|------|----|
| SCI | 80 | XYZ | 1 |
| SCI | 80 | ACB | 2 |
| SCI | 75 | PQR | 3 |
select m1.id, max(m1.marks) 'marks', max(m1.subject) 'subject'
from marks m1
left join marks m2 on m2.subject =m1.subject and ( m2.marks>=m1.marks )
left join marks m3 on (m3.subject =m2.subject) and m3.id<>m1.id and m3.id<>m2.id and m3.marks>=m2.marks
left join marks m4 on (m4.subject =m3.subject) and m4.id<>m1.Id and m4.id<>m2.id and m4.id <>m3.id and m4.marks>=m3.marks
left join marks m12 on (m12.subject =m1.subject and m12.subject =m2.subject ) and m12.marks>m1.marks and m12.marks<m2.marks
left join marks m23 on (m23.subject=m2.subject and m23.subject=m3.subject) and ( m23.marks>m2.marks and m23.marks < m3.marks)
where
(m4.id is null)
and m12.id is null
and m23.id is null
group by m1.id
order by max(m1.subject) asc, max(m1.marks) desc

SQL Group BY using strings in new columns

I have a database of race results. Each race event has multiple classes.
events table:
event_id | event_date
---------+------------
1 | 11/5/14
2 | 11/12/14
3 | 11/19/14
results table:
result_event | name | class | position
-------------+---------------+-------+-----------
1 | Jason Smith | 40 | 1
1 | David Johnson | 40 | 2
1 | Randy White | 30 | 1
1 | Billy Hansen | 30 | 2
2 | Wally Mann | 40 | 1
2 | Shawn Little | 40 | 2
2 | Eric Davis | 30 | 1
2 | Tom Handy | 30 | 2
I want to create a summary table that lists the Event Date and the winners of each class.
Like this:
Event Date | Class 40 Winner | Class 30 Winner
------------+-----------------+------------------
11/5/14 | Jason Smith | Randy White
11/12/14 | Wally Mann | Eric Davis
What query would I need so that I can create a GROUP BY event_id and list winners in separate columns?
You can join the events table on two queries from the results table, one for each class:
SELECT event_data, class_40_winner, class_30_winner
FROM events e
LEFT JOIN (SELECT result_event, name AS class_40_winner
FROM results
WHERE class = 40 AND position = 1) c40 ON e.id = c40.result_event
LEFT JOIN (SELECT result_event, name AS class_30_winner
FROM results
WHERE class = 30 AND position = 1) c30 ON e.id = c30.result_event
You are querying like pivoting data, so I suggest you to use a query like this:
select event_date
, max(case when r.class = 40 then name end) `Class 40 Winner`
, max(case when r.class = 30 then name end) `Class 30 Winner`
from events e
left join results r on e.event_id = r.result_event and r.position = 1
group by event_date;
[SQL Fiddle Demo]

MySQL - Student grade list, replace small grade by bonus grade

SELECT TestID, Grade FROM tests_points;
Returns:
+--------+-------+
| TestID | Grade |
+--------+-------+
| 10 | 125 |
| 11 | 110 |
| 12 | 100 |
| 13 | 75 |
| 14 | 50 |
| 15 | 65 |
| 16 | 70 |
| 17 | 100 |
| 18 | 100 |
+--------+-------+
But, tests ID 17 and 18 are "bonus tests", so I need replace the two lower grades by these two ones, and return the SUM of all grades.
So, how I can "replace"the two lower grades (From TestID 14 and 15) by testID 17 and 18 grades.
The "correct grade list" would be:
+--------+-------+
| TestID | Grade |
+--------+-------+
| 10 | 125 |
| 11 | 110 |
| 12 | 100 |
| 13 | 75 |
| 14 | 100|
| 15 | 100|
| 16 | 70 |
+--------+-------+
In the end I just need the SUM of all grades, fixing the lower grades.
SELECT SUM(Grade) FROM tests_points;
How can I do that?
This is what you need!
select testID, GREATEST(
IF(
grade = (select min(grade) from test_points),(select grade from test_points where testID = 17),
grade)
,
IF(grade = (select min(grade) from test_points WHERE grade > ( SELECT min(grade) FROM test_points))
,(select grade from test_points where testID = 18),
grade)) as Score
from test_points
where testID not in(17, 18)
Demo on SQLfiddle.com
the following query will get probably get what you want..
SELECT ttal-garba FROM
(SELECT sum(c.grade) as garba FROM (SELECT * from tests_points where testid < 17 order by grade asc limit 2) as c) as a,
(SELECT sum(grade) as ttal FROM tests_points) as b
(I know.. hard coding everything is bad ..)
If your just trying to add up all the values except the bottom two you can do something lik,e this.
First add a new field lets say test_type:
UPDATE test_points
SET test_type = 'test';
Now we can just select using group by the sum. For example
SELECT test_type, (SUM(Grade) OVER (ORDER BY Grade DESC))
FROM test_Points
LIMIT 7;
This will return the sum of all values, but it will show 7 columns. If you want only the total, Throw a group by into the mix, and it will narrow it down to one field, or just select the 7th row number.