如何优化MySQL游戏排行榜 - 大规模子查询问题

时间:2013-06-21 07:20:43

标签: php mysql sql performance query-optimization

我的游戏服务器上有一个巨大的瓶颈,用于以下查询,用于存储当前的排行榜。

我目前只通过cron每隔5分钟调用一次此查询,但是我希望能够优化它以便每分钟或在需要时调用它。

查询耗时30秒,目前只有约2000个用户和7000个游戏(存储在Games和TopPlayerScores中)。我担心它会变得更糟! 请帮帮我溢出 - 克诺比!你唯一的希望!

SET @rank=0;
INSERT INTO Board (TopScorePKID, GamePKID, UserPKID, UniquePlayerID, PlayerName, TopPlayerScore, Position, Date)
(SELECT bad.ID AS TopScorePKID, bad.GamePKID, bad.UserPKID, bad.UniquePlayerID, bad.PlayerName, bad.TopPlayerScore, @rank:=@rank+1 AS Position, bad.Date
FROM (
    SELECT g.GamePKID, g.TopPlayerScore, l.ID,  l.UserPKID, u.UniquePlayerID, u.PlayerName, (l.Date) AS Date
    FROM Games g, TopPlayerScores l, UserDetails u
    WHERE l.GamePKID = g.GamePKID
    AND u.UserPKID = l.UserPKID
    AND u.SECRET_DETAIL = 0 
    AND g.TopPlayerScore >= (SELECT DISTINCT k.TopPlayerScore AS Highest 
        FROM Games k, TopPlayerScores t 
        WHERE t.UserPKID = l.UserPKID
        AND k.GamePKID = t.GamePKID
        ORDER BY k.TopPlayerScore DESC
        LIMIT 1) 
    GROUP BY l.UserPKID
    ORDER BY g.TopPlayerScore DESC, Date ASC) 
AS bad);

请有人帮忙!我应该把它分解成观点吗?或使用Inner Join关键字?什么是最好的方法?

非常感谢甚至看着这个烂摊子:D!

更新1.0: EXPLAIN EXTENDED结果:

id  select_type table   type    possible_keys   key key_len ref rows    filtered    Extra
1   PRIMARY   ALL NULL    NULL    NULL    NULL    1521    100.00  
2   DERIVED l   ALL NULL    NULL    NULL    NULL    6923    100.00  Using temporary; Using filesort
2   DERIVED u   eq_ref  PRIMARY PRIMARY 4   DBNAME.l.UserPKID   1   100.00  Using where
2   DERIVED k   eq_ref  PRIMARY PRIMARY 4   DBNAME.l.GamePKID   1   100.00  Using where
3   DEPENDENT SUBQUERY  t   ALL NULL    NULL    NULL    NULL    6923    100.00  Using where; Using temporary; Using filesort
3   DEPENDENT SUBQUERY  g   eq_ref  PRIMARY PRIMARY 4   DBNAME.t.GamePKID   1   100.00  Using where

更新2.0: 用于查询表的有限模式

使用游戏存储游戏分数和有关游戏的其他信息

`Games` (
  `GamePKID` int(11) NOT NULL AUTO_INCREMENT,
  `TopPlayerScore` int(11) NOT NULL,
  `OTHER_MISC_STUFF_REMOVED` int(11) NOT NULL
  PRIMARY KEY (`GamePKID`)
)

使用以下内容将用户链接到游戏并存储时间/日期

`TopPlayerScores` (
  `ID` int(11) NOT NULL AUTO_INCREMENT,
  `UserPKID` int(11) NOT NULL,
  `GamePKID` int(11) NOT NULL,
  `Date` datetime NOT NULL,
  PRIMARY KEY (`ID`)
)

用于存储每个独特的播放器

`UserDetails` (
  `UserPKID` int(11) NOT NULL AUTO_INCREMENT,
  `UniquePlayerID` char(40) NOT NULL,
  `PlayerName` char(96) NOT NULL,
  `SECRET_DETAIL` tinyint(1) NOT NULL DEFAULT '0',
  `isPlayer` tinyint(4) DEFAULT NULL,
  PRIMARY KEY (`UserPKID`)
)

1 个答案:

答案 0 :(得分:4)

我要注意的第一件事,虽然这不会提高性能,但是你使用的JOIN语法在20多年前被ANSI 92 expcict join语法所取代,它完全是主题,但是{{3切换到更新语法的一些非常好的理由。

要注意的第二件事是您的结果将是非确定性的。您正在选择未包含在聚合或组中的列。虽然MySQL允许这样做,但您没有像MySQL那样使用该功能。 Aaron Bertrand explains州:

  

MySQL扩展了GROUP BY的使用,以便选择列表可以引用   未在GROUP BY子句中命名的非聚合列。这意味着   前面的查询在MySQL中是合法的。您可以使用此功能   通过避免不必要的列排序来获得更好的性能   分组。但是,这主要适用于每个中的所有值   GROUP BY中未命名的非聚合列对于每个列都是相同的   组。服务器可以自由选择每个组中的任何值,所以   除非它们相同,否则所选择的值是不确定的。

但是,您所包含的某些列(g.GamePKIDg.TopPlayerScorel.IDl.Date)不满足条件是相同的因此,如上所述,每个组,MySQL可以自由选择它喜欢的任何值,即使你有ORDER BY g.TopPlayerScore DESC, Date ASC,这也不会影响MySQL选择的每个组的单行。

第三,MySQL docs这可能会影响性能。如果您可以将这些更改为JOIN,您应该会看到性能提升。

考虑到这一切,我会重写您的查询:

SET @rank=0;
INSERT INTO Board (TopScorePKID, GamePKID, UserPKID, UniquePlayerID, PlayerName, TopPlayerScore, Position, Date)
SELECT  bad.ID AS TopScorePKID, 
        bad.GamePKID, 
        bad.UserPKID, 
        bad.UniquePlayerID, 
        bad.PlayerName, 
        bad.TopPlayerScore, 
        @rank:=@rank+1 AS Position, 
        bad.Date
FROM    (   SELECT  g.GamePKID, 
                    g.TopPlayerScore, 
                    l.ID,  
                    l.UserPKID, 
                    u.UniquePlayerID, 
                    u.PlayerName, 
                    l.Date
            FROM    Games g
                    INNER JOIN TopPlayerScores l
                        ON l.GamePKID = g.GamePKID
                    INNER JOIN UserDetails u
                        ON u.UserPKID = l.UserPKID
                    INNER JOIN
                    (   SELECT  TopPlayerScores.UserPKID, MAX(games.TopPlayerScore) AS MaxPlayerScore
                        FROM    TopPlayerScores
                                INNER JOIN Games
                                    ON Games.GamePKID = TopPlayerScores.GamePKID
                        GROUP BY TopPlayerScores.UserPKID
                    ) MaxScore
                        ON MaxScore.UserPKID = l.UserPKID
                        AND MaxScore.MaxPlayerScore = g.TopPlayerScore
            WHERE   u.SECRET_DETAIL = 0 
        ) AS bad
ORDER BY bad.TopPlayerScore DESC, bad.Date ASC;

<强> MySQL has limitations with correlated subqueries

子查询MaxScore应该具有将结果限制为每个玩家一行(只有他们的最高得分)的效果,尽管可能需要额外的逻辑来处理关系(例如,玩家在更多场景中具有相同的得分)比一场比赛)。在不知道确切要求的情况下,我无法纠正这一点。

修改

为了消除玩家在2个或更多游戏中获得相同最高分的重复项,并使其真正具有确定性,您需要添加更多子查询:

SET @rank=0;

SELECT  bad.ID AS TopScorePKID, 
        bad.GamePKID, 
        bad.UserPKID, 
        bad.UniquePlayerID, 
        bad.PlayerName, 
        bad.TopPlayerScore, 
        @rank:=@rank+1 AS Position, 
        bad.Date
FROM    (   SELECT  Games.GamePKID, 
                    Games.TopPlayerScore, 
                    TopPlayerScores.ID,  
                    TopPlayerScores.UserPKID, 
                    UserDetails.UniquePlayerID, 
                    UserDetails.PlayerName, 
                    TopPlayerScores.Date
            FROM    Games
                    INNER JOIN TopPlayerScores
                        ON TopPlayerScores.GamePKID = Games.GamePKID
                    INNER JOIN UserDetails
                        ON UserDetails.UserPKID = TopPlayerScores.UserPKID
                    INNER JOIN
                    (   SELECT  TopPlayerScores.UserPKID, MAX(games.TopPlayerScore) AS TopPlayerScore
                        FROM    TopPlayerScores
                                INNER JOIN Games
                                    ON Games.GamePKID = TopPlayerScores.GamePKID
                        GROUP BY TopPlayerScores.UserPKID
                    ) MaxScore
                        ON MaxScore.UserPKID = TopPlayerScores.UserPKID
                        AND MaxScore.TopPlayerScore = Games.TopPlayerScore
                    INNER JOIN
                    (   SELECT  TopPlayerScores.UserPKID, games.TopPlayerScore, MAX(Date) AS Date
                        FROM    TopPlayerScores
                                INNER JOIN Games
                                    ON Games.GamePKID = TopPlayerScores.GamePKID
                        GROUP BY TopPlayerScores.UserPKID, games.TopPlayerScore
                    ) MaxScoreDate
                        ON MaxScoreDate.UserPKID = TopPlayerScores.UserPKID
                        AND MaxScoreDate.TopPlayerScore = Games.TopPlayerScore
                        AND MaxScoreDate.Date = TopPlayerScores.Date
            WHERE   UserDetails.SECRET_DETAIL = 0 
        ) AS bad
ORDER BY bad.TopPlayerScore DESC, bad.Date ASC;

<强> Example on SQL Fiddle


N.B。如果/当MySQL引入诸如ROW_NUMBER()之类的分析函数,或者如果你切换到已经支持它们的DBMS,这个查询将变得更加简单,所以为了防止这些事情发生,这里有一个使用ROW_NUMBER的解决方案( )`

SELECT  bad.ID AS TopScorePKID, 
        bad.GamePKID, 
        bad.UserPKID, 
        bad.UniquePlayerID, 
        bad.PlayerName, 
        bad.TopPlayerScore, 
        ROW_NUMBER() OVER(ORDER BY TopPlayerScore DESC) AS Position, 
        bad.Date
FROM    (   SELECT  Games.GamePKID, 
                    Games.TopPlayerScore, 
                    TopPlayerScores.ID,  
                    TopPlayerScores.UserPKID, 
                    UserDetails.UniquePlayerID, 
                    UserDetails.PlayerName, 
                    TopPlayerScores.Date,
                    ROW_NUMBER(PARTITION BY UserDetails.UserPKID 
                                ORDER BY Games.TopPlayerScore DESC,
                                        TopPlayerScores.Date DESC) AS RN
            FROM    Games
                    INNER JOIN TopPlayerScores
                        ON TopPlayerScores.GamePKID = Games.GamePKID
                    INNER JOIN UserDetails
                        ON UserDetails.UserPKID = TopPlayerScores.UserPKID
            WHERE   UserDetails.SECRET_DETAIL = 0 
        ) AS bad
WHERE   bad.RN = 1
ORDER BY bad.TopPlayerScore DESC, bad.Date ASC;

<强> Example on SQL Fiddle