Update the rank in a MySQL Table - mysql

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.

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;

MySQL: Sequentially number a column based on change in a different column

If I have a table with the following columns and values, ordered by parent_id:
id parent_id line_no
-- --------- -------
1 2
2 2
3 2
4 3
5 4
6 4
And I want to populate line_no with a sequential number that starts over at 1 every time the value of parent_id changes:
id parent_id line_no
-- --------- -------
1 2 1
2 2 2
3 2 3
4 3 1
5 4 1
6 4 2
What would the query or sproc look like?
NOTE: I should point out that I only need to do this once. There's a new function in my PHP code that automatically creates the line_no every time a new record is added. I just need to update the records that already exist.
Most versions of MySQL do not support row_number(). So, you can do this using variables. But you have to be very careful. MySQL does not guarantee the order of evaluation of variables in the select, so a variable should not be assigned an referenced in different expressions.
So:
select t.*,
(#rn := if(#p = parent_id, #rn + 1,
if(#p := parent_id, 1, 1)
)
) as line_no
from (select t.* from t order by id) t cross join
(select #p := 0, #rn := 0) params;
The subquery to sort the table may not be necessary. Somewhere around version 5.7, this became necessary when using variables.
EDIT:
Updating with variables is fun. In this case, I would just use subqueries with the above:
update t join
(select t.*,
(#rn := if(#p = parent_id, #rn + 1,
if(#p := parent_id, 1, 1)
)
) as new_line_no
from (select t.* from t order by id) t cross join
(select #p := 0, #rn := 0) params
) tt
on t.id = tt.id
set t.line_no = tt.new_line_no;
Or, a little more old school...
DROP TABLE IF EXISTS my_table;
CREATE TABLE my_table
(id SERIAL PRIMARY KEY
,parent_id INT NOT NULL
);
INSERT INTO my_table VALUES
(1, 2),
(2 , 2),
(3 , 2),
(4 , 3),
(5 , 4),
(6 , 4);
SELECT x.*
, CASE WHEN #prev = parent_id THEN #i := #i+1 ELSE #i := 1 END i
, #prev := parent_id prev
FROM my_table x
, (SELECT #prev:=null,#i:=0) vars
ORDER
BY parent_id,id;
+----+-----------+------+------+
| id | parent_id | i | prev |
+----+-----------+------+------+
| 1 | 2 | 1 | 2 |
| 2 | 2 | 2 | 2 |
| 3 | 2 | 3 | 2 |
| 4 | 3 | 1 | 3 |
| 5 | 4 | 1 | 4 |
| 6 | 4 | 2 | 4 |
+----+-----------+------+------+
You can use subquery if the row_number() doesn't help :
select t.*,
(select count(*)
from table t1
where t1.parent_id = t.parent_id and t1.id <= t.id
) as line_no
from table t;

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

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

how to get rank according to group and year of birth

I have a table with gr_no, year_dob, family_id etc. etc.
I am trying to rank birth year according to family_id and am unable to generate the siblings_rank result.
+----------+--------------+-----------+
| gr_no | year_dob | family_id | siblings_rank
+----------+--------------+-----------+
| 1001 | 1992 | 95 | 1
| 10234 | 1995 | 95 | 2
| 10236 | 2004 | 96 | 1
| 15568 | 2006 | 96 | 2
| 1225 | 2004 | 92 | 1
+----------+--------------+-----------+
This query is working :
SET #prev := null;
SET #cnt := 1;
SELECT gr_no, gs_id, gf_id, year_dob, IF(#prev <> gf_id, #cnt := 1, #cnt := #cnt + 1) AS siblings_position, #prev := gf_id as previous_gf_id
FROM student_registered
ORDER BY gf_id, year_dob asc
This query is also working:
SELECT gr_no, gs_id, gf_id, year_dob, IF(#prev <> gf_id, #cnt := 1, #cnt := #cnt + 1) AS siblings_position, #prev := gf_id as previous_gf_id
FROM student_registered
JOIN (SELECT #prev := null) p
JOIN (SELECT #cnt := 1) c
ORDER BY gf_id, year_dob asc
............... I am unable to create view with these query?
or
If a procedure can update the student_registered column 'siblings_position' based on the queries?
you can't use sql variables in the view
here is another way to get the same result using correlated subquery
SELECT gr_no, family_id,year_dob,
( select count(*) from Table1 T1
where T1.family_id = T.family_id
and T1.year_dob <= T.year_dob) as siblings_position
FROM Table1 T
ORDER BY family_id, year_dob asc