Ordinal Ranking in MySQL Update - mysql

I've got a table like this:
id name incidence placeRef
1 John 10 1
2 Ann 9 1
3 Paul 9 1
4 Carl 8 1
5 John 4 1
6 Ann 4 1
7 Paul 7 1
8 Carl 1 1
I want to rank these using the ordinal ranking method. Which would add ranks to my table as such:
id name incidence placeRef rank
1 John 10 1 1
2 Ann 9 1 2
3 Paul 9 1 2
4 Carl 8 1 4
5 John 4 1 2
6 Ann 4 1 2
7 Paul 7 1 1
8 Carl 1 1 4
How can this be achieved?
N.B. I am going to answer my own question, but would like to know if anyone has any better solutions as it is a bit hacky; though I found numerous posts recommending hacks for this situation.

You can do this with variables or a correlated subquery. The subquery method looks like this:
select t.*,
(select 1 + count(t2.incidence)
from table t2
where t2.incidence > t.incidence
) as rank
from table t;
The variables method is a bit trickier because you have to remember the number of matching rows with a given value:
select t.*,
(#rn := if(#i = t.incidence, if(#cnt := #cnt + 1, #rn, #rn),
#cnt + if(#i := t.incidence, if(#cnt := 1, #rn, #rn), #rn)
)
) as rank
from table t cross join
(select #i := 0, #rn := 0, #cnt := 1) vars
order by incidence desc;
EDIT:
If you want to update the table, just use update with a join:
update table t join
(<either subquery above>) s
on t.id = s.id
set t.rank = s.rank;

The following works:
UPDATE names
JOIN ( SELECT * FROM names ORDER BY placeRef, incidence DESC ) AS p ON p.id = names.id,
( SELECT #curRank := 0, #nextRank := 0, #prevInc := 9999999999, #prevPlace := 0 ) AS v
SET
names.rank = IF( #prevPlace != p.placeRef, #curRank := 0, 0 ),
names.rank = IF( #prevPlace != p.placeRef, #nextRank := 0, 0 ),
names.rank = IF( #prevInc = p.incidence, #nextRank := #nextRank + 1, #curRank := #nextRank := #nextRank + 1 ),
names.rank = IF( #prevInc = p.incidence, #curRank := #curRank, #curRank := #nextRank ),
names.incidence = #prevInc := names.incidence,
names.placeRef = #prevPlace := names.placeRef;
Explanation:
UPDATE names
1 - Sets the table to be updated
JOIN ( SELECT * FROM names ORDER BY placeRef, incidence DESC ) AS p ON p.id = names.id,
2 - This makes a virtual table with the results ordered, so that rankings can be applied
( SELECT #curRank := 0, #nextRank := 0, #prevInc := 9999999999, #prevPlace := 0 ) AS v
3 - This set some variables that will be used to tell when to incrament and reset the rank
names.rank = IF( #prevPlace != p.placeRef, #curRank := 0, 0 ),
4 - This is a hack that resets the current rank to 0 when MySQL iterates into a new place
names.rank = IF( #prevPlace != p.placeRef, #nextRank := 0, 0 ),
5 - This is a hack that resets the next rank to 0 when MySQL iterates into a new place
names.rank = IF( #prevInc = p.incidence, #nextRank := #nextRank + 1, #curRank := #nextRank := #nextRank + 1 ),
6 - This is a hack that updates the next and current ranks when the current incidence is the same as the previous incidence
names.rank = IF( #prevInc = p.incidence, #curRank := #curRank, #curRank := #nextRank ),
7 - This sets the rank to the same as the last rank when its incidence is the same as the previous or increments the rank if it isn't
names.incidence = #prevInc := names.incidence,
8 - This is a hack that sets a variable to contain the previous incidence, so we can tell what to do in the next itteration
names.placeRef = #prevPlace := names.placeRef;
9 - This is a hack that sets a variable to contain the previous place, so we can tell what to do in the next itteration

Related

Mysql Select Correlation 2 Levels Deep Subqueries

I'm stuck and this is driving me crazy... I'd like to use the code below to get x number of wins in the row. The problem is, mysql only allows 1 correlation level (not sure if im explaining this right) but #id is supposed to return the players id from the first select query. Any idea on how I can make this work?
select (#rank := if(#points = points, #rank +1, if(#points := points, #rank + 1, #rank + 1 ))) as rank, er.* from (select
#id := cc6_MensLeague_players.id as `id`,
#rounds := (
ifnull((select sum(p1.w)
from (
select
#r1 := (case when p1_r1 like '%' then 1 else 0 end) as r1,
#r2 := (case when p1_r2 like '%' then 1 else 0 end) as r2,
#r3 := (case when p1_r3 like '%' then 1 else 0 end) as r3,
#r4 := (case when p1_r4 like '%' then 1 else 0 end) as r4,
(#r1+#r2+#r3+#r4) as w
from cc6_MensLeague_scoresheets where p1 = #id
)p1),0)
) as `rounds`,
sum((#rounds*2)+(#rounds*1)) as `points`
from cc6_MensLeague_players group by `id`) er cross join (select #rank := 0, #points := -1) params order by id desc limit 9;
Here is some sample data to play with:
(Using dynamic #id alias will not work)
https://www.db-fiddle.com/f/ao2zgyiy8U5doGER6XZT23/3
(Using static id 8 works perfectly)
https://www.db-fiddle.com/f/2f2KvZt5MHVUuaP3WLPmAi/2
Here is what the resulting output should be:
rank id rounds points
1 10 4 12
2 9 4 12
3 8 8 24
4 7 8 24
5 6 6 18
6 5 4 12
7 4 0 0
8 3 0 0
9 2 0 0
Thanks for the help

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;

Getting first N rows between 2 timestamps for each ID Mysql

I have table like below, start_t is my timestamp in unixtime for example (1438326239412) but for simplicity I wrote small numbers here:
user_id | start_t | duration
1 12 1
1 15 2
1 4 5
2 9 6
2 10 5
3 9 6
I want to get first N rows for each user_id between two time stamps. This is my code but it returns more than the limit I want:
SELECT us.* FROM (SELECT us.*, (#rn := if(#i = us.user_id, #rn + 1, if(#i := us.user_id, 1, 1) ) ) AS seqnum FROM user_stats us,tourn_user tu CROSS JOIN (SELECT #rn := 0, #i := -1) params WHERE (us.start_t+us.duration)<= 20 AND us.start_t >= 4 ORDER BY us.user_id, start_t ASC ) us WHERE seqnum <= 1
The result should look like this for that specific example:
user_id | start_t | duration
1 4 5
2 9 6
3 9 6
Here is the solution in case someone wants:
select us.* from (select us.*, (#rn := if(#i = us.user_id, #rn + 1, if(#i := us.user_id, 1, 1) ) ) as seqnum from user_stats us cross join (select #rn := 0, #i := -1) params where (us.start_t+us.duration)<= 15 AND us.start_t >= 0 order by us.user_id, start_t ASC ) us where seqnum <= 1

How to get correct position on ties in mysql rankings

This is my code and works for ties but it does not skip position on ties
SELECT `item`, (`totalrate` / `nrrates`),
#rank_count := #rank_count + (totalrate/nrrates < #prev_value) rank,
#prev_value := totalrate/nrrates avg
FROM table, (SELECT #prev_value := NULL, #rank_count := 1) init
ORDER BY avg DESC
Here is the out I get
item (`totalrate` / `nrrates`) rank avg
Virginia 10.0000 1 10
Ana 9.7500 2 9.75
Angeie 9.72 3 9.72
Carel 9.666666666 4 9.66
sammy 9.666666666 4 9.66
Oda 9.500000000 5 9.5
I want
item (`totalrate` / `nrrates`) rank avg
Virginia 10.0000 1 10
Ana 9.7500 2 9.75
Angeie 9.72 3 9.72
Carel 9.666666666 4 9.66
sammy 9.666666666 4 9.66
Oda 9.500000000 6 9.5
To skip the 5 position
I would like to merge with this that does skip position on ties
(I took the below code from this post
MySQL Rank in the Case of Ties)
SELECT t1.name, (SELECT COUNT(*) FROM table_1 t2 WHERE t2.score > t1.score) +1
AS rnk
FROM table_1 t1
how would I modify my code to get it to skip position with the above code it looks simple but i haven't figured it out.
Thanks
On ties, you may want to skip and use current row num to next unmatched avg value row as next rank.
Following should help you
SELECT `item`, #curr_avg := ( `totalrate` / `nrrates` )
, case when #prev_avg = #curr_avg then #rank := #rank
else #rank := ( #cur_row + 1 )
end as rank
, #cur_row := ( #cur_row + 1 ) as cur_row
, #prev_value := #curr_avg avg
FROM table
, ( SELECT #prev_avg := 0, #curr_avg := 0
, #rank := 0, #cur_row := 0 ) init
ORDER BY avg DESC
Similar examples:
To display top 4 rows using rank
Mysql Query for Rank (RowNumber) and Groupings
Update a field with an incrementing value that resets based on
field
Here's another alternative. First, the averages are calculated. If they are already available in a table, it would be even easier (as can be seen in the fiddle demo). Anyways, the rank is based on the logic of counting how many items have a lesser average than the current item.
SELECT
A1.`item`,
A1.avg,
COUNT(A2.`item`) avg_rank
FROM
(
SELECT `item`, (`totalrate` / `nrrates`),
#prev_value := totalrate/nrrates avg
FROM table, (SELECT #prev_value := NULL, #rank_count := 1) init
) A1 --alias for the inline view
INNER JOIN
(
SELECT `item`, (`totalrate` / `nrrates`),
#prev_value := totalrate/nrrates avg
FROM table, (SELECT #prev_value := NULL, #rank_count := 1) init
) A2 --alias for the inline view
ON A2.avg < A1.avg
GROUP BY A1.id, A1.avg
ORDER BY A1.avg;
SQL Fiddle demo

Mysql limit per parent id

How can I limit result per each id in WHERE clause?
My query is:
SELECT name
FROM location_areas
WHERE parent IN ("1,2,3")
ORDER BY popularity,name
Parent is not unique.
I need to get 10 results for each parent id in WHERE clause.
for example table structure is:
id name parent
1 name 0
2 name 1
3 name 1
4 name 80
5 name 80
6 name 80
7 name 80
8 name 1
Try this:
SELECT
T.name,
T.popularity,
T.parent,
T.rank
FROM
(
SELECT
L.name,
L.popularity,
L.parent,
#rank := IF(#parent = parent, #rank + 1, 1) rank,
#parent := parent
FROM location_areas L,
(SELECT #rank := 1, #parent := NULL) R
) T
WHERE T.rank <= 10
EDIT
SELECT T.name, T.popularity, T.parent, T.level, T.rank
FROM (
SELECT L.name, L.popularity,
L.parent, L.level,
#rank := IF(#parent = parent, #rank + 1, 1) rank,
#parent := parent
FROM location_areas L,
(SELECT #rank := 1, #parent := NULL) R
WHERE L.parent IN (".$ids.")
) T WHERE T.rank <= 10;
You can simply do it lie this
SET #level = 0;
SET #group = '';
SELECT
name
FROM (
SELECT
name ,
parent
#level := IF(#group = parent, #level+1, 1) AS level,
#group := parent as EGroup
FROM test
WHERE parent IN ("1,2,3")
ORDER BY parent
) rs
WHERE level < 11