2

I am attempting to store the rank of users based on a score, all it one table, and skipping ranks when there is a tie. For example:

ID   Score Rank
2    23    1
4    17    2
1    17    2
5    10    4
3    2     5

Each time a user's score is updated, The rank for the entire table must also be updated, so after a score update, the following query is run:

SET @rank=0;
UPDATE users SET rank= @rank:= (@rank+1) ORDER BY score DESC;

But this doesn't support ties, or skipping rank numbers after ties, for that matter.

I want to achieve this re-ranking in as few queries as possible and with no joins (as they seem rather time consuming).

I was able to get the desired result by adding two columns - last_score and tie_build_up - with the following code:

SET @rank=0, @last_score = null, @tie_build_up = 0;
UPDATE users SET
    rank= @rank:= if(@last_score = score, @rank, @rank+@tie_build_up+1),
    tie_build_up= @tie_build_up:= if(@last_score = score, @tie_build_up+1, 0),
    last_score= @last_score:= score, ORDER BY score DESC;

I don't want those extra columns, but I couldn't get the single query to work without them.

Any ideas?

Thanks.

Kyle
  • 21
  • 1
  • 3

3 Answers3

2

Here's an alternate solution: don't store ranks at all! :-)

You can calculate them on the fly.

Example:

SELECT id, (@next_rank := IF(@score <> score, 1, 0)) nr, 
           (@score := score) score, (@r := IF(@next_rank = 1, @r + 1, @r)) rank 
FROM rank, (SELECT @r := 0) dummy1
ORDER BY score DESC;

Result:

  +------+----+-------+------+
  | id   | nr | score | rank |
  +------+----+-------+------+
  |    2 |  1 |    23 |    1 |
  |    4 |  1 |    17 |    2 |
  |    1 |  0 |    17 |    2 |
  |    5 |  1 |    10 |    3 |
  |    3 |  1 |     2 |    4 |
  +------+----+-------+------+

nr here is aт auxilliary column that indicates whether we should assign next rank or not.

You can wrap this query in another select and perform paging, for example.

SELECT id, score, rank 
FROM (SELECT id, (@next_rank := IF(@score <> score, 1, 0)) nr, 
           (@score := score) score, (@r := IF(@next_rank = 1, @r + 1, @r)) rank
      FROM rank, (SELECT @r := 0) dummy1
      ORDER BY score DESC) t
      WHERE rank > 1 and rank < 3;

Result:

  +------+-------+------+
  | id   | score | rank |
  +------+-------+------+
  |    4 |    17 |    2 |
  |    1 |    17 |    2 |
  +------+-------+------+

CAUTION: since now rank is a calculated column, you can't index it and efficiently page far into dataset (that is, "select records with ranks from 3000 to 3010"). But it's still good for "select top N ranks" (provided that you put a corresponding LIMIT on a query)

Sergio Tulentsev
  • 226,338
  • 43
  • 373
  • 367
  • +1, it's what I'd do. Doesn't seem right to store a calculated column, unless absolutely necessary. Performance on calculating the column is probably better than storing, as locks aren't needed for the write. – Andrew Dunkman Dec 31 '11 at 04:25
0

I'm sure you have a good reason for this design choice but I think you should leave the rank out of the database all together. Updating the entire table for every change in one user's score can cause very serious performance issues with almost any size table. I suggest you reconsider that choice. I would advice to simply sort the table by score and assign ranks in the application code.

Assaf Karmon
  • 915
  • 1
  • 10
  • 23
-1

i calculated rank and position the following way:

to update AND get the values i needs, i first added them, added an additional 1 and subtracted the original value. that way i do not need any help-columns inside the table;

SET @rank=0;
SET @position=0;
SET @last_points=null;

UPDATE tip_invitation 
set 
    rank = @rank:=if(@last_points = points, @rank, @rank + 1),
    position = ((@last_points := points)-points) + (@position := @position+1)
where
    tippgemeinschaft_id = 1 ORDER BY points DESC;
Bruno Jennrich
  • 360
  • 5
  • 5