1

我正在研究如何为我公司发布的 Facebook 赛车游戏实施全球排行榜。我想做的是能够存储玩家的用户 ID 和他们的比赛时间。我有一张如下表:

+--------+-----------------------+------+-----+---------+-------+
| Field  | Type                  | Null | Key | Default | Extra |
+--------+-----------------------+------+-----+---------+-------+
| userID | mediumint(8) unsigned | NO   | PRI | 0       |       |
| time   | time                  | YES  | MUL | NULL    |       |
+--------+-----------------------+------+-----+---------+-------+

还有一组样本数据,如下所示:

+--------+----------+
| userID | time     |
+--------+----------+
| 505610 | 10:10:10 |
| 544222 | 10:10:10 |
| 547278 | 10:10:10 |
| 659241 | 10:10:10 |
| 681087 | 10:10:10 |
+--------+----------+

我的查询将来自 PHP。现在,如果我假设我有无限的资源,我能做的是:

$q1 = "Set @rank := 0";
$q2 = "select @rank:=@rank+1 as rank,userID,time from highscore order by time asc where userID=$someUserID";
$q3 = "Set @rank := 0";
$q4 = "select @rank:=@rank+1 as rank,userID,time from highscore order by time asc where rank > $rankFromSecondQuery - 10 and rank < $rankFromSecondQuery + 10";

但我没有无限的资源,我必须能够扩展它以支持数百万玩家,因为它正在进入 Facebook 上的社交游戏。因此,在花了几天时间在 Google 上爬行之后,我已经能够将我的查询归结为:

$q5 = "select rank,userID,time from (select @rank:=0) r, (select @rank:=@rank+1 as rank,userID,time from highscore order by time asc) as myMine where userID=$someUserID"
$q6 = "select rank,userID,time from (select @rank:=0) r, (select @rank:=@rank+1 as rank,userID,time from highscore order by time asc) as myMine where rank > $rankFromFirstQuery - 10 and rank < $rankFromSecondQuery + 10";

这可行,但它不是很漂亮,每个查询的平均运行时间约为 2.3 秒。

编辑:这是 $q5 和 $q6 在运行它们时给我的:

mysql> select rank,userID,time from (select @rank:=0) r, (select @rank:=@rank+1 as rank,userID,time from highscore order by time asc) as myMine where userID=11345;                                                                          
+--------+--------+----------+
| rank   | userID | time     |
+--------+--------+----------+
| 423105 |  11345 | 12:47:23 |
+--------+--------+----------+
1 row in set (2.42 sec)

mysql> select rank,userID,time from (select @rank:=0) r, (select @rank:=@rank+1 as rank,userID,time from highscore order by time asc) as myMine where rank>423100 and rank<423110;
+--------+---------+----------+
| rank   | userID  | time     |
+--------+---------+----------+
| 423101 | 2416665 | 12:47:22 |
| 423102 | 2419720 | 12:47:22 |
| 423103 | 2426606 | 12:47:22 |
| 423104 | 2488517 | 12:47:22 |
| 423105 |   11345 | 12:47:23 |
| 423106 |   92350 | 12:47:23 |
| 423107 |   94277 | 12:47:23 |
| 423108 |  114685 | 12:47:23 |
| 423109 |  135434 | 12:47:23 |
+--------+---------+----------+
9 rows in set (2.58 sec)

这是解释扩展块 $q5 和 $q6 的一个看起来几乎相同:

mysql> explain select rank,userID,time from (select @rank:=0) r, (select @rank:=@rank+1 as rank,userID,time from highscore order by time asc) as myMine where userID=11345;
+----+-------------+------------+--------+---------------+----------+---------+------+---------+----------------+
| id | select_type | table      | type   | possible_keys | key      | key_len | ref  | rows    | Extra          |
+----+-------------+------------+--------+---------------+----------+---------+------+---------+----------------+
|  1 | PRIMARY     | <derived2> | system | NULL          | NULL     | NULL    | NULL |       1 |                |
|  1 | PRIMARY     | <derived3> | ALL    | NULL          | NULL     | NULL    | NULL | 2500000 | Using where    |
|  3 | DERIVED     | highscore  | index  | NULL          | idx_time | 4       | NULL | 2500842 | Using index    |
|  2 | DERIVED     | NULL       | NULL   | NULL          | NULL     | NULL    | NULL |    NULL | No tables used |
+----+-------------+------------+--------+---------------+----------+---------+------+---------+----------------+

所以最终,我真正想做的就是把它简化为一个查询,这样我就可以使用一两个高 CPU 服务器来缓和执行时间。要么,要么我想找出一种方法,只在查询部分中命中与表中所有行的解释块中的派生3 行相关联的索引。

到目前为止,这是我尝试过的几个查询但没有成功:

select rank,userID,time from (select @rank:=0) r, (select @playerRank := rank from (select @rank:=@rank+1 as rank,userID,time from highscore order by time asc) as myMine where userID=11345) as myFoo where @playerRank>423100 and @playerRank<423110;
select rank,userID,time from (select @playerRank := rank from (select @rank := 0) r, (select @rank:=@rank+1 as rank,userID,time from highscore order by time asc) as myMine where userID=11345) as myFoo where @playerRank>423100 and @playerRank<423110;
select * from (select @rank:=0) r, (select @playerRank := userID from (select @rank:=@rank+1 as rank,userID,time from highscore order by time asc) as myMine where userID=11345) as myFoo where @playerRank>423100 and @playerRank<423110;

前两个游戏给我一个“错误 1054 (42S22): Unknown column 'rank' in 'field list' 错误,第三个只是返回一个空集而不是我正在寻找的数据。

任何人都知道如何让我上面列出的两个查询命中索引以减少执行时间,或者如何将两个查询合并为一个,这样我只需要忍受一次痛苦的执行时间?如果有人有使用类似东西的经验并想分享他们的经验,我也愿意接受调整/优化,例如调整 MySQL 配置设置和/或使用 Percona 之类的东西。

4

3 回答 3

0

我想提出这种替代解决方案来实现您想要实现的目标。

制作一个单独的表来存储排名。不要每次用户想知道他/她的排名时都计算它,也不要将它包含在现有表中。当分数更新与排名计算竞争时,将排名放在单独的表中有望缓解锁争用问题。

定期重新计算等级。当您执行此重新计算时,请截断等级表并从头开始重新创建它。使用批量加载操作 (LOAD DATA INFILE) 或将其设为 MyISAM 表(在表末尾插入时速度很快)来执行此操作。无论哪种方式都应该相对较快地实际写出表格;至少比更新已经存在的表中的数百万行更快。这两种方法都会使您的排名表变得脆弱,并且在发生崩溃时容易丢失,但这没关系,因为这本质上是瞬态数据。只要你的分数表是稳定的,你就是安全的。通过定期重新计算,您可以避免随着播放次数的增加而越来越频繁地进行计算,直到撞墙为止。

如果用户得分在前 100 名之内,则立即推出他们的新得分。用户可能希望浏览前 100 名,看看谁的得分最高。我认为几乎没有人想要真正浏览该点下方的列表。

允许用户立即查看他们朋友的分数,以及他们之间的相对排名。这可能是大多数用户感兴趣的排名。我知道我妻子在玩 Facebook 游戏时,她对自己的整体排名不感兴趣,但她很想知道她是否击败了她的大学同学。

显示玩家的整体排名,以及他们朋友的整体排名,在用户最近一次游戏后失效,并在下次更新准备好时异步加载。

另一个考虑因素是,如果这个游戏会持续几年,你的记分牌最终会被不活跃玩家的旧分数堵塞,尤其是在低端。您可能需要考虑是否值得存档这些分数。例如,您可以说,只有在最近 6 个月内参加过比赛的记分板中 75% 的球员才会被考虑在排名中。然后,将他们的分数移到存档表中,在那里他们将被记住,如果该球员返回,则可以将其恢复到记分牌中,但不必在每次计算排名时都包含在排序中。是的,这可能会使您的排名不那么“真实”,但无论如何人们只是为了好玩。这将产生使他们的排名看起来更好的副作用,这也很有趣。

于 2012-07-20T16:13:57.247 回答
0

在 runnint$q5之后,您应该知道用户的等级,之后您应该能够使用限制来获得正确的行

$lowest_rank_to_fetch = max(0, $rankFromFirstQuery - 10);
$q6l = "SELECT userID, time
        FROM highscore
        ORDER BY time ASC
        LIMIT {$lowest_rank_to_fetch}, 21";

/* some execute query function */

foreach(range($lowest_rank_to_fetch, $lowest_rank_to_fetch+21) as $current_rank)
{
   /* some database fetch function */
   /* add $current_rank to result */
}
于 2012-07-19T22:22:57.913 回答
0

您可以先使用 count() 获得排名,这对于第一个查询应该会更好一些:

SELECT COUNT(h.userID) as rank, h2.userID, h2.time
   FROM highscore h
   LEFT OUTER JOIN highscore h2 ON (h.time <= h2.time)
   WHERE h2.userID = ?

然后您可以使用 Puggan 的技术来查询附近的排名。

SELECT ... ORDER BY time LIMIT $lowest_rank, 21
于 2012-07-20T00:57:45.523 回答