Rank users in mysql by their points - mysql

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

Related

How to convert this query from MySQL to SQL Server?

Here is the source table:
+----+-------+
| Id | Score |
+----+-------+
| 1 | 3.50 |
| 2 | 3.65 |
| 3 | 4.00 |
| 4 | 3.85 |
| 5 | 4.00 |
| 6 | 3.65 |
+----+-------+
Here is the result table:
+-------+------+
| Score | Rank |
+-------+------+
| 4.00 | 1 |
| 4.00 | 1 |
| 3.85 | 2 |
| 3.65 | 3 |
| 3.65 | 3 |
| 3.50 | 4 |
+-------+------+
I have the MySQL version query, how to convert it to SQL server version? I tried to do the declare but I have no idea how to update the value of the variables.
SELECT Score, ranking AS Rank
FROM
(
SELECT
Score,
CASE
WHEN #dummy = Score
THEN #ranking := #ranking
ELSE #ranking := #ranking + 1
END as ranking,
#dummy := Score
FROM Scores, (SELECT #ranking := 0, #dummy := -1) init
ORDER BY Score DESC
)AS Result
Your code is a MySQL work-around for the ANSI standard DENSE_RANK() function (as explained by Sean Lange in a comment). The code simply looks like:
SELECT s.*, DENSE_RANK() OVER (ORDER BY Score DESC) as rank
FROM Scores s
ORDER BY Score DESC;
Incidentally, the MySQL code itself is not really accurate. The following is much safer:
SELECT Score, ranking AS Rank
FROM (SELECT Score,
(#rn := if(#dummy = Score, #rn,
if(#dummy := Score, #rn + 1, #rn + 1)
)
) as ranking
FROM Scores CROSS JOIN
(SELECT #rn := 0, #dummy := -1) init
ORDER BY Score DESC
) s;
The key difference is that all the assignments to variables occur in a single expression. MySQL does not guarantee the order of evaluations of expressions in a SELECT, so you should not use #dummy in one expression and then assign it in another.
SQL Server doesn't support variables used in that manner, so the syntax doesn't translate. Scalar variables are set once by a query, not set once for each row of the result set. MySQL's syntax here is a hack to get analytic-function-like behavior without analytic function support. You should just use:
SELECT Score,
DENSE_RANK() OVER(ORDER BY Score DESC) AS Rank
FROM Scores
ORDER BY Score DESC;
If you insist on not using DENSE_RANK(), you can use the SQL Server syntax from SQL Server 2000:
SELECT s1.Score,
(SELECT COUNT(DISTINCT s2.Score) FROM Scores s2 WHERE s1.Score <= s2.Score) AS Ranking
FROM Scores s1
ORDER BY s1.Score DESC;

Ranking Where Clause

I have a problem with my Query. I want to select data from my Ranking Query.
My query output is Perfect Like:
------------------------------
Rank | ID | Username | Value
-------------------------------
1 | 5 | Julian | 5000
2 | 2 | Masha | 2400
3 | 4 | Misha | 2300
4 | 1 | Jackson | 1900
5 | 9 | Beruang | 400
-------------------------------
But when I select ID = 4, the output like this:
------------------------------
Rank | ID | Username | Value
-------------------------------
***1*** | 4 | Misha | 2300
-------------------------------
The output of ranking is 1, not 3.
My Query is :
SELECT #curRank := #curRank + 1 AS rank,
a.id, a.username
FROM partimer a CROSS JOIN
(SELECT #curRank := 0) vars
# WHERE a.id = 4
ORDER By id;
If Rank is dinamically computed, you could do this:
SELECT *
FROM (
SELECT #curRank := #curRank + 1 AS rank
, a.id
, a.username
FROM partimer a
CROSS JOIN (SELECT #curRank := 0) vars
ORDER BY value
) p
WHERE p.id = 4;
This way, you store temporary table with rank, and then select from it.
you should like this
SELECT *
FROM (
...{your Query}...
) qry
WHERE qry.id = 4
Your rank is calculated dynamically in the query. The issue here is that these dynamic calculations are applied after the where clause. In other words, when MySQL executes your query, it first filters out all the rows that adhere to the where clause, and only then applies the rank calculation. In the given query, the row with id=4 is indeed the 1st row between all the rows that adhere to the where clause.
One way to get your desired behavior is to perform the original query first and only then filter the results by using this query as a subquery and applying the where clause to the outer query:
SELECT *
FROM (SELECT #curRank := #curRank + 1 AS rank, a.id, a.username
FROM partimer a
CROSS JOIN (SELECT #curRank := 0) vars
ORDER By id) t
WHERE id = 4

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;

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

Update the rank in a MySQL Table

I have the following table structure for a table Player
Table Player {
Long playerID;
Long points;
Long rank;
}
Assuming that the playerID and the points have valid values, can I update the rank for all the players based on the number of points in a single query? If two people have the same number of points, they should tie for the rank.
UPDATE:
I'm using hibernate using the query suggested as a native query. Hibernate does not like using variables, especially the ':'. Does anyone know of any workarounds? Either by not using variables or working around hibernate's limitation in this case by using HQL?
One option is to use a ranking variable, such as the following:
UPDATE player
JOIN (SELECT p.playerID,
#curRank := #curRank + 1 AS rank
FROM player p
JOIN (SELECT #curRank := 0) r
ORDER BY p.points DESC
) ranks ON (ranks.playerID = player.playerID)
SET player.rank = ranks.rank;
The JOIN (SELECT #curRank := 0) part allows the variable initialization without requiring a separate SET command.
Further reading on this topic:
SQL: Ranking without self join
Stack Overflow: Create a Cumulative Sum Column in MySQL
Test Case:
CREATE TABLE player (
playerID int,
points int,
rank int
);
INSERT INTO player VALUES (1, 150, NULL);
INSERT INTO player VALUES (2, 100, NULL);
INSERT INTO player VALUES (3, 250, NULL);
INSERT INTO player VALUES (4, 200, NULL);
INSERT INTO player VALUES (5, 175, NULL);
UPDATE player
JOIN (SELECT p.playerID,
#curRank := #curRank + 1 AS rank
FROM player p
JOIN (SELECT #curRank := 0) r
ORDER BY p.points DESC
) ranks ON (ranks.playerID = player.playerID)
SET player.rank = ranks.rank;
Result:
SELECT * FROM player ORDER BY rank;
+----------+--------+------+
| playerID | points | rank |
+----------+--------+------+
| 3 | 250 | 1 |
| 4 | 200 | 2 |
| 5 | 175 | 3 |
| 1 | 150 | 4 |
| 2 | 100 | 5 |
+----------+--------+------+
5 rows in set (0.00 sec)
UPDATE: Just noticed the that you require ties to share the same rank. This is a bit tricky, but can be solved with even more variables:
UPDATE player
JOIN (SELECT p.playerID,
IF(#lastPoint <> p.points,
#curRank := #curRank + 1,
#curRank) AS rank,
#lastPoint := p.points
FROM player p
JOIN (SELECT #curRank := 0, #lastPoint := 0) r
ORDER BY p.points DESC
) ranks ON (ranks.playerID = player.playerID)
SET player.rank = ranks.rank;
For a test case, let's add another player with 175 points:
INSERT INTO player VALUES (6, 175, NULL);
Result:
SELECT * FROM player ORDER BY rank;
+----------+--------+------+
| playerID | points | rank |
+----------+--------+------+
| 3 | 250 | 1 |
| 4 | 200 | 2 |
| 5 | 175 | 3 |
| 6 | 175 | 3 |
| 1 | 150 | 4 |
| 2 | 100 | 5 |
+----------+--------+------+
6 rows in set (0.00 sec)
And if you require the rank to skip a place in case of a tie, you can add another IF condition:
UPDATE player
JOIN (SELECT p.playerID,
IF(#lastPoint <> p.points,
#curRank := #curRank + 1,
#curRank) AS rank,
IF(#lastPoint = p.points,
#curRank := #curRank + 1,
#curRank),
#lastPoint := p.points
FROM player p
JOIN (SELECT #curRank := 0, #lastPoint := 0) r
ORDER BY p.points DESC
) ranks ON (ranks.playerID = player.playerID)
SET player.rank = ranks.rank;
Result:
SELECT * FROM player ORDER BY rank;
+----------+--------+------+
| playerID | points | rank |
+----------+--------+------+
| 3 | 250 | 1 |
| 4 | 200 | 2 |
| 5 | 175 | 3 |
| 6 | 175 | 3 |
| 1 | 150 | 5 |
| 2 | 100 | 6 |
+----------+--------+------+
6 rows in set (0.00 sec)
Note: Please consider that the queries I am suggesting could be simplified further.
Daniel, you have very nice solution. Except one point - the tie case. If tie happens between 3 players this update doesn't work properly. I changed your solution as following:
UPDATE player
JOIN (SELECT p.playerID,
IF(#lastPoint <> p.points,
#curRank := #curRank + #nextrank,
#curRank) AS rank,
IF(#lastPoint = p.points,
#nextrank := #nextrank + 1,
#nextrank := 1),
#lastPoint := p.points
FROM player p
JOIN (SELECT #curRank := 0, #lastPoint := 0, #nextrank := 1) r
ORDER BY p.points DESC
) ranks ON (ranks.playerID = player.playerID)
SET player.rank = ranks.rank;
EDIT: The update statement presented earlier did not work.
Although this is not exactly what you are asking for: You can generate the rank on the fly when selecting:
select p1.playerID, p1.points, (1 + (
select count(playerID)
from Player p2
where p2.points > p1.points
)) as rank
from Player p1
order by points desc
EDIT: Trying the UPDATE statement once more. How about a temporary table:
create temporary table PlayerRank
as select p1.playerID, (1 + (select count(playerID)
from Player p2
where p2.points > p1.points
)) as rank
from Player p1;
update Player p set rank = (select rank from PlayerRank r
where r.playerID = p.playerID);
drop table PlayerRank;
Hope this helps.
According to Normalization rules, rank should be evaluated at SELECT time.