My problem is,
I have a table in mysql
which colums are
id Student score
1 A 55
2 B 86
3 C 65
4 D 23
5 E 84
6 F 45
7 G 80
I want to find rank of any student in whole class based on score, with the student who scored just greater them him and an another student who scored just less them him.
for example if I am searching for student E
then output should be
id User score rank_in_classs
2 B 86 1
5 E 84 2
7 G 80 3
An another example can be that if I am looking for student A
id User score rank_in_classs
3 c 65 4
1 A 55 5
6 F 45 6
How can I find it using mysql query.
Thanks
Query
SELECT id, Student, score,
FIND_IN_SET( score, (
SELECT GROUP_CONCAT( score
ORDER BY score DESC )
FROM tbl )
) AS rank_in_class
FROM tbl
ORDER BY rank_in_class
LIMIT 3;
DEMO
Using the FIND_IN_SET() based solution proposed by Ullas you can locate a particular student +/- 1 rank this way:
set #this_rank := (
SELECT
FIND_IN_SET( score, (
SELECT GROUP_CONCAT( score
ORDER BY score DESC )
FROM tbl
)
) AS rank_in_class
FROM tbl
where student = 'A'
);
select
*
from (
SELECT
id
, Student
, score
, FIND_IN_SET( score, (
SELECT GROUP_CONCAT( score
ORDER BY score DESC )
FROM tbl
)
) AS rank_in_class
FROM tbl
) ric
where rank_in_class between #this_rank-1 AND #this_rank+1
ORDER BY rank_in_class
LIMIT 3;
Note: IF you stored the rank value in the table, then this would not be so cumbersome and could perform way better.
The best way to do that is probably with a mix of two queries and a little of PHP.
The first query retrieves the rank of the "focused" student you want;
The second query retrieves the range of all the students around the focused "student" with a dynamic LIMIT statement (this is where the use of PHP is unavoidable);
So something like this would probably do the job :
$user = 'C'; // Student user we want to "focus"
$range = 1; // Range around the "focus" : 1 before and 1 after (could be changed to anything else)
// First query : retrieving the rank of the "focused" student
$stmt = $mysqli->prepare('SELECT COUNT(*) AS Rank FROM Student AS Focused INNER JOIN Student as Others ON Others.Score > Focused.Score OR (Others.Score = Focused.Score AND Others.Id > Focused.Id) WHERE Focused.user = ?');
$stmt->bind_param('s',$user);
$stmt->execute();
$res = $stmt->get_result()->fetch_assoc();
$startRank = $res['Rank'];
// Computing the dynamic LIMIT
if (($startRank - $range) < 1) {
$offset = 0;
$rowCount = $startRank + $range + 1;
} else {
$offset = $startRank - $range;
$rowCount = ($range * 2)+1;
}
// Second query : retrieving the rank of all the students around the "focused" student
$stmt = $mysqli->prepare('SELECT id, user, score, #curRank := #curRank + 1 AS rank FROM Student, (SELECT #curRank := ?) Rank ORDER BY Score DESC, id DESC LIMIT ?, ?');
$stmt->bind_param('iii',$offset,$offset,$rowCount);
$stmt->execute();
This is probably the most optimized way to query the database to get what you want. As a bonus, you can change the range to whatever you like.
Related
I have a MySQL query as below; I would like to select the top record for each range of 600 records in a table with 1.8M records. So far I have to loop 3,000 times to accomplish this which is not an efficient solution.
Database Schema;
Table: bet_perm_13predict
id bet_id perm_id avg_odd avg_odd2 avg_odd3
1 23 1 43.29 28.82 28.82
2 23 2 42.86 28.59 28.59
3 23 3 43.13 28.73 28.73
Table: bet_permute_13games
perm_id perm_code
1 0000000000000
2 0000000000001
3 0000000000002
4 0000000000010
Sample MySQL Query in PHP
$totRange = 0; //Used as starting point in rang
$range = 600; //Used as range
$stop = 0;//Used as endPoint of range
while($totRange < 1800000){
$stop = $totRange+$range;
$sql = "SELECT (tb1.avg_odd2 + tb1.avg_odd3) AS totAvg_odd ,
tb1.perm_id , tb1.avg_odd, tb1.avg_odd2, tb1.avg_odd3, tb2.perm_code
FROM bet_perm_13predict tb1
INNER JOIN bet_permute_13games tb2 ON tb2.perm_id = tb1.perm_id
WHERE tb1.bet_id = '$bet_id' && tb1.perm_id
BETWEEN $startRange AND $stop ORDER BY totAvg_odd ASC LIMIT 1"
$q1 = $this->db->query($sql);
$totRange = $stop;
}
In other words I want to select a sample of the data that will represent the entire table with the sample not being random but predefined using the top record in range of 600. So far I have no idea how to proceed. There is no clear online material on this subject.
You can use integer division to create groups.
DEMO
SELECT ID, ID DIV 600 as grp
FROM Table1
Then find the max value on each group. Some options here
Get records with max value for each group of grouped SQL results
For those who might encounter the same issue, this is how I solved it. I used #Juan Carlos suggestion and added a way to pick top record of group using Subquery.
SELECT * FROM
(SELECT * , perm_id DIV $limit as grp , (avg_odd2 + avg_odd3) AS totAvg_odd
FROM bet_perm_13predict WHERE bet_id = '$bet_id' ORDER BY grp ASC ) tb1
INNER JOIN bet_permute_13games tb2 ON tb2.perm_id = tb1.perm_id
INNER JOIN bet_entry tb3 ON tb3.bet_id = tb1.bet_id
WHERE tb1.avg_odd2 < (SELECT AVG(avg_odd2) FROM bet_perm_13predict WHERE bet_id = '$bet_id' )
&& tb1.avg_odd3 < (SELECT AVG(avg_odd3) FROM bet_perm_13predict WHERE bet_id = '$bet_id' )
GROUP BY grp ORDER BY totAvg_odd ASC
LIMIT 100
Introduction
I have a highscore table for my game which uses ranks. The scores table represents current highscores and player info and the recent table represents all recently posted scores by a user which may or may not have been a new top score.
The rank drop is calculated by calculating the player's current rank minus their rank they had at the time of reaching their latest top score.
The rank increase is calculated by calculating the player's rank they had at the time of reaching their latest top score minus the rank they had at the time of reaching their previous top score.
Finally, as written in code: $change = ($drop > 0 ? -$drop : $increase);
Question
I am using the following two queries combined with a bit of PHP code to calculate rank change. It works perfectly fine, but is sometimes a bit slow.
Would there be a way to optimize or combine the two queries + PHP code?
I created an SQL Fiddle of the first query: http://sqlfiddle.com/#!9/30848/1
The tables are filled with content already, so their structures should not be altered.
This is the current working code:
$q = "
select
(
select
coalesce(
(
select count(distinct b.username)
from recent b
where
b.istopscore = 1 AND
(
(
b.score > a.score AND
b.time <= a.time
) OR
(
b.score = a.score AND
b.username != a.username AND
b.time < a.time
)
)
), 0) + 1 Rank
from scores a
where a.nickname = ?) as Rank,
t.time,
t.username,
t.score
from
scores t
WHERE t.nickname = ?
";
$r_time = 0;
if( $stmt = $mysqli->prepare( $q ) )
{
$stmt->bind_param( 'ss', $nick, $nick );
$stmt->execute();
$stmt->store_result();
$stmt->bind_result( $r_rank, $r_time, $r_username, $r_score );
$stmt->fetch();
if( intval($r_rank) > 99999 )
$r_rank = 99999;
$stmt->close();
}
// Previous Rank
$r_prevrank = -1;
if( $r_rank > -1 )
{
$q = "
select
coalesce(
(
select count(distinct b.username)
from recent b
where
b.istopscore = 1 AND
(
(
b.score > a.score AND
b.time <= a.time
) OR
(
b.score = a.score AND
b.username != a.username AND
b.time < a.time
)
)
), 0) + 1 Rank
from recent a
where a.username = ? and a.time < ? and a.score < ?
order by score desc limit 1";
if( $stmt = $mysqli->prepare( $q ) )
{
$time_minus_one = ( $r_time - 1 );
$stmt->bind_param( 'sii', $r_username, $time_minus_one, $r_score );
$stmt->execute();
$stmt->store_result();
$stmt->bind_result( $r_prevrank );
$stmt->fetch();
if( intval($r_prevrank) > 99999 )
$r_prevrank = 99999;
$stmt->close();
}
$drop = ($current_rank - $r_rank);
$drop = ($drop > 0 ? $drop : 0 );
$increase = $r_prevrank - $r_rank;
$increase = ($increase > 0 ? $increase : 0 );
//$change = $increase - $drop;
$change = ($drop > 0 ? -$drop : $increase);
}
return $change;
If you are separating out the current top score into a new table, while all the raw data is available in the recent scores.. you have effectively produced a summary table.
Why not continue to summarize and summarize all the data you need?
It's then just a case of what do you know and when you can know it:
Current rank - Depends on other rows
Rank on new top score - Can be calculated as current rank and stored at time of insert/update
Previous rank on top score - Can be transferred from old 'rank on new top score' when a new top score is recorded.
I'd change your scores table to include two new columns:
scores - id, score, username, nickname, time, rank_on_update, old_rank_on_update
And adjust these columns as you update/insert each row.
Looks like you already have queries that can be used to backfit this data for your first iteration.
Now your queries become a lot simpler
To get rank from score:
SELECT COUNT(*) + 1 rank
FROM scores
WHERE score > :score
From username:
SELECT COUNT(*) + 1 rank
FROM scores s1
JOIN scores s2
ON s2.score > s1.score
WHERE s1.username = :username
And rank change becomes:
$drop = max($current_rank - $rank_on_update, 0);
$increase = max($old_rank_on_update - $rank_on_update, 0);
$change = $drop ? -$drop : $increase;
UPDATE
Comment 1 + 3 - Oops, may have messed that up.. have changed above.
Comment 2 - Incorrect, if you keep the scores (all the latest high-scores) up to date on the fly (every time a new high-score is recorded) and assuming there is one row per user, at the time of calculation current rank should simply be a count of scores higher than the user's score (+1). Should hopefully be able to avoid that crazy query once the data is up to date!
If you insist on separating by time, this will work for a new row if you haven't updated the row yet:
SELECT COUNT(*) + 1 rank
FROM scores
WHERE score >= :score
The other query would become:
SELECT COUNT(*) + 1 rank
FROM scores s1
JOIN scores s2
ON s2.score > s1.score
OR (s2.score = s1.score AND s2.time < s1.time)
WHERE s1.username = :username
But I'd at least try union for performance:
SELECT SUM(count) + 1 rank
FROM (
SELECT COUNT(*) count
FROM scores s1
JOIN scores s2
ON s2.score > s1.score
WHERE s1.username = :username
UNION ALL
SELECT COUNT(*) count
FROM scores s1
JOIN scores s2
ON s2.score = s1.score
AND s2.time < s1.time
WHERE s1.username = :username
) counts
An index on (score, time) would help here.
Personally I'd save yourself a headache and keep same scores at the same rank (pretty standard I believe).. If you want people to be able to claim first bragging rights just make sure you order by time ASC on any score charts and include the time in the display.
I spent a lot of time trying to figure out what the rank logic is and put in a comment about it. In the meantime, here is a join query that you can run on your data - I think your solution will something something to this effect:
SELECT s.username, count(*) rank
FROM scores s LEFT JOIN recent r ON s.username != r.username
WHERE r.istopscore
AND r.score >= s.score
AND r.time <= s.time
AND (r.score-s.score + s.time-r.time)
GROUP BY s.username
ORDER BY rank ASC;
+----------+------+
| username | rank |
+----------+------+
| Beta | 1 |
| Alpha | 2 |
| Echo | 3 |
+----------+------+
(note that last AND is just to ensure you don't account for r.score==s.score && r.time==s.time - which i guess would be a "tie" game?)
I am not a MySQL guy, but I think that using self-join for ranking is a bad practice in any RDBMS. You should consider using of ranking functions. But there are no ranking functionality in MySQL. But there are workarounds.
There are some assumptions that have to be made here in order to move forward with this. I assume that the scores table has only one entry per 'username' which is somehow equivalent to a nickname.
Try this,
If I had a working db, this would be quick to figure out and test, but basically you are taking the 'sub query' you are running in the selected field and you are building a temp table with ALL of the records and filtering them out.
select a.nickname
, count(distinct b.username) as rank
, t.time
, t.username
, t.score
from
(
select
a.nickname
, b.username
from (select * from scores where nickname=? ) a
left join (select * from recent where istopscore = 1) as b
on (
b.score > a.score and b.time <= a.time -- include the b record if the b score is higher
or
b.score = a.score and b.time < a.time and a.username != b.username -- include b if the score is the same, b got the score before a got the score
)
) tmp
join scores t on (t.nickname = tmp.nickname)
where t.nickname = ?
I did not attempt to address your later logic, you can use the same theory, but it is not worth trying unless you can confirm that this method returns the correct rows.
If you would like to get deeper, you should create some data sets and fully setup the SQL Fiddle.
To get the ranking of the user 3 I am using this query (It works fine):
$sql = "SELECT
score,
FIND_IN_SET(score,
(
SELECT GROUP_CONCAT(score
ORDER BY score DESC)
FROM results)
) AS rank
FROM results
WHERE user_id = 3
";
Table structure:
user_id - score
Now I want to give the user 3 some points. My formule is:
Number of score (Or users) - rank + 1.
So if I have 20 users (Or score) and the ranking of the user 3 is 10, so the points will be:
20 - 10 + 1 = 11 points.
PS: users (Or score) = some users have more than one score.
How can I get the number of score (Or users) using this mysql query?
SQL FIDDLE
http://sqlfiddle.com/#!9/ff4505/4
Check this ... I hope it help you ...
SELECT user_id,#all_user := (SELECT COUNT(*) FROM `results`) as all_count,((#all_user-score)+1) as rank FROM `results` WHERE user_id = 3
Improved Answer
$sql = "SELECT #all_user := (SELECT COUNT(*) FROM `results`)
as all_count,((#all_user-score)+1) as rank FROM `results` WHERE user_id = 3";
Okay so I have a table that has the following
KEY username password score
The above columns are not in any specific order.
I want to send my Database a username and have it send me back what rank that user name is based on its score. So for example if I had 10 people in there and the 3rd person in has the highest score. When I pass the 3rd persons username in I want it to send back 1.
Is this possible?
I have been trying things like this
$result = mysql_query("SELECT * FROM tablename where username='$username' ORDER BY score DESC");
but it doesnt seem to give me the row number
This will handle ranks that have the same score.
SELECT d.*, c.ranks
FROM
(
SELECT Score, #rank:=#rank+1 Ranks
FROM
(
SELECT DISTINCT Score
FROM tableName a
ORDER BY score DESC
) t, (SELECT #rank:= 0) r
) c
INNER JOIN tableName d
ON c.score = d.score
// WHERE d.username = 'Helen'
SQLFiddle Demo (Ranking with duplicates)
SQLFiddle Demo (with filtering)
for example
KEY username password score Ranks
1 Anna 123 5 3
2 Bobby 345 6 2
3 Helen 678 6 2
4 Jon 567 2 4
5 Arthur ddd 8 1
for better performance, add an INDEX on column Score,
ALTER TABLE tableName ADD INDEX (Score)
SELECT
(SELECT COUNT(*)+1 FROM tablename WHERE score > t.score) as rank,
*
FROM
tablename t
where
username='$username'
The ORDER BY in your query is useless since you're only returning one row.
This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Mysql rank function
I have the following countryTable
country clicks
------- ------
0 222
66 34
175 1000
45 650
How do I get the ranking of say country 45 which is 2 in this case?
Ordered by country ASC:
SELECT 1+COUNT(*) AS ranking
FROM countryTable
WHERE country < 45 ;
Ordered by clicks DESC:
SELECT 1+COUNT(*) AS ranking
FROM countryTable AS t
JOIN countryTable AS c
ON c.clicks > t.clicks
WHERE t.country = 45 ;
You can get 2 rank as below it like below:
Select * from tabeName order by clicks limit 1,1
For 3 rank:
Select * from tabeName order by clicks limit 2,1
SELECT *
FROM
(
SELECT #ranking:= #ranking + 1 rank,
a.country,
a.clicks
FROM tableName a, (SELECT #ranking := 0) b
ORDER BY a.clicks DESC
) s
WHERE country = 45
SQLFiddle Demo
This will show the correct rank (2) for country 45. You don't specify how to rank ties, so you may want to change the comparison to suit you. Non existing countries rank as 0.
SELECT COUNT(*) rank
FROM countryTable a
JOIN countryTable b
ON a.clicks <= b.clicks
WHERE a.country = 45
SQLfiddle here.
X is the rank you need to look for:
SELECT * FROM T ORDER BY clicks DESC LIMIT X-1,1
Here's another (stunningly fast) way (albeit limited to 256 rows):
SELECT country
, clicks
, FIND_IN_SET(clicks,(SELECT GROUP_CONCAT(DISTINCT clicks ORDER BY clicks DESC) FROM country_clicks)) rank
FROM country_clicks
or, if you prefer...
SELECT FIND_IN_SET(clicks,(SELECT GROUP_CONCAT(DISTINCT clicks ORDER BY clicks DESC) FROM country_clicks)) rank
FROM country_clicks
WHERE country = 45;