How to display RANK in mysql table? - mysql

If we have rank depends on two column, lets say score and time,
and i want rank on highest score and lowest time.
CREATE TABLE yourTable (id int, userid int, questions int, `date` varchar(10),
rightquestions int, examid int, `time` int);
INSERT INTO yourTable (id, userid, questions, `date`, rightquestions, examid, `time`)
VALUES
(1, 10, 5, '02/09/2017', 5, 2, 11),
(2, 12, 5, '02/09/2017', 5, 2, 11),
(9, 16, 5, '02/09/2017', 4, 2, 18),
(8, 15, 5, '02/09/2017', 3, 2, 18);
as you can see above,In my table score = rightanswers and time=time
Now if i want rank by Highest rightanswers and lowest time then what should be the query?
I tried this one but getting unexpected results
SELECT id, rightquestions, leagueid,time,
CASE
WHEN #prevRank = rightquestions AND #prevTime != time THEN #curRank
WHEN #prevRank != rightquestions AND #prevTime != time THEN #curRank
WHEN #prevRank := rightquestions AND #prevTime := time AND #curRank = 0 THEN #curRank := #curRank + 1
END AS rank
FROM results p,
(SELECT #curRank :=0, #prevRank := NULL, #prevTime := NULL) r
ORDER BY rightquestions DESC,time ASC

You require a session variable that will increase and restart every time the score changes. You can also initialize it in a "fake" join, within the actual query as follows:
select timer, if(#score = score,#rank:=#rank+1,#rank:= 1) as rank ,#score:=score as score
from (
select 1 score, now() + interval rand()*10 hour as timer
union all
select 1 score, now() + interval rand()*10 hour as timer
union all
select 2 score, now() + interval rand()*10 hour as timer
union all
select 3 score, now() + interval rand()*10 hour as timer
union all
select 3 score, now() + interval rand()*10 hour as timer
)
t
join (select #rank:=0, #score := 0)r on (1=1)
order by score, timer asc

You can simulate a dense rank in MySQL by using a row number session variable along with a subquery which identifies unique rightquestions/time pairs. Try the following query which is based off the table snapshot you shared with us:
SET #row_number = 0;
SELECT t1.rightquestions, t1.`time`, t2.rank
FROM yourTable t1
INNER JOIN
(
SELECT (#row_number:=#row_number + 1) AS rank, t.rightquestions, t.`time`
FROM
(
SELECT rightquestions, `time`
FROM yourTable
GROUP BY rightquestions, `time`
) t
ORDER BY t.rightquestions DESC, t.`time`
) t2
ON t1.rightquestions = t2.rightquestions AND
t1.`time` = t2.`time`
Here is the output I got in Workbench while testing locally:
rightquestions | time | rank
5 | 11 | 1
5 | 11 | 1
4 | 18 | 2
3 | 18 | 3

Related

Select recent n number of entries of all users from table

I have a below table and wants to select only last 2 entries of all users.
Source table:
-------------------------------------
UserId | QuizId(AID)|quizendtime(AID)|
--------------------------------------
1 10 2016-5-12
2 10 2016-5-12
1 11 2016-6-12
2 12 2016-8-12
3 12 2016-8-12
2 13 2016-8-12
1 14 2016-9-12
3 14 2016-9-12
3 11 2016-6-12
Expected output is like, (should list only recent 2 quizid entries for all users)
-------------------------------------
UserId | QuizId(AID)|quizendtime(AID)|
--------------------------------------
1 14 2016-9-12
1 11 2016-6-12
2 13 2016-8-12
2 12 2016-8-12
3 14 2016-9-12
3 12 2016-8-12
Any idea's to produce this output.
Using MySQL user defined variables you can accomplish this:
SELECT
t.UserId,
t.`QuizId(AID)`,
t.`quizendtime(AID)`
FROM
(
SELECT
*,
IF(#sameUser = UserId, #a := #a + 1 , #a := 1) row_number,
#sameUser := UserId
FROM your_table
CROSS JOIN (SELECT #a := 1, #sameUser := 0) var
ORDER BY UserId , `quizendtime(AID)` DESC
) AS t
WHERE t.row_number <= 2
Working Demo
Note: If you want at most x number of entries for each user then change the condition in where clause like below:
WHERE t.row_number <= x
Explanation:
SELECT
*,
IF(#sameUser = UserId, #a := #a + 1 , #a := 1) row_number,
#sameUser := UserId
FROM your_table
CROSS JOIN (SELECT #a := 1, #sameUser := 0) var
ORDER BY UserId , `quizendtime(AID)` DESC;
This query sorts all the data in ascending order of userId and descending order of quizendtime(AID).
Now take a walk on this (multi) sorted data.
Every time you see a new userId assign a row_number (1). If you see the same user again then just increase the row_number.
Finally filtering only those records which are having row_number <= 2 ensures the at most two latest entries for each user.
EDIT: As Gordon pointed out that the evaluation of expressions using user defined variables in mysql is not guaranteed to follow the same order always so based on that the above query is slightly modified:
SELECT
t.UserId,
t.`QuizId(AID)`,
t.`quizendtime(AID)`
FROM
(
SELECT
*,
IF (
#sameUser = UserId,
#a := #a + 1,
IF(#sameUser := UserId, #a := 1, #a:= 1)
)AS row_number
FROM your_table
CROSS JOIN (SELECT #a := 1, #sameUser := 0) var
ORDER BY UserId , `quizendtime(AID)` DESC
) AS t
WHERE t.row_number <= 2;
WORKING DEMO V2
User-defined variables are the key to the solution. But, it is very important to have all the variable assignments in a single expression. MySQL does not guarantee the order of evaluation of expressions in a select -- and, in fact, sometimes processes them in different orders.
select t.*
from (select t.*,
(#rn := if(#u = UserId, #rn + 1,
if(#u := UserId, 1, 1)
)
) as rn
from t cross join
(select #u := -1, #rn := 0) params
order by UserId, quizendtime desc
) t
where rn <= 2;

Sorting Leaderboard in MySQL

I use the following SQL to order scores by rank in my leaderboard table:
SELECT score, 1+(SELECT COUNT(*) FROM leaderboard a WHERE a.score > b.score) AS rank
FROM leaderboard b
WHERE stage=1
GROUP BY id
where my table schema is like this:
CREATE TABLE `leaderboard` (
`auto_id` int(11) NOT NULL AUTO_INCREMENT,
`score` int(11) NOT NULL DEFAULT '0',
`id` int(11) NOT NULL,
`created_on` datetime NOT NULL,
PRIMARY KEY (`auto_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Some sample data rows are as follow:
auto_id score id created_on
====================================================
1, 72023456, 1, '2014-12-30 11:49:59'
2, 1420234, 1, '2014-12-30 12:00:21'
3, 420234, 1, '2014-12-30 12:00:38'
4, 16382, 1, '2014-12-30 16:31:12'
5, 16382, 1, '2014-12-30 16:34:18'
6, 16382, 1, '2014-12-30 16:37:43'
7, 17713, 1, '2014-12-30 16:38:35'
8, 17257, 1, '2014-12-30 18:53:45'
9, 10625, 1, '2014-12-30 18:58:10'
10, 17272, 1, '2014-12-30 18:58:59'
11, 17328, 1, '2014-12-30 18:59:44'
12, 17267, 37, '2015-01-02 17:11:59'
13, 16267, 37, '2015-01-02 17:12:30'
14, 16267, 37, '2015-01-02 17:13:02'
15, 35509, 37, '2015-01-02 17:17:46'
16, 18286, 37, '2015-01-02 18:20:09'
17, 16279, 37, '2015-01-02 18:20:43'
18, 16264, 37, '2015-01-02 18:21:15'
19, 16265, 37, '2015-01-02 18:40:04'
Since id is player's ID, I have to GROUP BY id. It gives the following result:
id score rank
=========================
1 72023456 1
37 17267 11
How can I obtain the following expected results?
id score rank
=========================
1 72023456 1
37 35509 2
The current problem is, the existing result is not the MAX score of the player.
Bonus: My ultimate goal is to get the entries 1 rank higher & 1 rank lower than specific id.
As MySql does not have Windowing Functions the query that you need has to mimic its behavior, so you have to use variables.
select id, score, #rank :=#rank+1 as rank
from (
SELECT b.id, max(b.score) as score
FROM leaderboard b
GROUP BY id
order by score desc
) tab
,(select #rank := 0) r
EDIT: I made a little mistake. I've corrected it now.
The output will be:
id score rank
=========================
1 72023456 1
37 35509 2
Basically what I'm doing is creating an iterator on the query and for every row it will increment the variable. As I added the order by it will rank your values based on that order by. But that rank has to happen outside the query because the order be alongside with the rank will mess things up if there is more than two IDs
I will edit the query with the solution for "1 rank higher & 1 rank lower than specific id."
EDIT: for the bonus (not pretty though)
select id, score, rank
from (
select tab.id, tab.score, #rank :=#rank+1 as rank
from (select #rank := 0) r,
(SELECT b.id, max(b.score) as score
FROM leaderboard b
GROUP BY id
order by score desc) tab
) spec
where spec.id=2
UNION
select id, score, rank
from (
select tab.id, tab.score, #rank :=#rank+1 as rank
from (select #rank := 0) r,
(SELECT b.id, max(b.score) as score
FROM leaderboard b
GROUP BY id
order by score desc) tab
) spec
where spec.rank=
(select rank-1
from (
select tab.id, tab.score, #rank :=#rank+1 as rank
from (select #rank := 0) r,
(SELECT b.id, max(b.score) as score
FROM leaderboard b
GROUP BY id
order by score desc) tab
) spec
where spec.id=2)
UNION
select id, score, rank
from (
select tab.id, tab.score, #rank :=#rank+1 as rank
from (select #rank := 0) r,
(SELECT b.id, max(b.score) as score
FROM leaderboard b
GROUP BY id
order by score desc) tab
) spec
where spec.rank=
(select rank+1
from (
select tab.id, tab.score, #rank :=#rank+1 as rank
from (select #rank := 0) r,
(SELECT b.id, max(b.score) as score
FROM leaderboard b
GROUP BY id
order by score desc) tab
) spec
where spec.id=2)
order by rank;
Note that you put the specific ID on the clauses spec.id=2 (I've put 2 because I had to change the values on my enviroment to test it)
Here the SQL Fiddle with my test with the two queries working: http://sqlfiddle.com/#!2/75047/2
The reason the score isn't the max is that, since score isn't in the GROUP BY clause, MySQL is just picking the first value as a representative. Technically, this isn't valid SQL. You probably want to use MAX(score) AS score.
As for the rank, since MySQL doesn't support window functions you'll have to hack something yourself. You can look at this SO post for more info. The standard ways seem to be to use mutable variables to count the rows, or to join the query to itself using an inequality in the ON clause. Neither seems very elegant.

Per group, find first N users with SUM(x) >= N

Problem: Find the first 2 users who have at least 10 items in a category, per category.
Table structure:
CREATE TABLE items(
id INT AUTO_INCREMENT PRIMARY KEY,
datetime datetime,
category INT,
user INT,
items_count INT
);
Sample data:
INSERT INTO items (datetime, category, user, items_count) VALUES
('2013-01-01 00:00:00', 1, 1, 10),
('2013-01-01 00:00:01', 1, 2, 1),
('2013-01-01 00:00:02', 1, 3, 10),
('2013-01-01 00:00:03', 1, 2, 9),
('2013-01-01 00:00:00', 2, 4, 10),
('2013-01-01 00:00:01', 2, 1, 10),
('2013-01-01 00:00:01', 2, 5, 10);
Desired result:
category user
1 1
1 3
2 4
2 5
Note: As shown in the result, I need to be able to show preference towards a user when multiple users meet the requirements simultaneously.
SQL Fiddle:
http://sqlfiddle.com/#!2/58e60
This is what I have tried:
SELECT
Derived.*,
IF (#category != Derived.category, #rank := 1, #rank := #rank + 1) AS rank,
#category := category
FROM(
SELECT
category,
user,
SUM(items_count) AS items_count,
MAX(datetime) AS datetime
FROM items
GROUP BY
category,
user
HAVING
SUM(items_count) >= 10
) AS Derived
JOIN(SELECT #rank := 0, #category := 0) AS r
HAVING
rank <= 2
ORDER BY
Derived.category,
Derived.datetime
But it is faulty. Not only does it not take user precedence into account, it would produce the wrong result with data such as this:
('2013-01-01 00:00:00', 1, 1, 10),
('2013-01-01 00:00:01', 1, 2, 1),
('2013-01-01 00:00:02', 1, 3, 10),
('2013-01-01 00:00:03', 1, 2, 9),
('2013-01-01 00:00:10', 1, 3, 1);
Additional information: I do not know if procedures could make a difference in this scenario, but unfortunately it is not an option either. The user running this query only has SELECT privilege.
In order to find the users that meet your needs, you need the cumulative sum of the counts. The following query finds the occasions when a user first reaches 10 units. If the counts are never negative, then there is only one:
select i.*
from (select i.*,
(select sum(items_count)
from items i2
where i2.user = i.user and
i2.category = i.category and
i2.datetime <= i.datetime
) as cumsum
from items i
) i
where cumsum - items_count < 10 and cumsum >= 10
order by datetime;
To get the first two, you need to use MySQL tricks for counting within a group. Here is an example that generally works:
select i.*
from (select i.*, if(#prevc = category, #rn := #rn + 1, #rn := 1) as rn, #prevc := category
from (select i.*,
(select sum(items_count)
from items i2
where i2.user = i.user and
i2.category = i.category and
i2.datetime <= i.datetime
) as cumsum
from items i
) i
cross join
(select #rn := 0) const
where cumsum - items_count < 10 and cumsum >= 10
) i
where rn <= 2
order by category, datetime;
I have a problem with this approach, because nothing in MySQL says that the expression #prevc := category will actually be calculated after the calculation for rn. However, it seems to be the case, and this seems to work in practice.
I tried Gordon's query, but unfortunately it does not seem to work with large tables; after waiting 15 minutes for the result I decided to kill it.
However the following query worked very well for me, it chewed it's way through a table of ~6M rows in about 8 seconds.
#Variable
SET #min_items = 10,
#max_users = 2,
#preferred_user = 5,
#Static
#category = 0,
#user = 0,
#items = 0,
#row_num = 1;
--
SELECT
category,
user,
datetime
FROM(
SELECT
category,
user,
datetime,
IF (#category = category, #row_num := #row_num + 1, #row_num := 1) AS row_num,
#category := category
FROM(
SELECT
category,
user,
datetime,
IF (#user != user, #items := 0, NULL),
IF (#items < #min_items, #items := #items + items_count, NULL) AS items_cumulative,
#user := user
FROM items
ORDER BY
category,
user,
datetime
) AS Derived
WHERE items_cumulative >= #min_items
ORDER BY
category,
datetime,
FIELD(user, #preferred_user, user)
) AS Derived
WHERE row_num <= #max_users;

Mysql alternative for LIMIT inside subquery in mysql 5.1.49

SELECT student_id FROM `students` AS s1
WHERE student_id IN
(SELECT s2.student_id FROM `students` AS s2
WHERE s1.year_of_birth = s2.year_of_birth
LIMIT 10)
Can't process this query on my server. It drops errors, that says that this version of mysql doesn't support limit inside subqueries etc(ERROR 1235).
Is there any solution for my version of mysql 5.1.49?
SELECT
id,
region
FROM (
SELECT
region,
id,
#rn := CASE WHEN #prev_region = region
THEN #rn + 1
ELSE 1
END AS rn,
#prev_region := region
FROM (SELECT #prev_region := NULL) vars, ads T1
ORDER BY region, id DESC
) T2
WHERE rn <= 4
ORDER BY region, id
Thanks to Mark Byers
I think you want any ten students with each birthdate. This is a greatest-n-per-group query and you can search Stack Overflow to see how this can be done in MySQL.
It would be easy if MySQL supported the ROW_NUMBER function, but since it does not you can emulate it using variables. For example to get 3 students for each birth date you could do it like this:
SELECT
student_id,
year_of_birth
FROM (
SELECT
year_of_birth,
student_id,
#rn := CASE WHEN #prev_year_of_birth = year_of_birth
THEN #rn + 1
ELSE 1
END AS rn,
#prev_year_of_birth := year_of_birth
FROM (SELECT #prev_year_of_birth := NULL) vars, students T1
ORDER BY year_of_birth, student_id DESC
) T2
WHERE rn <= 3
ORDER BY year_of_birth, student_id
Result:
1, 1990
2, 1990
5, 1990
4, 1991
7, 1991
8, 1991
6, 1992
Test data:
CREATE TABLE students (student_id INT NOT NULL, year_of_birth INT NOT NULL);
INSERT INTO students (student_id, year_of_birth) VALUES
(1, 1990),
(2, 1990),
(3, 1991),
(4, 1991),
(5, 1990),
(6, 1992),
(7, 1991),
(8, 1991);

With MySQL, how can I generate a column containing the record index in a table?

Is there any way I can get the actual row number from a query?
I want to be able to order a table called league_girl by a field called score; and return the username and the actual row position of that username.
I'm wanting to rank the users so i can tell where a particular user is, ie. Joe is position 100 out of 200, i.e.
User Score Row
Joe 100 1
Bob 50 2
Bill 10 3
I've seen a few solutions on here but I've tried most of them and none of them actually return the row number.
I have tried this:
SELECT position, username, score
FROM (SELECT #row := #row + 1 AS position, username, score
FROM league_girl GROUP BY username ORDER BY score DESC)
As derived
...but it doesn't seem to return the row position.
Any ideas?
You may want to try the following:
SELECT l.position,
l.username,
l.score,
#curRow := #curRow + 1 AS row_number
FROM league_girl l
JOIN (SELECT #curRow := 0) r;
The JOIN (SELECT #curRow := 0) part allows the variable initialization without requiring a separate SET command.
Test case:
CREATE TABLE league_girl (position int, username varchar(10), score int);
INSERT INTO league_girl VALUES (1, 'a', 10);
INSERT INTO league_girl VALUES (2, 'b', 25);
INSERT INTO league_girl VALUES (3, 'c', 75);
INSERT INTO league_girl VALUES (4, 'd', 25);
INSERT INTO league_girl VALUES (5, 'e', 55);
INSERT INTO league_girl VALUES (6, 'f', 80);
INSERT INTO league_girl VALUES (7, 'g', 15);
Test query:
SELECT l.position,
l.username,
l.score,
#curRow := #curRow + 1 AS row_number
FROM league_girl l
JOIN (SELECT #curRow := 0) r
WHERE l.score > 50;
Result:
+----------+----------+-------+------------+
| position | username | score | row_number |
+----------+----------+-------+------------+
| 3 | c | 75 | 1 |
| 5 | e | 55 | 2 |
| 6 | f | 80 | 3 |
+----------+----------+-------+------------+
3 rows in set (0.00 sec)
SELECT #i:=#i+1 AS iterator, t.*
FROM tablename t,(SELECT #i:=0) foo
Here comes the structure of template I used:
select
/*this is a row number counter*/
( select #rownum := #rownum + 1 from ( select #rownum := 0 ) d2 )
as rownumber,
d3.*
from
( select d1.* from table_name d1 ) d3
And here is my working code:
select
( select #rownum := #rownum + 1 from ( select #rownum := 0 ) d2 )
as rownumber,
d3.*
from
( select year( d1.date ), month( d1.date ), count( d1.id )
from maindatabase d1
where ( ( d1.date >= '2013-01-01' ) and ( d1.date <= '2014-12-31' ) )
group by YEAR( d1.date ), MONTH( d1.date ) ) d3
You can also use
SELECT #curRow := ifnull(#curRow,0) + 1 Row, ...
to initialise the counter variable.
Assuming MySQL supports it, you can easily do this with a standard SQL subquery:
select
(count(*) from league_girl l1 where l2.score > l1.score and l1.id <> l2.id) as position,
username,
score
from league_girl l2
order by score;
For large amounts of displayed results, this will be a bit slow and you will want to switch to a self join instead.
If you just want to know the position of one specific user after order by field score, you can simply select all row from your table where field score is higher than the current user score. And use row number returned + 1 to know which position of this current user.
Assuming that your table is league_girl and your primary field is id, you can use this:
SELECT count(id) + 1 as rank from league_girl where score > <your_user_score>
I found the original answer incredibly helpful but I also wanted to grab a certain set of rows based on the row numbers I was inserting. As such, I wrapped the entire original answer in a subquery so that I could reference the row number I was inserting.
SELECT * FROM
(
SELECT *, #curRow := #curRow + 1 AS "row_number"
FROM db.tableName, (SELECT #curRow := 0) r
) as temp
WHERE temp.row_number BETWEEN 1 and 10;
Having a subquery in a subquery is not very efficient, so it would be worth testing whether you get a better result by having your SQL server handle this query, or fetching the entire table and having the application/web server manipulate the rows after the fact.
Personally my SQL server isn't overly busy, so having it handle the nested subqueries was preferable.
I know the OP is asking for a mysql answer but since I found the other answers not working for me,
Most of them fail with order by
Or they are simply very inefficient and make your query very slow for a fat table
So to save time for others like me, just index the row after retrieving them from database
example in PHP:
$users = UserRepository::loadAllUsersAndSortByScore();
foreach($users as $index=>&$user){
$user['rank'] = $index+1;
}
example in PHP using offset and limit for paging:
$limit = 20; //page size
$offset = 3; //page number
$users = UserRepository::loadAllUsersAndSortByScore();
foreach($users as $index=>&$user){
$user['rank'] = $index+1+($limit*($offset-1));
}