Mysql Ranking within grouped results - mysql

I have read posts that answer how to rank results in mysql, but my question is how to assign ranks within a group
Let me explain with an example
Data:
sem_id | result_month
--------------------
1 |1313907325000
1 |1345529725000
2 |1329804925000
2 |1361427325000
3 |1377065725000
3 |1440137725000
What i am able to achieve with the below query:
SELECT #ss := #ss + 1 AS rank,
res.sm_id,
res.result_month
FROM (SELECT sm_id, result_month
FROM xx_table
GROUP BY sm_id,
result_month) AS res,(SELECT #ss := 0) AS ss;
Current results:
rank | sem_id | result_month
----------------------------
1 | 1 |1313907325000
2 | 1 |1345529725000
3 | 2 |1329804925000
4 | 2 |1361427325000
5 | 3 |1377065725000
6 | 3 |1440137725000
What I actually want :
rank | sem_id | result_month
----------------------------
1 | 1 |1345529725000
2 | 1 |1313907325000
1 | 2 |1361427325000
2 | 2 |1329804925000
1 | 3 |1440137725000
2 | 3 |1377065725000
In the above results things to observe is each group is ranked within itself and each group is ordered by result_month desc
Help me on how can i achieve the above results
Thanks in advance!

You're almost there, use another variable to compute group:
SELECT #ss := CASE WHEN #grp = sem_id THEN #ss + 1 ELSE 1 END AS rank, sem_id, result_month, #grp := sem_id
FROM (select * from xx_table ORDER BY sem_id, result_month DESC) m
CROSS JOIN (SELECT #ss := 0, #grp = null) ss
See demo here.

Related

How to reference generated/aliased table in same query?

I want to find a user's position in a leaderboard and return the 4 users above and 4 users below their position.
My table, 'predictions', looks something like this:
+----+---------+--------+-------+---------+
| id | userId | score | rank | gameId |
+----+---------+--------+-------+---------+
| 1 | 12 | 11 | 1 | 18 |
| 2 | 1 | 6 | 4 | 18 |
| 3 | 43 | 7 | 3 | 12 |
| 4 | 4 | 9 | 2 | 18 |
| 5 | 98 | 2 | 5 | 19 |
| 6 | 3 | 0 | 6 | 18 |
+----+---------+--------+-------+---------+
Obviously this isn't properly ordered, so I run this:
SELECT l.userId,
l.rank,
l.score,
l.createdAt,
#curRow := #curRow + 1 AS row_number
FROM (SELECT * FROM `predictions` WHERE gameId = 18) l
JOIN (SELECT #curRow := 0) r
ORDER BY rank ASC
which gets me a nice table with each entry numbered.
I then want to search this generated table, find the row_number where userId = X, and then return the values 'around' that result.
I think I have the logic of the query down, I just can't work out how to reference the table 'generated' by the above query.
It would be something like this:
SELECT *
FROM (
SELECT l.userId,
l.rank,
l.score,
l.createdAt,
#curRow := #curRow + 1 AS row_number
FROM (SELECT * FROM `predictions` WHERE gameId = 18) l
JOIN (SELECT #curRow := 0) r
ORDER BY rank ASC) generated_ordered_table
WHERE row_number < (SELECT row_number FROM generated_ordered_table WHERE userId = 1)
ORDER BY row_number DESC
LIMIT 0,5
This fails. What I'm trying to do is to generate my first table with the correct query, give it an alias of generated_ordered_table, and then reference this 'table' later on in this query.
How do I do this?
MySQL version 8+ could have allowed the usage of Window functions, and Common Table Expressions (CTEs); which would have simplified the query quite a bit.
Now, in the older versions (your case), the "Generated Rank Table" (Derived Table) cannot be referenced again in a subquery inside the WHERE clause. One way would be to do the same thing twice (select clause to get generated table) again inside the subquery, but that would be relatively inefficient.
So, another approach can be to use Temporary Tables. We create a temp table first storing the ranks. And, then reference that temp table to get results accordingly:
CREATE TEMPORARY TABLE IF NOT EXISTS gen_rank_tbl AS
(SELECT l.userId,
l.rank,
l.score,
l.createdAt,
#curRow := #curRow + 1 AS row_number
FROM (SELECT * FROM `predictions` WHERE gameId = 18) l
JOIN (SELECT #curRow := 0) r
ORDER BY rank ASC)
Now, you can reference this temp table to get the desired results:
SELECT *
FROM gen_rank_tbl
WHERE row_number < (SELECT row_number FROM gen_rank_tbl WHERE userId = 1)
ORDER BY row_number DESC
LIMIT 0,5
You could use a bunch of unions
select userid,rank,'eq'
from t where gameid = 18 and userid = 1
union
(
select userid,rank,'lt'
from t
where gameid = 18 and rank < (select rank from t t1 where t1.userid = 1 and t1.gameid = t.gameid)
order by rank desc limit 4
)
union
(
select userid,rank,'gt'
from t
where gameid = 18 and rank > (select rank from t t1 where t1.userid = 1 and t1.gameid = t.gameid)
order by rank desc limit 4
);
+--------+------+----+
| userid | rank | eq |
+--------+------+----+
| 1 | 4 | eq |
| 4 | 2 | lt |
| 12 | 1 | lt |
| 3 | 6 | gt |
+--------+------+----+
4 rows in set (0.04 sec)
But it's not pretty
You can use two derived tables:
SELECT p.*,
(#user_curRow = CASE WHEN user_id = #x THEN rn END) as user_rn
FROM (SELECT p.*, #curRow := #curRow + 1 AS rn
FROM (SELECT p.*
FROM predictions p
WHERE p.gameId = 18
ORDER BY rank ASC
) p CROSS JOIN
(SELECT #curRow := 0, #user_curRow := -1) params
) p
HAVING rn BETWEEN #user_curRow - 4 AND #user_currow + 4;

Rank users in mysql by their points

I am trying to rank my students by their points that I've calculated before
but the problem is if students have same points they both should be in same rank
E.g
Student 1 has full points
Student 2 has full points
they both have to be rank as 1;
Here an example of my database
the query I am trying to do is (just for select then I can insert the values to my column)
SELECT a.points
count(b.points)+1 as rank
FROM examresults a left join examresults b on a.points>b.points
group by a.points;
Edit for being more clear:
Student 1 points 80
Student 2 points 77.5
Student 3 points 77.5
Student 4 points 77
their ranks should be like
Student 1 Rank 1
Student 2 Rank 2
Student 3 Rank 2
Student 4 Rank 3
my current query returns a values like
As it is missing the third rank. (because second rank has 2 values)
This is just a fix of Gordon solution using variables. The thing is your rank function isnt the way rank should work. (student 4 should be rank 4)
SQL Fiddle Demo You can add more student to improve the testing.
select er.*,
(#rank := if(#points = points,
#rank,
if(#points := points,
#rank + 1,
#rank + 1
)
)
) as ranking
from students er cross join
(select #rank := 0, #points := -1) params
order by points desc;
OUTPUT
| id | points | ranking |
|----|--------|---------|
| 1 | 80 | 1 |
| 2 | 78 | 2 |
| 3 | 78 | 2 |
| 4 | 77 | 3 |
| 5 | 66 | 4 |
| 6 | 66 | 4 |
| 7 | 66 | 4 |
| 8 | 15 | 5 |
You want a real rank, which is calculated by the ANSI standard rank() function. You can implement this in MySQL using this logic:
select er.*,
(select 1 + count(*)
from examresults er2
where er2.points > er.points
) as ranking
from exampleresults er;
For larger tables, you can do this with variables, but it is a rather awkward:
select er.*,
(#rank := if(#rn := #rn + 1 -- increment row number
if(#points = points, #rank, -- do not increment rank
if(#points := points, -- set #points
#rn, #rn -- otherwise use row number
)
)
)
) as ranking
from examresults er cross join
(select #rn := 0, #rank := 0, #points := -1) params
order by points desc;
this query achieve what do you want:
SELECT student_id , points, (select count(distinct(points))+1 as rank
from examresults internal
where internal.points > external.points order by points)
FROM examresults external
group by student_id

Get average of multiple ranks in mysql

I am trying to calculate the spearmans rank correlation for some data in mysql. For this I need to rank my data on a descending order. I got this working but when 2 rows have the same variable the rank should be the average of the 2 or more ranks.
As an example here is some example data with the current ranks and the expected ranks
| id|var|rank|
| 8 | 1 | 1 |
| 2 | 2 | 2 | # rank should be 2.5
| 6 | 2 | 3 | # rank should be 2.5
| 4 | 3 | 4 |
| 5 | 4 | 5 |
| 1 | 5 | 6 |
| 3 | 6 | 7 | # rank should be 8
| 7 | 6 | 8 | # rank should be 8
| 9 | 6 | 9 | # rank should be 8
My query looks like this right now:
SET #rownum := 0;
SET #rownum2 := 0;
SELECT rank_x.id, rank_x.var1, rank_x.rk_x
FROM
(SELECT id, #rownum := #rownum + 1 AS rk_x, var1
FROM sampledata order by var1 asc) as rank_x;
You can do this by assigning the sequential number and then taking the average. This requires some nested subqueries, but is doable. The idea is:
First assign the sequential value
Then find the max for each id.
Then find the min
Then take the average
The query looks like:
SELECT id, var1, (minrn + maxrn) / 2
FROM (SELECT sd.*,
(#maxrn := if(#v2 = var1, #maxrn,
if(#v2 := var1, rn, rn)
)
) as maxrn
FROM (SELECT sd.*,
(#minrn := if(#v = var1, #minrn,
if(#v := var1, rn, rn)
)
) as minrn
FROM (SELECT id, var1, (#rn := #rn + 1) as rn
FROM sampledata sd CROSS JOIN
(SELECT #rn := 0) vars
ORDER BY var1 asc
) sd CROSS JOIN
(SELECT #minrn := 0, #v := -1) vars
ORDER BY var1, rn
) sd CROSS JOIN
(SELECT #maxrn := 0, #v2 := -1) vars
ORDER BY var1, rn desc
) sd;

SQL Find Position in table

I have a table in mySql which has the users ID and scores.
What I would like to do is organise the table by scores (simple) but then find where a certain user ID sits in the table.
So far I would have:
SELECT * FROM table_score
ORDER BY Score DESC
How would I find where userID = '1234' is (i.e entry 10 of 12)
The following query will give you a new column UserRank, which specify the user rank:
SELECT
UserID,
Score,
(#rownum := #rownum + 1) UserRank
FROM table_score, (SELECT #rownum := 0) t
ORDER BY Score DESC;
SQL Fiddle Demo
This will give you something like:
| USERID | SCORE | USERRANK |
-----------------------------
| 4 | 100 | 1 |
| 10 | 70 | 2 |
| 2 | 55 | 3 |
| 1234 | 50 | 4 |
| 1 | 36 | 5 |
| 20 | 33 | 6 |
| 8 | 25 | 7 |
Then you can put this query inside a subquery and filter with a userId to get that user rank. Something like:
SELECT
t.UserRank
FROM
(
SELECT *, (#rownum := #rownum + 1) UserRank
FROM table_score, (SELECT #rownum := 0) t
ORDER BY Score DESC
) t
WHERE userID = '1234';
SQL Fiddle Demo
For a given user id, you can do this with a simple query:
select sum(case when ts.score >= thescore.score then 1 else 0 end) as NumAbove,
count(*) as Total
from table_score ts cross join
(select ts.score from table_score ts where userId = '1234') thescore
If you have indexes on score and userid, this will be quite efficient.

How to compute ranks in mysql?

I have a table 'student_marks' with two columns 'student_id' and 'mark':
student_id | marks
-------------------
1 | 5
2 | 2
3 | 5
4 | 1
5 | 2
I need to compute the rank corresponding to the marks. The expected output for the above table is:
student_id | marks | rank
-------------------------
1 | 5 | 1
2 | 2 | 3
3 | 5 | 1
4 | 1 | 5
5 | 2 | 3
Since the two students with students_id 1 and 3 has highest mark 5, they are placed in rank 1. For students with marks 2, the rank is 3 as there are two students who has more marks then these guys.
How do we write queries to compute the ranks as shown above?
This should work although it's heavy on variables.
SELECT student_id, mark, rank FROM (
SELECT t.*,
#rownum := #rownum + 1 AS realRank,
#oldRank := IF(mark = #previous,#oldRank,#rownum) AS rank,
#previous := mark
FROM student_marks t,
(SELECT #rownum := 0) r,
(SELECT #previous := 100) g,
(SELECT #oldRank := 0) h
ORDER BY mark DESC
) as t
ORDER BY student_id;
Look at this fiddle: http://sqlfiddle.com/#!2/2c7e5/32/0