【问题标题】:Mysql query statement index tuningMysql查询语句索引调优
【发布时间】:2012-07-19 02:46:42
【问题描述】:

我正在研究如何为我的公司发布的 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 colum 'rank' in 'field list' 错误,第三个只是返回一个空集而不是我正在寻找的数据。

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

【问题讨论】:

  • 查询的示例结果集是什么?
  • 我编辑了我的原始帖子以包含两个查询的结果集。
  • 不是每次有人想查看排名时都计算这个,您是否考虑过将排名存储在表格中并在每次有人记录新分数时更新它?这将导致很少的计算。
  • 我想过那样做。将每个用户的当前排名存储在表中。不利的一面是,我必须不断地用新计算的等级更新表格。虽然如果您查看 20 行并不一定很糟糕,但如果表中有 250 万行,并且有人刚刚更新到排名 2,那么我必须更新超过 240 万条记录。
  • 只有在用户第一次记录分数时才会出现这种情况。实际上,大多数更新可能只是分数的局部小变化,因为玩家不会经常大幅超过他们之前的高分。

标签: php mysql database optimization


【解决方案1】:

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

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

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

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

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

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

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

【讨论】:

  • 相信我,我更愿意只进入前 100 名,而不是全球排名。但是,唉,最终还是我的老板要求做最后的决定。我只能提出建议!如果我必须做一个全球排行榜,我唯一担心这个解决方案是我必须存储的数据量。游戏中有 400 场比赛,如果我必须为每场比赛存储几百万条记录/次,并为每个表格存储一个排名表,那么需要存储大量数据。但就像我已经提到的那样,它肯定会在前 100 名的情况下运作良好。
  • 我会模拟一个示例,看看它实际占用了多少空间。我不知道您认为什么是“大”数据库。我来自哪里,2TB 是一个大数据库。
  • 不过,关于可扩展性的问题,我承认实际上您可能只能期望 N 个用户,而设计超出此范围是没有意义的。
  • 哈哈是的,“大”绝对是一个非常相对的术语,所以我应该更具体一点。对此我深表歉意!老实说,我更担心必须对所有数据进行排序,而不是实际大小。我们的服务器是 Amazon AWS EC2 实例,我经常被要求使用尽可能小的服务器来节省运行时成本。因此,它根据 cpu/内存限制限制了我能/不能做的事情。而且我也认为您是对的,尝试扩展 N 个用户可能是不切实际/毫无意义的。
【解决方案2】:

您可以先使用 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

【讨论】:

    【解决方案3】:

    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 */
    }
    

    【讨论】:

    • 嘿 Puggan Se 我绝对喜欢极限范围的想法。我试了一下,它确实改善了大约 0.2 秒。唯一要扔掉的是我没有存储排名列。我在查询中动态生成它,以避免随着等级的变化而不断更新表。因此,我无法找到从排名列构建的索引,而是必须进行全表查询。
    • 获取当前用户的时间,然后在您的选择上添加一个 where time
    猜你喜欢
    • 2013-05-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-02-24
    • 2012-01-27
    • 1970-01-01
    相关资源
    最近更新 更多