How do we display rank order in a MySQL query, for example from a table like this?
CREATE TABLE votes( name CHAR(10), votes INT );
INSERT INTO votes VALUES
('Smith',10),('Jones',15),('White',20),
('Black',40),('Green',50),('Brown',20);
Windowing functions, long available in other RDBMSs, came to MySQL in version 8 and to MariaDB in version 10.2. They make simple work of querying rank order:
SELECT name, votes, rank() OVER(win) AS 'Vote Rank' FROM votes WINDOW win AS (ORDER BY votes); +-------+-------+-----------+ | name | votes | Vote Rank | +-------+-------+-----------+ | Smith | 10 | 1 | | Jones | 15 | 2 | | White | 20 | 3 | | Brown | 20 | 3 | | Black | 40 | 5 | | Green | 50 | 6 | +-------+-------+-----------+Before MySQL 8 and MariaDB 10.2, the query is a two-step: 1. Join the table to itself on the value to be ranked, handling ties 2. Group and order the result of the self-join on rank: SELECT v1.name, v1.votes, COUNT(v2.votes) AS Rank FROM votes v1 JOIN votes v2 ON v1.votes < v2.votes OR (v1.votes=v2.votes and v1.name = v2.name) GROUP BY v1.name, v1.votes ORDER BY v1.votes DESC, v1.name DESC; +-------+-------+------+ | name | votes | Rank | +-------+-------+------+ | Green | 50 | 1 | | Black | 40 | 2 | | White | 20 | 3 | | Brown | 20 | 3 | | Jones | 15 | 5 | | Smith | 10 | 6 | +-------+-------+------+Suppose you want to know the vote count for White and the names and tallies for the next highest and next lowest counts:
SELECT
s1.name, s1.votes, COUNT(s2.name) rank,
IF(s1.name = 'White','<-',' ') AS 'Near Ranks'
FROM votes s1
JOIN votes s2
ON s1.votes < s2.votes
OR (s1.votes=s2.votes AND s1.name = s2.name)
GROUP BY s1.name, s1.votes
ORDER BY rank
) a
LEFT JOIN (
SELECT
s1.name, s1.votes, COUNT(s2.name) rank,
IF(s1.name = 'White', '<-',' ') AS 'Near Ranks'
FROM votes s1
JOIN votes s2
ON s1.votes < s2.votes
OR (s1.votes=s2.votes and s1.name = s2.name)
GROUP BY s1.name, s1.votes
ORDER BY rank
) b ON a.rank BETWEEN b.rank-1 AND b.rank+1
WHERE a.name = 'White';
+-------+-------+------+------------+
| name | votes | rank | Near Ranks |
+-------+-------+------+------------+
| Black | 40 | 2 | |
| White | 20 | 3 | <- |
| Brown | 20 | 3 | |
+-------+-------+------+------------+
But these methods don't scale; they're O(N2) or worse, especially if non-trivial calculation is needed to compute rank. If you do not need to identify ties in the ranking column, writing a temporary ordering table and then querying it is often much faster:
DROP TEMPORARY TABLE IF EXISTS tmp; SET @i=0; CREATE TEMPORARY TABLE tmp SELECT idcol,valuecol,@i:=@i+1 AS rank ORDER BY valuecol DESC;Baron Schwartz has a useful page on optimising rankings for leaderboards. It requires maintenance of a ranking computation table, as described on his page under "How to maintain the rank column". That table has this structure: CREATE TABLE score_ranked ( gamer INT NOT NULL, game INT NOT NULL, score INT NOT NULL, rank_in_game INT NOT NULL DEFAULT 0, PRIMARY KEY (gamer,game), KEY (game,score), KEY (score), KEY (game,rank_in_game) ) ENGINE=InnoDB;If you use Baron's schema, you will find the following query, developed on a MySQL forum by Rick James, reasonably efficient for retrieving top score sums for all games:
SELECT
@seq := 0, -- raw 1,2,3,...
@rank := 0, -- allow for ties
@prev := 999999999; -- catch ties
SELECT Rank, gamer, point
FROM (
SELECT
@seq := IF(@prev = point, @seq, @seq + 1) AS seq,
@rank := IF(@prev = point, @rank, @seq) AS Rank,
@prev := point as prev,
gamer,
point
FROM (
SELECT gamer, SUM(score) as point
FROM score_ranked s
GROUP BY gamer
ORDER BY point DESC
) x
) y
WHERE Rank <= 10
ORDER BY Rank, gamer;
Last updated 17 Dec 2024 |