I have a table of items that users are allowed to vote on. In this table, there is a votes column, that holds the number of votes that item has accumulated, and a rank column, that is a ranking of all items based on the number of votes they have (i.e. most votes gets rank 1, second most gets rank 2, etc.)
Currently, I'm recalculating the rank of every item after every vote. That is, when a user votes, I add one to that item's votes column, and then update every rank with the following query:
SET #rank = 0
UPDATE items SET rank = #rank := #rank + 1 ORDER BY votes DESC
This works for the most part, but doesn't take in to account voting ties. If I have votes [10, 4, 3, 0], I would expect ranks [1, 2, 3, 4]. However, if I have votes [10, 10, 3, 0], I would like ranks [1, 1, 3, 4]. This doesn't happen; I still get ranks [1, 2, 3, 4].
How can I incorporate ties like I've described above?
I wouldn't save the rank in database. You can calculate it while showing the result.
$rank = 1;
$lastVotes = -1;
$lastAdd = 0;
$query = mysql_query("SELECT * FROM table WHERE * ORDER BY votes DESC", $link);
while( $row = mysql_fetch_array( $query ) ) {
// local variable with votes
$votes = $row['votes'];
// check if we have a tie
if( $lastVotes == $votes ) {
// don't change rank if there is a tie but inc $lastAdd
$lastAdd += 1;
} else {
// there is no tie: save last votes, adjust $rank and reset $lastAdd
$lastVotes = $votes;
$rank += $lastAdd;
$lastAdd = 1;
}
// $rank is your rank
}
This inelegant solution would return what you desire:
update ITEMS I1
set rank =
(select count(*)
from ITEMS I2
where I2.VOTES >= I1.VOTES)
- (select count(*) - 1
from ITEMS I3
where I3.VOTES = I1.VOTES)
No mysql instance to try this out on, but how about something like this:
SELECT v.id, v.votes, r.rank
FROM votes v
,(SELECT votes, #rownum = #rownum + 1 AS rank
FROM votes
GROUP BY votes
ORDER BY votes DESC
) r
WHERE v.votes = r.votes
ORDER BY rank
Figure out the distinct set of "votes", order them, give each a number, then use that to associate the number (rank) back to each vote.
Really depends on the table size, and on the freq of updates, but how about a trigger? MySQL- Trigger updating ranking
Related
Suppose I have a table of Players, each player has a score, and now I want to divide all players into levels of equal size, based on their score, so if I have n players, level 1 will have the first n/10 players with the highest score, level 2 will have the next n/10, and so on.
I have come up with a query:
UPDATE Players SET Level=? WHERE PlayerID IN (
SELECT * FROM (
SELECT PlayerID FROM Players ORDER BY Score DESC, PlayerID ASC LIMIT ?,?
) AS T1
);
Where I run this 10 times, with the first parameter running from 1-10, the second is 0, n/10, 2*n/10, ... and the third is always n/10.
This works, but it takes quite a long time. Trying to get a better result, I have created a temporary table:
CREATE TEMPORARY TABLE TempTable (
IDX INT UNSIGNED NOT NULL AUTO_INCREMENT,
ID INT UNSIGNED NOT NULL,
PRIMARY KEY (IDX)
) ENGINE=MEMORY;
INSERT INTO TempTable (ID) SELECT PlayerID FROM Players ORDER BY Score DESC, PlayerID ASC;
Then I run ten times:
UPDATE Players SET Level=? WHERE PlayerID IN (
SELECT * FROM TempTable WHERE IDX BETWEEN ? AND ?
);
With the appropriate parameters, and finally:
DROP TABLE TempTable;
However, this runs even slower. So is there a more efficient way to do this in MySQL? I've found this answer, but it appears NTILE is not available in MySQL.
Note: Players have an index on PlayerID (Primary key) and on Score, although running without index on Score doesn't seem to make much of a difference. The reason I sort also by PlayerID is so I have well-defined (consistent) behavior in case of ties.
You could try using a ranking function. This is what I'd use:
SELECT PlayerID,
score,
#levelLimit,
#counter := #counter + 1 AS counter,
#level := IF(#counter % #levelLimit = 0, #level:= #level + 1, #level) as level
FROM Players,
(SELECT #counter := 0) a,
(SELECT #levelLimit := round(count(*)/4 -- number of groups you want to end with
, 0)
FROM Players) b,
(SELECT #level := 1) c
ORDER BY Score DESC,
PlayerID ASC
;
To update the table:
UPDATE Players join (
SELECT PlayerID,
score,
#levelLimit, #counter := #counter + 1 AS counter,
#level := IF(#counter % #levelLimit = 0, #level:= #level + 1, #level) AS level
FROM Players,
(SELECT #counter := 0) a,
(SELECT #levelLimit := round(count(*)/4 -- number of clusters
, 0)
FROM Players) b,
(SELECT #level := 1) c
ORDER BY Score DESC,
PlayerID ASC
) as a on a.PlayerID = Players.PlayerID
SET Players.level = a.level
http://sqlfiddle.com/#!9/7f55f9/3
The reason that your query is slow is because of this limit bit at the end:
SELECT PlayerID FROM Players ORDER BY Score DESC, PlayerID ASC LIMIT ?,?
Without an offset, limit you would be doing a table scan in ten steps. With a offset,limit You are doing it several times over! Essentially to get the offset the whole set of data has to be sorted and then only can mysql move to the data of interest. My suggestion is to avoid limit clause entire by breaking up the field into levels based on their scores.
For example if you have 10 levels, you could do a simple query to get
SELECT max(score), min(score) from ...
and then split the fields into 10 equals levels based on the difference of the highest and lovest score. If like stack overflow you have millions of users with a score of one, instead of min you can choose an arbitary number of the lowest bound.
then
UPDATE Players SET Level=? WHERE PlayerID IN (
SELECT * FROM (
SELECT PlayerID FROM score < level_upper_bound and score > leve_lower bound ) AS T1
);
You would still be doing a table scan in 10 steps, but now there is only one table scan and not 10
I am trying to generate row number for each row selected from my database but it seems that the row number follows the sequence of the table before it's arranged (order by).
Actual table
https://www.dropbox.com/s/otstzak20yxcgt6/test1.PNG?dl=0
After query
https://www.dropbox.com/s/i9jaoy04vq6u2zh/test2.PNG?dl=0
Code
SET #row_num = 0;
SELECT #row_num := #row_num + 1 as Position, Student.Stud_ID, Student.Stud_Name, Student.Stud_Class, SUM(Grade.Percentage) AS Points
FROM Student, Student_Subject, Grade
WHERE Student.Stud_ID = Student_Subject.Stud_ID
AND Student_Subject.Stud_Subj_ID = Grade.Stud_Subj_ID
AND Student.Stud_Form = '1'
AND Grade.Quarter = '1'
GROUP BY Student.Stud_ID
ORDER BY Points DESC
Pls help. Looking forward to receiving replies from yall. Thanks!
Try an inner select, so the row number will be generated after the ORDER BY like so:
SET #row_num = 0;
SELECT #row_num := #row_num + 1 as Position, s.*
FROM
(
SELECT
Student.Stud_ID, Student.Stud_Name, Student.Stud_Class, SUM(Grade.Percentage) AS Points
FROM Student, Student_Subject, Grade
WHERE Student.Stud_ID = Student_Subject.Stud_ID
AND Student_Subject.Stud_Subj_ID = Grade.Stud_Subj_ID
AND Student.Stud_Form = '1'
AND Grade.Quarter = '1'
GROUP BY Student.Stud_ID
ORDER BY Points DESC
) AS s;
I am using the following query to group the top N rows in my data set:
SELECT mgap_ska_id,mgap_ska_id_name, account_manager_id,
mgap_growth AS growth,mgap_recovery,
(mgap_growth+mgap_recovery) total
FROM
(SELECT mgap_ska_id,mgap_ska_id_name, account_manager_id, mgap_growth,
mgap_recovery,(mgap_growth+mgap_recovery) total,
#acid_rank := IF(#current_acid = account_manager_id, #acid_rank + 1, 1)
AS acid_rank,
#current_acid := account_manager_id
FROM mgap_orders
ORDER BY account_manager_id, mgap_growth DESC
) ranked
WHERE acid_rank <= 5
and the result is VERY close to what I need, but I am having an aggregate issue that I need help with. I have attcached a screenshot of my query results (I had to block out the customer names and ids for privacy; the mgap_ska_id and account_manager_id are INT columns and the mgap_ska_id_name is a VARCHAR.
In theory I need to SUM (I know its an aggregate; that is the issue) multiple mgap_growth values while keeping the ranking in tact.
If I GROUP BY, then I lose the top 5 ranking. Currently, the mgap_growth value is only one value per mgap_ska_id within the mgap_growth column; I need it to be the SUM of all mgap_growth values per mgap_ska_id and keep the top five ranking as shown.
Thanks!
You can add the following line in your select fields:
(SELECT SUM(t.mgap_growth) FROM mgap_orders t WHERE t.mgap_ska_id = ranked.mgap_ska_id ) AS total_mgap_growth
So your code will be:
SELECT mgap_ska_id,mgap_ska_id_name, account_manager_id,
mgap_growth AS growth,mgap_recovery,
(mgap_growth+mgap_recovery) total,
(SELECT SUM(t.mgap_growth) FROM mgap_orders t WHERE t.mgap_ska_id = ranked.mgap_ska_id ) AS total_mgap_growth
FROM
(SELECT mgap_ska_id,mgap_ska_id_name, account_manager_id, mgap_growth,
mgap_recovery,(mgap_growth+mgap_recovery) total,
#acid_rank := IF(#current_acid = account_manager_id, #acid_rank + 1, 1)
AS acid_rank,
#current_acid := account_manager_id
FROM mgap_orders
ORDER BY account_manager_id, mgap_growth DESC
) ranked
WHERE acid_rank <= 5
I'm ordering a recordset like this:
SELECT * FROM leaderboards ORDER BY time ASC, percent DESC
Say I have the id of the record which relates to you, how can I find out what position it is in the recordset, as ordered above?
I understand if it was just ordered by say 'time' I could
SELECT count from table where time < your_id
But having 2 ORDER BYs has confused me.
You can use a variable to assign a counter:
SELECT *, #ctr := #ctr + 1 AS RowNumber
FROM leaderboards, (SELECT #ctr := 0) c
ORDER BY time ASC, percent DESC
Does this do what you want?
SELECT count(*)
FROM leaderboards lb cross join
(select * from leaderboards where id = MYID) theone
WHERE lb.time < theone.time or
(lb.time = theone.time and lb.percent >= theone.percent);
This assumes that there are no duplicates for time, percent.
I am trying to do something like this in MYSQL, but without making query multiple times (50 times, in my case) through a PHP foreach.
foreach($this->map_ids as $key => $val) {
$this->db->query("SELECT scores.profile_number, scores.score FROM scores
LEFT JOIN players ON scores.profile_number = players.profile_number
WHERE scores.map_id = {'$val'}
AND scores.profile_number IN (SELECT profile_number FROM players WHERE banned = 0) LIMIT 10");
}
This is how it looks approximately when I retrieve all scores without LIMIT.
profile score map_id
76561198026851335 2478 47455
76561198043770492 2480 47455
... ... ...
76561198043899549 1340 47452
76561198048179892 1345 47452
... ... ...
I want only 10 entries (scores) from each unique map_id.
This is surprisingly difficult to do but I've ended up using user variables to do the job, check out the following demo. Obviously my data structure is much simplified but it should be enough to get you going:
SQL Fiddle example
Here is the SQL for anyone who may be interested in skipping the demo (hideous, I know)
SELECT *
FROM (
SELECT profile_number, score, map_id
FROM (
SELECT
profile_number, score, map_id,
IF( #prev <> map_id, #rownum := 1, #rownum := #rownum+1 ) AS rank,
#prev := map_id
FROM scores
JOIN (SELECT #rownum := NULL, #prev := 0) AS r
ORDER BY map_id
) AS tmp
WHERE tmp.rank <= 10
) s
JOIN players p
ON s.profile_number = p.profile_number
Basically, what is happening is this:
ORDER BY map_id
Orders your table by map_id so that all the same ones are together.
Next we assign a rownumber to each row by using the following logic:
IF( #prev <> map_id, #rownum := 1, #rownum := #rownum+1 )
If the previous row's map_id is not equal to the current row's ID, set the row number = 1, otherwise increase the rownumber by 1.
Finally, only return the rows who have a rownumber less than or equal to 10
WHERE tmp.rank <= 10
Hope that makes it a little clearer for you.
You can use the limit directive.
SELECT * FROM `your_table` LIMIT 0, 10
This will display the first 10 results from the database.
SELECT * FROM `your_table` LIMIT 5, 5
This will show records 6, 7, 8, 9, and 10