39

I have a very simple MySQL table where I save highscores. It looks like that:

Id     Name     Score

So far so good. The question is: How do I get what's a users rank? For example, I have a users Name or Id and want to get his rank, where all rows are ordinal ordered descending for the Score.

An Example

Id  Name    Score
1   Ida     100
2   Boo     58
3   Lala    88
4   Bash    102
5   Assem   99

In this very case, Assem's rank would be 3, because he got the 3rd highest score.

The query should return one row, which contains (only) the required Rank.

0

10 Answers 10

44
SELECT id, name, score, FIND_IN_SET( score, (
SELECT GROUP_CONCAT( score
ORDER BY score DESC ) 
FROM scores )
) AS rank
FROM scores

gives this list:

id name  score rank
1  Ida   100   2
2  Boo    58   5
3  Lala   88   4
4  Bash  102   1
5  Assem  99   3

Getting a single person score:

SELECT id, name, score, FIND_IN_SET( score, (    
SELECT GROUP_CONCAT( score
ORDER BY score DESC ) 
FROM scores )
) AS rank
FROM scores
WHERE name =  'Assem'

Gives this result:

id name score rank
5 Assem 99 3

You'll have one scan to get the score list, and another scan or seek to do something useful with it. An index on the score column would help performance on large tables.

4
  • 3
    The correlated (SELECT GROUP_CONCAT(score) FROM TheWholeTable) is not the best way. And it may have a problem with the size of the row created. Commented Feb 23, 2012 at 8:04
  • 2
    This will fails in case of ties.
    – Arvind07
    Commented May 3, 2013 at 11:14
  • 3
    The single person score query is extremely slow for larger tables.. A much better query to determine rank (with gaps for ties) for a single person's score is: SELECT 1 + COUNT(*) AS rank FROM scores WHERE score > (SELECT score FROM scores WHERE name='Assem'). Which 'just' counts the number of entries with a higher score than the current entry. (If you add DISTINCT you will get the rank without gaps..)
    – Paul
    Commented Jan 10, 2019 at 13:43
  • 1
    IMPORTANT: GROUP_CONTAT has a default limit of 1024 characters, on large sets of data it will result in wrong ranks, for example, it might stop at rank 100 and then report 0 as the rank
    – 0plus1
    Commented Jun 14, 2019 at 5:01
35

When multiple entries have the same score, the next rank should not be consecutive. The next rank should be incremented by number of scores that share the same rank.

To display scores like that requires two rank variables

  • rank variable to display
  • rank variable to calculate

Here is a more stable version of ranking with ties:

SET @rnk=0; SET @rank=0; SET @curscore=0;
SELECT score,ID,rank FROM
(
    SELECT AA.*,BB.ID,
    (@rnk:=@rnk+1) rnk,
    (@rank:=IF(@curscore=score,@rank,@rnk)) rank,
    (@curscore:=score) newscore
    FROM
    (
        SELECT * FROM
        (SELECT COUNT(1) scorecount,score
        FROM scores GROUP BY score
    ) AAA
    ORDER BY score DESC
) AA LEFT JOIN scores BB USING (score)) A;

Let's try this out with sample data. First Here is the sample data:

use test
DROP TABLE IF EXISTS scores;
CREATE TABLE scores
(
    id int not null auto_increment,
    score int not null,
    primary key (id),
    key score (score)
);
INSERT INTO scores (score) VALUES
(50),(40),(75),(80),(55),
(40),(30),(80),(70),(45),
(40),(30),(65),(70),(45),
(55),(45),(83),(85),(60);

Let's load the sample data

mysql> DROP TABLE IF EXISTS scores;
Query OK, 0 rows affected (0.15 sec)

mysql> CREATE TABLE scores
    -> (
    ->     id int not null auto_increment,
    ->     score int not null,
    ->     primary key (id),
    ->     key score (score)
    -> );
Query OK, 0 rows affected (0.16 sec)

mysql> INSERT INTO scores (score) VALUES
    -> (50),(40),(75),(80),(55),
    -> (40),(30),(80),(70),(45),
    -> (40),(30),(65),(70),(45),
    -> (55),(45),(83),(85),(60);
Query OK, 20 rows affected (0.04 sec)
Records: 20  Duplicates: 0  Warnings: 0

Next, let initialize the user variables:

mysql> SET @rnk=0; SET @rank=0; SET @curscore=0;
Query OK, 0 rows affected (0.01 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Now, here is the output of the query:

mysql> SELECT score,ID,rank FROM
    -> (
    ->     SELECT AA.*,BB.ID,
    ->     (@rnk:=@rnk+1) rnk,
    ->     (@rank:=IF(@curscore=score,@rank,@rnk)) rank,
    ->     (@curscore:=score) newscore
    ->     FROM
    ->     (
    ->         SELECT * FROM
    ->         (SELECT COUNT(1) scorecount,score
    ->         FROM scores GROUP BY score
    ->     ) AAA
    ->     ORDER BY score DESC
    -> ) AA LEFT JOIN scores BB USING (score)) A;
+-------+------+------+
| score | ID   | rank |
+-------+------+------+
|    85 |   19 |    1 |
|    83 |   18 |    2 |
|    80 |    4 |    3 |
|    80 |    8 |    3 |
|    75 |    3 |    5 |
|    70 |    9 |    6 |
|    70 |   14 |    6 |
|    65 |   13 |    8 |
|    60 |   20 |    9 |
|    55 |    5 |   10 |
|    55 |   16 |   10 |
|    50 |    1 |   12 |
|    45 |   10 |   13 |
|    45 |   15 |   13 |
|    45 |   17 |   13 |
|    40 |    2 |   16 |
|    40 |    6 |   16 |
|    40 |   11 |   16 |
|    30 |    7 |   19 |
|    30 |   12 |   19 |
+-------+------+------+
20 rows in set (0.18 sec)

Please note how multiple IDs that share the same score have the same rank. Also note that rank is not consecutive.

Give it a Try !!!

5
  • Since this is using session-scoped variables, is this safe if, say, multiple end-users are requesting the scoreboard at the same time? Is it possible for the result set to have different results because another user is also executing this query? Imagine an API in front of this query with many clients hitting it at once. Commented May 21, 2019 at 20:21
  • @XaeroDegreaz You are right, It is possible. Imagine calculating ranks for a game. One user queries for rank and another user queries 5 seconds after a person beats the high score or enters the top X scores. Notwithstanding, the same can happen if the ranking were done at the application level rather than the server level. Commented May 21, 2019 at 20:24
  • Thanks for the reply. My concern isn't really whether or not the data shifts organically over time, my concern is that multiple users performing the query would be modifying / overwriting the data stored in the session-scoped variables while other users are also performing the query. Does that make sense? Commented May 21, 2019 at 20:32
  • @XaeroDegreaz that's the beauty of session scope variables. They are in your session only, and nobody else's. You will not see session variables from other users and nobody will see your session variables. Commented May 21, 2019 at 20:45
  • Okay, that's what I was sort of leaning towards believing -- that session variables are scoped to the connection, and a single-connection cannot be occupied by more than one person at a time. Once the connection is free, or thrown back into the pool, another user can hop on the connection and the session variables are re-initialized (when performing this query). Thanks again for the information. Commented May 21, 2019 at 20:47
17
SELECT 
    id, 
    Name,
    1+(SELECT count(*) from table_name a WHERE a.Score > b.Score) as RNK,
    Score
FROM table_name b;
0
9

One option would be to use USER variables:

SET @i=0;
SELECT id, name, score, @i:=@i+1 AS rank 
 FROM ranking 
 ORDER BY score DESC;
0
4

The accepted answer has a potential problem. If there are two or more identical scores, there will be gaps in the ranking. In this modified example:

 id name  score rank
 1  Ida   100   2
 2  Boo    58   5
 3  Lala   99   3
 4  Bash  102   1
 5  Assem  99   3

The score of 58 has rank 5, and there is no rank 4.

If you want to make sure there are no gaps in the rankings, use DISTINCT in the GROUP_CONCAT to build a list of distinct scores:

SELECT id, name, score, FIND_IN_SET( score, (
SELECT GROUP_CONCAT( DISTINCT score
ORDER BY score DESC ) FROM scores)
) AS rank
FROM scores

Result:

id name  score rank
1  Ida   100   2
2  Boo    58   4
3  Lala   99   3   
4  Bash  102   1
5  Assem  99   3

This also works for getting a single user's rank:

SELECT id, name, score, FIND_IN_SET( score, (    
SELECT GROUP_CONCAT(DISTINCT score
ORDER BY score DESC ) 
FROM scores )
) AS rank
FROM scores
WHERE name =  'Boo'

Result:

id name score rank
 2  Boo   58    4
3
  • The single user's rank query can be optimized enormously by using COUNT and a subquery instead. See my comment at the Accepted Answer
    – Paul
    Commented Jan 10, 2019 at 13:47
  • Good note and enhancement. works very well Commented Jul 15, 2019 at 14:09
  • i am getting this error #1064 - You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '( score, (SELECT GROUP_CONCAT( DISTINCT scoreORDER BY score DESC )
    – Tausif
    Commented Nov 19, 2022 at 12:53
3

Here's the best answer:

SELECT 1 + (SELECT count( * ) FROM highscores a WHERE a.score > b.score ) AS rank FROM
highscores b WHERE Name = 'Assem' ORDER BY rank LIMIT 1 ;

This query will return:

3

1
  • I'm having a small problem with it thought. For example: if the first two users have different scores and all the rest have 0, the rankings for the the zero-score people is #4 instead of #3. But the first is correctly getting #1 and the second #2. Any ideas?
    – fersarr
    Commented Apr 30, 2014 at 5:15
3

This solution gives the DENSE_RANK in case of ties:

SELECT *,
IF (@score=s.Score, @rank:=@rank, @rank:=@rank+1) rank,
@score:=s.Score score
FROM scores s,
(SELECT @score:=0, @rank:=0) r
ORDER BY points DESC
0
0

Wouldn't the following work (assuming your table is called Scores)?

SELECT COUNT(id) AS rank FROM Scores 
WHERE score <= (SELECT score FROM Scores WHERE Name = "Assem")
0

Improved version of @a1ex07, to consider duplicated scores

SELECT 
    id, 
    Name,
    (SELECT count(*) 
        from (select Score from table_name group by Score) a 
        WHERE a.Score > b.Score) as RNK,
    Score
FROM table_name b;

You can replace the nested group select by count(DISTINCT Score) instead of count(*) to simplify the query, but the performance will be dramatically decreased

-4

I have this, which gives the same results as the one with variables. It works with ties and it may be faster:

SELECT COUNT(*)+1 as rank
FROM 
(SELECT score FROM scores ORDER BY score) AS sc
WHERE score <
(SELECT score FROM scores WHERE Name="Assem")

I didn't test it, but I'm using one that works perfect, which I adapted to this with the variables you were using here.

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.