views:

248

answers:

3

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?

+2  A: 

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. Cheers, Tom

Tom Bartel
@Tom: No, it won't work. You'd get a `You can't specify target table 'p1' for update in FROM clause`, because of the `p1` reference in the subquery.
Daniel Vassallo
Thanks for the clarification Daniel. Since Col. Shrapnel pointed out that, strictly speaking, rank should be computed at select time, let me point out that my subselect should work for that purpose.
Tom Bartel
@Tom: Yes, that subquery would work at `SELECT` time, but it still won't handle ties. The OP even tagged the question as 'tie'! :)
Daniel Vassallo
I have a modified version of this query as update player g1 set g1.rank = 1 + ((SELECT count(*) from (select * from player) g2 where g2.points > g1.points))I wonder if there is any optimization that can be done on it.
sammichy
@Daniel: I don't see why my statement won't handle ties. Every player with the same points will get the same rank. If two players tie for rank 5, the next player(s) will be assigned rank 7. If that's not how "handling ties" is supposed to work, we have a different understanding of the term.
Tom Bartel
@sammichy: And does your statement work? Is it slow or why do you want to optimize it?
Tom Bartel
@Tom: It does in fact. I misinterpreted the query. +1 because it's a good option. However I think you should edit the answer, because it is results in an error as it is now. The select subqery works fine: `SELECT p1.points, p1.playerID, 1 + (select count(playerID) FROM Player p2 WHERE p2.points > p1.points) rank FROM player p1 ORDER BY rank;`
Daniel Vassallo
@Daniel: Right you are. I edited my answer. Thanks for the +1.
Tom Bartel
@Tom, my statement does work, but it is slow as it takes exponential time and I wanted to see if there was a way to make it run faster.
sammichy
@Tom - is there way to convert your select clause into an update statement for the whole table. I'm having trouble because of 'can't specify target table for update in FROM clause' ...
sammichy
@sammichy: See another edit.
Tom Bartel
@Tom, thank you for this suggestion, I will try this out.
sammichy
A: 

According to Normalization rules, rank should be evaluated at SELECT time.

Col. Shrapnel
Yes, but this is primarily a lookup table where rank is calculated periodically and I do not want to run this every time a user logs in.
sammichy
+2  A: 

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:


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 Vassallo
@Daniel, thank you, this is exactly what I needed. Thank you for the links.
sammichy
Daniel, please see my comment to my own answer.
Tom Bartel