MySQL FROM Subquery with parent group by - mysql

I need to show a ranking lists for a sport we manage.
It needs to sum up the 4 best results for each player (from a table that could have hundreds of results per player) and then sort the entire list from the player with the most points to least points.
The query below returns
ERROR 1054 (42S22): Unknown column 'r1.user_id' in 'where clause'
so I've gone off track somewhere.
SELECT r1.user_id, (
SELECT SUM(points)
FROM (
SELECT *
FROM ranking_matrix_points r2
WHERE user_id=r1.user_id
ORDER BY points DESC
LIMIT 4
) r3
) AS total_points
FROM ranking_matrix_points r1
WHERE
user.status IN('active')
GROUP BY r1.user_id
ORDER BY total_points DESC

One possible solution might be to number the rows for each user in order of points descending and then sum up the points with a rank <= 4. This might not perform very well though, and also you'll get a problem with ties (but you would have using limit too).
select user_id, sum(points) total_points
from (
select user_id, points,
(
case user_id
when #cur_user
then #row := #row + 1
else #row := 1 and #cur_user := user_id end
) as rank
from ranking_matrix_points,
(select #row := 0, #cur_user := '') r
order by user_id, points desc
) t
where rank <= 4
group by user_id;
I'm pretty sure there are better ways to do this but I can't think of any at the moment. This would have been very easy in just about any database with support for window functions, but sadly MySQL doesn't support any yet.

You don't need a double query, just
SELECT user_id, SUM(points)
FROM ranking_matrix_points
WHERE user.status in('active')
GROUP BY user_id
ORDER BY total_points DESC
LIMIT 4
or
SELECT TOP 4 user_id, SUM(points)
FROM ranking_matrix_points
WHERE user.status in('active')
GROUP BY user_id
ORDER BY total_points DESC

Related

Order by highest value alternating with lowest value

I currently need to order data by highest value down, and then lowest value up, in between.
My Query is close, but doesn't quite order by largest down, though it is inserting the lowest in between:
DEMO Fiddle
select users.*
from users CROSS JOIN (select #even := 0, #odd := 0) param
order by
IF(score > 1, 2*(#odd := #odd + 1), 2*(#even := #even + 1) + 1),
score DESC;
Current Results
Email Score
----- --------
foo1#gmail.com 42
foo5#gmail.com 1
foo2#gmail.com 49
foo6#gmail.com 0
foo3#gmail.com 37
foo4#gmail.com 7
foo#gmail.com 22
Desired Results
Email Score
----- --------
foo2#gmail.com 49
foo6#gmail.com 0
foo1#gmail.com 42
foo5#gmail.com 1
foo3#gmail.com 37
foo4#gmail.com 7
foo#gmail.com 22
You can achieve using MySQL but avoid such complex SQL statements as the same can be achieved using programming language very easily.
SET #totalRows := (CASE WHEN (SELECT COUNT(*) FROM users) IS NULL THEN 0 ELSE (SELECT COUNT(*) FROM users) END);
PREPARE stmt1 FROM '(SELECT t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY score DESC) AS row_num, email, score FROM users UNION ALL SELECT ROW_NUMBER() OVER (ORDER BY score ASC) AS row_num, email, score FROM users) AS t ORDER BY t.row_num, t.score DESC LIMIT 0,?)';
EXECUTE stmt1 USING #totalRows;
Here is the explanation to achieve it:
Set variable #totalRows contains total rows in users table as that many rows will be displayed as a final result set.
Used ROW_NUMBER() function of MySQL to set ordering based on SOCRE field DESCENDING and ASCENDING for another result set.
Combined both the result set using UNION ALL statement of MySQL
Add the LIMIT keyword to make sure the final result must have a total number of rows that should not exceed #totalRows variable.
As in MySQL LIMIT statement we can't pass a dynamic value at a query level. I used the approach of prepare a statement.
You can ignore row_num column I used as final result set.
Hope my solution will help you.
For MySql 8.0+ you can use ROW_NUMBER() window function:
SELECT email, score
FROM (
SELECT *,
ROW_NUMBER() OVER (ORDER BY score DESC) rn1,
ROW_NUMBER() OVER (ORDER BY score ASC) rn2
FROM users
) t
ORDER BY LEAST(rn1, rn2), rn1;
For previous versions you can simulate ROW_NUMBER() with correlated subqueries (with the cost of poor performance for large datasets):
SELECT email, score
FROM (
SELECT u1.*,
(SELECT COUNT(*) FROM users u2 WHERE u2.score > u1.score) rn1,
(SELECT COUNT(*) FROM users u2 WHERE u2.score < u1.score) rn2
FROM users u1
) t
ORDER BY LEAST(rn1, rn2), rn1;
See the demo.

Ranking position without duplicates

I have an issue with a query,
this is my old question that was previously solved
mysql get ranking position grouped with joined table
The problem is that when two players have the same score, the query returns the same ranking position, like 1-1-2-3 ecc. How can I fix this?
In the player's table there are also player_tickets (that is the number of game played) and player_date that is a timestamp.
I thought to get my ranking on base of player_score first, player_tickets then, and finally player_date
This is my older query
SELECT *,
(SELECT 1 + Count(*)
FROM players p2
WHERE p2.`player_game` = p.`player_game`
AND p2.player_score > p.player_score
AND p2.player_status = 0) AS ranking
FROM players p
ORDER BY `player_game`,
player_score DESC
You can simply add more columns to the order by of the window function:
rank() over (
partition by player_game_id
order by player_score desc, player_tickets desc, player_date
) as rank
If you really want to avoid having the same rank twice, you can also use row_number(), which guarantees this - when there are ties, row_number() affects distinct numbers (whose order is hence undefined).
Just add the ranking criteria to your WHERE clause:
SELECT *,
(
SELECT 1 + COUNT(*)
FROM players p2
WHERE p2.player_game = p.player_game
AND
(
(p2.player_score > p.player_score) OR
(p2.player_score = p.player_score AND p2.player_tickets > p.player_tickets) OR
(p2.player_score = p.player_score AND p2.player_tickets = p.player_tickets AND p2.player_date > p.player_date) OR
(p2.player_score = p.player_score AND p2.player_tickets = p.player_tickets AND p2.player_date = p.player_date AND p2.player_id > p.player_id)
)
) AS ranking
FROM players p
ORDER BY player_game, player_score DESC;
You can add additional comparisons in the subquery. Or more easily, use variables:
select p.*,
#rn := if(#g = player_game, #rn + 1,
if(#g := player_game, 1, 1)
) as ranking
from (select p.*
from players p
order by player_game, player_score desc, player_tickets desc, player_date desc
) p cross join
(select #rn := 0, #g := 0) as seqnum;
In newer versions, you would just use row_number() if you don't want ties.

Get maximum value from two values

I have a table which gives the no of rides by a rider at each stand point. I need to find the stand for each rider for which he has the maximum rides.
My first result is in this format: 1
I require my final result like this: 2
I'm currently using this query, but I know it can be done in a better manner. Any suggestions would be helpful.
select c.rider_id, c.end_stand, b.max_rides
from
(select rider_id, max(rides) as max_rides
from
(select rider_id, end_stand, count(id) as rides
from ride where end_stand is not null
group by 1,2) a
group by 1
order by 2 desc, 1) b
join
(select rider_id, end_stand, count(id) as rides
from ride where end_stand is not null
group by 1,2) c
on c.rider_id = b.rider_id and c.rides = b.max_rides
order by 3 desc, 2,1
Before window functions, one method is a correlated subquery in the having clause:
select rider_id, end_stand, count(*) as rides
from ride r
where end_stand is not null
group by rider_id, end_stand
having count(*) = (select count(*)
from ride r2
where r2.end_stand is not null and
r2.rider_id = r.rider_id
group by r2.rider_id, r2.end_stand
order by count(*) desc
limit 1
);
With window functions, this is, of course, much simpler:
select *
from (select rider_id, end_stand, count(*) as rides
rank() over (partition by rider_id order by count(*) desc) as seqnum
from ride r
where end_stand is not null
group by rider_id, end_stand
) r
where seqnum = 1;
Both these will return duplicates, if there are ties for the max. The second version is easy to fix, if you want only one row: use row_number() instead of rank().

how to find index ( id ) of generic column in mysql

I have a table with name rating as this.
I am writing the query like this
SELECT user_id, sum(score) as score
FROM quiz_rashad.rating
group by user_id
order by score desc
then how I get rating index of the 12th user?
Thank you for helping.
Assuming, that "the 12th user" means the user with the ID 12:
In MySQL 8.0+ you can use dense_rank().
SELECT x.rating_index
FROM (SELECT r.user_id,
dense_rank() OVER (ORDER BY sum(r.score) DESC) rating_index
FROM quiz_rashad.rating r
GROUP BY r.user_id) x
WHERE x.user_id = 12;
Edit:
For MySQL 5.7 you have to use subqueries getting the distinct count of total scores greater than or equal the total score for the user with ID 12.
SELECT count(DISTINCT x.score) rating_index
FROM (SELECT r.user_id,
sum(r.score) score
FROM quiz_rashad.rating r
GROUP BY r.user_id) x
WHERE x.score >= (SELECT sum(r.score)
FROM quiz_rashad.rating r
WHERE r.user_id = 12)
We can try using LIMIT with OFFSET here:
SELECT user_id, SUM(score) AS score
FROM quiz_rashad.rating
GROUP BY user_id
ORDER BY score DESC
LIMIT 1 OFFSET 11;
This answer assumes that what you really want here is the record with the twelfth rank. It also assumes that no two users would be tied for the same score.

Possible to add auto incrementing variable to row results of GROUP BY in mysql?

Basically, I'm using this query to group a bunch of users based on the sum of numbers associated with them. I need to some how assign an index to each result. I am blanking on how to do this. I'm thinking I need to alias something with AS but not sure quite how. Any ideas?
This is the current query where I switch out the page and per dynamically:
SELECT COUNT(*) as count, user_id, SUM(earnings) as sum FROM ci_league_result
GROUP BY user_id ORDER BY sum desc LIMIT ".$page.', '.$per;
I'm lookin for it to work something like this:
SELECT COUNT(*) as count, user_id, SUM(earnings) as sum, *NEW-RESULTS-OVERALL-INDEX* AS newindex FROM ci_league_result
GROUP BY user_id ORDER BY sum desc LIMIT ".$page.', '.$per;
notice the AS newindex in the second query.
Thanks for your advice!
Try this:
SELECT user_id, count, sum, #row := #row + 1 AS newindex FROM
(SELECT
COUNT(*) as count,
user_id,
SUM(earnings) as sum
FROM ci_league_result
GROUP BY user_id ORDER BY sum desc LIMIT ".$page.', '.$per) r
CROSS JOIN (SELECT #row := 0) rr;
EDITED
You should be able to accomplish this with SQL variables
SELECT
COUNT(*) as count,
user_id,
SUM(earnings) as sum,
#rownum := #rownum+1 AS newindex
FROM ci_league_result,
(SELECT #rownum:=0) r
GROUP BY user_id
ORDER BY sum DESC
LIMIT ".$page.', '.$per;