请帮我优化这个 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 |
机器规格
- 英特尔至强 E5
- E5-1630v3 4/8t
- 3.7 / 3.8 GHz
- 64 GB 内存
- DDR4 ECC 2133 MHz
- 2 x 480GB SSD SOFT
数据库
- MariaDB 10.0
- InnoDB
评论里已经提到了;您几乎无能为力,因为您 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
感谢大家花时间帮忙。这将是我网站的巨大改进。
我有一个查询,在没有其他 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 |
机器规格
- 英特尔至强 E5
- E5-1630v3 4/8t
- 3.7 / 3.8 GHz
- 64 GB 内存
- DDR4 ECC 2133 MHz
- 2 x 480GB SSD SOFT
数据库
- MariaDB 10.0
- InnoDB
评论里已经提到了;您几乎无能为力,因为您 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
感谢大家花时间帮忙。这将是我网站的巨大改进。