请帮我优化这个MySQL SELECT语句

时间:2016-04-17 07:31:50

标签: mysql sql sql-tuning

我有一个查询大约需要四分钟才能在高性能SSD服务器上运行而没有其他值得注意的进程在运行。如果可能的话,我想让它更快。

数据库存储一个名为Dota 2的热门视频游戏的匹配历史记录。在这个游戏中,每个选择一个"英雄"和战斗吧。

我的查询意图是创建一个过去的匹配列表以及多少的" XP依赖"基于所使用的英雄,每个团队都有。有200,000个匹配(和2,000,000行匹配到英雄关系表),查询大约需要四分钟。拥有1,000,000场比赛,大约需要15场比赛。

我完全控制了服务器,因此也欢迎任何配置建议。谢谢你的帮助。以下是详细信息......

CREATE TABLE matches (
*   match_id BIGINT UNSIGNED NOT NULL,
    start_time INT UNSIGNED NOT NULL,
    skill_level TINYINT NOT NULL DEFAULT -1,
*   winning_team TINYINT UNSIGNED NOT NULL,
    PRIMARY KEY (match_id),
    KEY start_time (start_time),
    KEY skill_level (skill_level),
    KEY winning_team (winning_team));

CREATE TABLE heroes (
*   hero_id SMALLINT UNSIGNED NOT NULL,
    name CHAR(40) NOT NULL DEFAULT '',
    faction TINYINT NOT NULL DEFAULT -1,
    primary_attribute TINYINT NOT NULL DEFAULT -1,
    group_index TINYINT NOT NULL DEFAULT -1,
    match_count BIGINT UNSIGNED NOT NULL DEFAULT 0,
    win_count BIGINT UNSIGNED NOT NULL DEFAULT 0,
*   xp_from_wins BIGINT UNSIGNED NOT NULL DEFAULT 0,
*   team_xp_from_wins BIGINT UNSIGNED NOT NULL DEFAULT 0,
    xp_from_losses BIGINT UNSIGNED NOT NULL DEFAULT 0,
    team_xp_from_losses BIGINT UNSIGNED NOT NULL DEFAULT 0,
    gold_from_wins BIGINT UNSIGNED NOT NULL DEFAULT 0,
    team_gold_from_wins BIGINT UNSIGNED NOT NULL DEFAULT 0,
    gold_from_losses BIGINT UNSIGNED NOT NULL DEFAULT 0,
    team_gold_from_losses BIGINT UNSIGNED NOT NULL DEFAULT 0,
    included TINYINT UNSIGNED NOT NULL DEFAULT 0,
    PRIMARY KEY (hero_id));

CREATE TABLE matches_heroes (
*   match_id BIGINT UNSIGNED NOT NULL,
    player_id INT UNSIGNED NOT NULL,
*   hero_id SMALLINT UNSIGNED NOT NULL,
    xp_per_min SMALLINT UNSIGNED NOT NULL,
    gold_per_min SMALLINT UNSIGNED NOT NULL,
    position TINYINT UNSIGNED NOT NULL,
    PRIMARY KEY (match_id, hero_id),
    KEY match_id (match_id),
    KEY player_id (player_id),
    KEY hero_id (hero_id),
    KEY xp_per_min (xp_per_min),
    KEY gold_per_min (gold_per_min),
    KEY position (position));

查询

SELECT
    matches.match_id,
    SUM(CASE     
        WHEN position < 5 THEN xp_from_wins / team_xp_from_wins     
        ELSE 0    
    END) AS radiant_xp_dependence,
    SUM(CASE     
        WHEN position >= 5 THEN xp_from_wins / team_xp_from_wins     
        ELSE 0    
    END) AS dire_xp_dependence,
    winning_team   
FROM
    matches   
INNER JOIN
    matches_heroes     
        ON matches.match_id = matches_heroes.match_id   
INNER JOIN
    heroes     
        ON matches_heroes.hero_id = heroes.hero_id   
GROUP BY
    matches.match_id

示例结果

match_id   | radiant_xp_dependence | dire_xp_dependence | winning_team

2298874871 | 1.0164                | 0.9689             | 1
2298884079 | 0.9932                | 1.0390             | 0
2298885606 | 0.9877                | 1.0015             | 1

EXPLAIN

id | select_type | table          | type   | possible_keys            | key     | key_len | ref                            | rows | Extra

1  | SIMPLE      | heroes         | ALL    | PRIMARY                  | NULL    | NULL    | NULL                           | 111  | Using temporary; Using filesort
1  | SIMPLE      | matches_heroes | ref    | PRIMARY,match_id,hero_id | hero_id | 2       | dota_2.heroes.hero_id          | 3213 |
1  | SIMPLE      | matches        | eq_ref | PRIMARY                  | PRIMARY | 8       | dota_2.matches_heroes.match_id | 1    |

机器规格

  • Intel Xeon E5
  • E5-1630v3 4 / 8t
  • 3.7 / 3.8 GHz
  • 64 GB的RAM
  • DDR4 ECC 2133 MHz
  • 2 x 480GB的SSD SOFT

数据库

  • MariaDB 10.0
  • InnoDB的

4 个答案:

答案 0 :(得分:3)

很可能主要的性能驱动因素是GROUP BY。有时,在MySQL中,使用相关子系统会更快。所以,尝试编写这样的查询:

SELECT m.match_id,
       (SELECT SUM(h.xp_from_wins / h.team_xp_from_wins)
        FROM matches_heroes mh INNER JOIN
             heroes h   
             ON mh.hero_id = h.hero_id
        WHERE m.match_id = mh.match_id AND mh.position < 5
       ) AS radiant_xp_dependence,
       (SELECT SUM(h.xp_from_wins / h.team_xp_from_wins)
        FROM matches_heroes mh INNER JOIN
             heroes h   
             ON mh.hero_id = h.hero_id
        WHERE m.match_id = mh.match_id AND mh.position >= 5
       ) AS dire_xp_dependence,
       m.winning_team   
FROM matches m;

然后,您需要索引:

  • matches_heroes(match_id, position)
  • heroes(hero_id, xp_from_wins, team_xp_from_wins)

为完整起见,您可能也需要此索引:

  • matches(match_id, winning_team)

如果您将order by match_id添加到查询中,这将更为重要。

答案 1 :(得分:2)

正如评论中已经提到的那样;你可以做的很少,因为你从表中选择了所有数据。查询看起来很完美。

想到的一个想法是覆盖索引。使用包含查询所需的所有数据的索引,不再需要访问表本身。

CREATE INDEX matches_quick ON matches(match_id, winning_team);

CREATE INDEX heroes_quick ON heroes(hero_id, xp_from_wins, team_xp_from_wins);

CREATE INDEX matches_heroes_quick ON matches_heroes (match_id, hero_id, position);

由于您仍然在读取所有数据,因此无法保证加快查询速度,因此运行索引可能与读取表格一样多。但是,连接有可能更快,并且可能会有更少的物理读取。试试吧。

答案 2 :(得分:1)

等待另一个想法? : - )

嗯,总有数据仓库方法。如果您必须一次又一次地运行此查询并且始终对所有已播放的匹配进行操作,那么为什么不存储查询结果并在以后访问它们呢?

我认为所播放的比赛不会被改变,所以你可以访问你计算的所有结果,比如上周,并且只从你真实的桌子中检索游戏中的其他结果。

创建表格archived_results。在archived表格中添加标记matches。然后将查询结果添加到archived_results表,并将这些匹配的标志设置为TRUE。当您必须执行查询时,您要么重新更新archived_results表格,只显示其内容,要么将归档和当前内容组合在一起:

select match_id, radiant_xp_dependence, radiant_xp_dependence winning_team
from archived_results
union all
SELECT
    matches.match_id,
    SUM(CASE     
        WHEN position < 5 THEN xp_from_wins / team_xp_from_wins     
        ELSE 0    
    END) AS radiant_xp_dependence,
...
WHERE matches.archived = FALSE
GROUP BY matches.match_id;

答案 3 :(得分:1)

人们关于将整个表格加载到内存中的评论让我思考。我搜索了“MySQL内存分配”,并学习了如何更改InnoDB表的缓冲池大小。默认值比我的数据库小得多,所以我使用my.cnf中的innodb_buffer_pool_size指令将其增加到8 GB。查询速度从1308秒急剧增加到114.

在研究了更多设置之后,我的my.cnf文件现在看起来如下(没有进一步的速度改进,但在其他情况下应该更好)。

[mysqld]
bind-address=127.0.0.1
character-set-server=utf8
collation-server=utf8_general_ci
innodb_buffer_pool_size=8G
innodb_buffer_pool_dump_at_shutdown=1
innodb_buffer_pool_load_at_startup=1
innodb_flush_log_at_trx_commit=2
innodb_log_buffer_size=8M
innodb_log_file_size=64M
innodb_read_io_threads=64
innodb_write_io_threads=64

感谢大家花时间帮忙。这对我的网站来说是一个巨大的改进。