1. 程式人生 > >mysql 中對使用者分數排名

mysql 中對使用者分數排名

2012-09-11

周海漢/文 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協議,轉載請註明來源