请帮我优化这个 MySQL SELECT 语句

Please help me optimize this MySQL SELECT statement

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

数据库存储了一款名为 Dota 2 的流行视频游戏的比赛历史。在这款游戏中,十名玩家(每队五名)每人 select 一个 "hero" 并进行战斗。

我查询的目的是根据使用的英雄创建一个过去比赛的列表,以及每支球队有多少"XP dependence"。对于 200,000 个匹配项(以及 2,000,000 行匹配到英雄的关系 table),查询大约需要四分钟。 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

解释

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    |

机器规格

数据库

评论里已经提到了;您几乎无能为力,因为您 select 来自 table 的所有数据。查询看起来很完美。

想到的一个想法是覆盖索引。有了包含查询所需的所有数据的索引,table 本身就不必再访问了。

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);

不能保证这会加速您的查询,因为您仍在读取所有数据,因此 运行 通过索引可能与读取 table 一样多。但是连接速度可能会更快,并且物理读取可能会更少。试试吧。

还在等另一个主意吗? :-)

嗯,总有 数据仓库 方法。如果您必须 运行 一次又一次地查询所有比赛,那么为什么不存储查询结果并在以后访问它们呢?

我想打的比赛不会被改变,所以你可以访问你计算的所有结果,比方说,上周,并且只从你真正的 tables 中检索自那以后的比赛的额外结果。

创建一个tablearchived_results。在你的 matches table 中添加一个标记 archived。然后将查询结果添加到 archived_results table 并将这些匹配项的标志设置为 TRUE。当必须执行查询时,您可以重新更新 archived_results table 并仅显示其内容,或者将存档和当前合并:

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;

很可能,主要的性能驱动因素是 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 添加到查询中,这将更加重要。

人们关于将整个表加载到内存中的评论让我开始思考。我搜索了 "MySQL memory allocation" 并了解了如何更改 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

感谢大家花时间帮忙。这将是我网站的巨大改进。