mysql 中對使用者分數排名
周海漢/文 2012.9.11
mysql 不提供排名函式,所以需自己去實現。 排序先用mapreduce進行,但對於相同成績的,其排名應該一樣。而mapreduce由於沒有先後關係的資料,所以沒法做這工作。可以在應用程式中將資料迴圈讀出,再判斷是否分數相等,如果相等,則其名次相等。也可以在mysql 5.0以後的版本中,採用儲存過程來實現。
需求中還需要單獨取一個使用者資料時,也得到其正確的排名資料。所以客戶端臨時排名不合適。伺服器端臨時排名也不合適。必須將排好名的資料寫在資料庫裡。
準備 先建立表: mysql> desc mysort; +———+————-+——+—–+———+—————-+ | Field | Type | Null | Key | Default | Extra | +———+————-+——+—–+———+—————-+ | id | int(11) | NO | PRI | NULL | auto_increment | | name | varchar(20) | YES | | NULL | | | score | int(11) | YES | | NULL | | | rank | int(11) | YES | | NULL | | | caldate | varchar(20) | YES | | NULL | | | userid | int(11) | YES | | NULL | | +———+————-+——+—–+———+—————-+ 6 rows in set (0.00 sec)
CREATE TABLE `mysort` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(20) DEFAULT NULL, `score` int(11) DEFAULT NULL, `rank` int(11) DEFAULT NULL, `caldate` varchar(20) DEFAULT NULL, `userid` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM AUTO_INCREMENT=7 DEFAULT CHARSET=latin1
插入資料 mysql> select * from mysort; +—-+——+——-+——+—————+——–+ | id | name | score | rank | caldate | userid | +—-+——+——-+——+—————+——–+ | 1 | a1 | 10 | 1 | 2012-09-10_24 | 151 | | 2 | a2 | 8 | 2 | 2012-09-10_24 | 131 | | 3 | a3 | 8 | 3 | 2012-09-10_24 | 12 | | 4 | a4 | 1 | 6 | 2012-09-10_24 | 11 | | 5 | a5 | 2 | 4 | 2012-09-10_24 | 211 | | 6 | a6 | 2 | 5 | 2012-09-10_24 | 212 | +—-+——+——-+——+—————+——–+ 可見其排名是正確的,唯一有問題的地方是相同分數排名不一樣,使用者會覺得不公平。
目標要排名後成為: +—-+——+——-+——+—————+——–+ | id | name | score | rank | caldate | userid | +—-+——+——-+——+—————+——–+ | 1 | a1 | 10 | 1 | 2012-09-10_24 | 151 | | 2 | a2 | 8 | 2 | 2012-09-10_24 | 131 | | 3 | a3 | 8 | 2 | 2012-09-10_24 | 12 | | 5 | a5 | 2 | 4 | 2012-09-10_24 | 211 | | 6 | a6 | 2 | 4 | 2012-09-10_24 | 212 | | 4 | a4 | 1 | 6 | 2012-09-10_24 | 11 | +—-+——+——-+——+—————+——–+
rank 的排名是一致的。這樣保證相同分數score的人排名是一個。
單純排名,可以有重複 對於不用更新資料庫的方式,或者更新到臨時表中的方式,最簡單:
mysql> set @rank:=0;
mysql> select @rank:[email protected]+1 as rank,id,name,score from mysort order by score desc;
+------+----+------+-------+
| rank | id | name | score |
+------+----+------+-------+
| 1 | 1 | a1 | 10 |
| 2 | 2 | a2 | 8 |
| 3 | 3 | a3 | 8 |
| 4 | 5 | a5 | 2 |
| 5 | 6 | a6 | 2 |
| 6 | 4 | a4 | 1 |
+------+----+------+-------+
rank是排過名後的,只是對分數相同的排名有先後,而且沒有更新到表中。這對資料量少,只是顯示,是一種最簡便的排名方法。
另外一種排名,有select子句,所以效率較差
mysql> select * from (select (select count(id)+1 from mysort where score>a.score) as arank,a.name, a.score from mysort a) b order by b.arank;
+-------+------+-------+
| arank | name | score |
+-------+------+-------+
| 1 | a1 | 10 |
| 2 | a2 | 8 |
| 2 | a3 | 8 |
| 4 | a5 | 2 |
| 4 | a6 | 2 |
| 6 | a4 | 1 |
+-------+------+-------+
6 rows in set (0.00 sec)
但這個排名是滿足要求的,只是沒有寫到表中,如果可以將整個資料導到另一個表,也是一種可行的辦法。
第三種,用儲存過程。 在mysql 5.0以上的版本,支援儲存過程。 本程式碼是mysql儲存過程例項,將表和時間做成了引數。
CREATE PROCEDURE p(in tname varchar(100),in cdate varchar(20))
BEGIN
DECLARE done BOOLEAN DEFAULT FALSE;
DECLARE auid INT;
DECLARE ascore,arank INT;
DECLARE cur1 CURSOR FOR SELECT rank,userid,score FROM myview;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
drop view if exists myview;
SET @last_score =0;
SET @last_rank =0;
SET @last_uid =0;
SET @myuid=0;
SET @myrank=0;
SET @cmdc=concat("create view myview as SELECT rank,userid,score FROM ", tname," WHERE caldate='",cdate, "' ORDER BY rank" );
PREPARE pcursor FROM @cmdc;
EXECUTE pcursor;
DEALLOCATE PREPARE pcursor;
SET @cmdu = concat("update ",tname," set rank=? where userid=? ");
PREPARE pupdate FROM @cmdu;
OPEN cur1;
read_loop: LOOP
FETCH cur1 INTO arank,auid,ascore;
IF done THEN
LEAVE read_loop;
END IF;
IF ascore = @last_score THEN
select auid,@last_rank;
IF @myrank=0 THEN
set @myrank:[email protected]_rank;
END IF;
set @myuid:=auid;
EXECUTE pupdate USING @myrank,@myuid;
ELSE
set @myrank:=0;
END IF;
SELECT arank,auid,ascore INTO @last_rank,@last_uid,@last_score;
END LOOP;
DEALLOCATE PREPARE pupdate;
CLOSE cur1;
END;
這裡要注意一個陷阱,就是更新資料後,遊標並不能得到新資料。
本儲存過程用到如下幾個知識點: 1.儲存過程引數 2.表名做為儲存過程的引數傳入 3.遊標使用,因為返回多行資料,需要針對每一行進行處理,所以用遊標。 4.檢視。遊標不支援表名做為引數,所以建立了一個檢視view 5.語句組建,採用prepare和execute及字串處理concat函式。 6.區域性變數和session變數使用,賦值和判斷。set 賦值可以用=或:=;select 賦值必須用:=;@變數是session變數,不需宣告。區域性變數需宣告。 7.if 語句 8.迴圈 9.如何判斷資料讀取結束,也可以用這句:DECLARE CONTINUE HANDLER FOR SQLSTATE ‘02000’ SET bOVER=TRUE;SQLSTATE ‘02000’就是結束。
呼叫 在mysql命令列下,先設定結束符: mysql> delimiter // 再輸入edit 在vi中輸入以上儲存過程,儲存。 呼叫:
mysql> delimiter //
mysql> edit
-> //
Query OK, 0 rows affected (0.00 sec)
mysql> call p('mysort','2012-09-10_24');
-> //
Query OK, 0 rows affected (0.05 sec)
mysql> select * from mysort order by rank;
-> //
+----+------+-------+------+---------------+--------+
| id | name | score | rank | caldate | userid |
+----+------+-------+------+---------------+--------+
| 1 | a1 | 10 | 1 | 2012-09-10_24 | 151 |
| 2 | a2 | 8 | 2 | 2012-09-10_24 | 131 |
| 3 | a3 | 8 | 2 | 2012-09-10_24 | 12 |
| 5 | a5 | 2 | 4 | 2012-09-10_24 | 211 |
| 6 | a6 | 2 | 4 | 2012-09-10_24 | 212 |
| 4 | a4 | 1 | 6 | 2012-09-10_24 | 11 |
+----+------+-------+------+---------------+--------+
6 rows in set (0.00 sec)
儲存過程操作 最後,如果要檢視有哪些儲存過程列表:
mysql> show procedure status;
如果要檢視儲存過程內容:
mysql> show create procedure p;
如果要更新儲存過程,可以先刪除,再建立。
mysql> drop procedure p;
p是我的儲存過程名字。
mysql儲存過程除錯 我沒用什麼客戶端ide,我直接select相關變數輸出。
參考 排名方法討論: http://www.ericyue.info/archive/mysql-seek-rankings
mysql變數使用總結 http://www.cnblogs.com/wangtao_20/archive/2011/02/21/1959734.html
如非註明轉載, 均為原創. 本站遵循知識共享CC協議,轉載請註明來源