Mysql 为什么添加一个内部连接会使这个查询变得如此缓慢?

Mysql 为什么添加一个内部连接会使这个查询变得如此缓慢?,mysql,sql,Mysql,Sql,我有一个包含以下三个表的数据库: matches表有200000个匹配项 CREATE TABLE `matches` ( `match_id` bigint(20) unsigned NOT NULL, `start_time` int(10) unsigned NOT NULL, PRIMARY KEY (`match_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 英雄表有大约100个英雄 CREATE TABLE `heroes` ( `hero_i

我有一个包含以下三个表的数据库:

matches表有200000个匹配项

CREATE TABLE `matches` (
`match_id` bigint(20) unsigned NOT NULL,
`start_time` int(10) unsigned NOT NULL,
PRIMARY KEY (`match_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
英雄表有大约100个英雄

CREATE TABLE `heroes` (
`hero_id` smallint(5) unsigned NOT NULL,
`name` char(40) NOT NULL,
PRIMARY KEY (`hero_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
matches_heroes表有2000000个关系(每场比赛10个随机英雄)

下面的查询耗时1秒,对于如此简单的事情,我觉得速度相当慢:

SELECT SQL_NO_CACHE COUNT(*) AS match_count
FROM matches INNER JOIN matches_heroes ON matches.match_id = matches_heroes.match_id
WHERE hero_id = 5
只删除WHERE子句没有帮助,但是如果我也删除了内部连接,就像这样:

SELECT SQL_NO_CACHE COUNT(*) AS match_count FROM matches
…只需0.05秒。似乎内部连接非常昂贵。我在加入方面没有太多经验。这是正常的还是我做错了什么

更新1:下面是解释结果

id  select_type  table          type   possible_keys                     key     key_len  ref                                rows  Extra  
1   SIMPLE       matches_heroes ref    match_id,hero_id,match_id_hero_id hero_id 2        const                              34742
1   SIMPLE       matches        eq_ref PRIMARY                           PRIMARY 8        mydatabase.matches_heroes.match_id 1     Using index
更新#2:在听了你们的发言后,我认为它工作正常,而且速度很快。如果你不同意,请告诉我。谢谢你的帮助。非常感谢。

使用
COUNT(matches.match\u id)
而不是
COUNT(*)
,因为在使用连接时,最好不要使用
*
,因为它会进行额外的计算。使用联接中的列是确保不请求任何其他操作的最佳方法。(MySql InnerJoin没有问题,我的错)

此外,您还应该验证是否已对所有密钥进行碎片整理,以及是否有足够的可用ram将索引加载到内存中

更新1:
尝试为
match\u id,hero\u id
添加一个组合索引,因为它会提供更好的性能

ALTER TABLE `matches_heroes` ADD KEY `match_id_hero_id` (`match_id`,`hero_id`)

更新2:
我对公认的答案不满意,mysql的速度太慢了,只有2百万条记录,我在我的ubuntu PC上运行了基准测试(i7处理器,带标准HDD)

相当于:

SELECT SQL_NO_CACHE COUNT(*) AS match_count 
FROM matches_heroes 
WHERE hero_id = 5` 

因此,如果这是您需要的计数,您不需要联接,但我猜这只是一个示例。

那么您说读取一个包含200000条记录的表要比读取一个包含2000000条记录的表快,找到所需的记录,然后将它们全部带到200000条记录表中查找匹配的记录

这让你惊讶吗?对于dbms来说,这只是更多的工作。(顺便说一句,当dbms认为全表扫描更快时,它甚至可能决定不使用hero_id索引。)


所以在我看来,这里发生的事情没有什么不对。

你有英雄id的索引吗?EXPLAIN()告诉您有关该查询的信息是什么?看起来
hero\u id=5
需要进行表扫描。也许您可以在该字段上添加索引。在字段
匹配英雄。英雄\u id
匹配英雄。匹配英雄id
上有没有索引的外键。我建议您将索引添加到
匹配的英雄。英雄id
并重复查询。@Andomar,@Cornelius,@user1516873我看过他的执行计划,他在那里有键:
键匹配id(匹配id),键英雄id(英雄id),
创建表中,@clickstefan:你说得对,我看到你删除了第一段,因为它与内部连接问题无关。我想补充一点,这甚至是错误的。COUNT(*)仅统计记录,而COUNT(matches.match\u id)统计matches.match\u id不为空的记录。因此,
COUNT(matches.match\u id)
可能会导致额外的工作,反之亦然。尽可能使用计数(*);仅当您确实希望只计算非空值或不同值(使用关键字distinct)时才使用COUNT(某物)。我可以在phpMyAdmin中“对键进行碎片整理”吗?我会尝试添加关键建议,如果它不起作用,我会把解释内容放在那里的帖子里。哦,ic。你是说双重索引是更好的设计吗?我不确定这个关系id是否必要。我应该把它拿出来,用匹配和英雄id组合作为主键吗@托尔斯滕Kettner@ThorstenKettner是的,在本例中您是对的,但是一些sql实现中的count(*)没有得到优化,并且发现使用联接中使用的列可以提供最佳性能,尽管正如您在某些情况下所说,它可以改变结果。Optimize似乎没有做任何事情。但当我重新创建数据库时,它工作得非常好!说真的,谢谢你的帮助!我已经认为这是最快的,可能会有一个痛苦缓慢的网站,可能从来没有被修复过。非常感谢。从负分到最佳答案^_^是的,我知道应该慢一点。看起来,当它们被索引时,底层数据库逻辑将能够非常快速地找到所需的英雄ID,然后反过来非常快速地统计与它们相关的匹配项。我不知道数据库是如何工作的,但我假设这是树或映射在编程中的工作方式,这将是一个简单的/2/2/2问题,直到找到指向hero_id=5的索引,然后计算它所指向的匹配项(在本例中,这等于hero_id=5的原始计数)。但是,是的,我想这可能比我预期的要花更多的时间。我同意,从查询优化的角度来看,你现在所能做的就是优化你的服务器配置。例如,我的i7 Linux PC可以在大约1.3秒内在3900万到1700万个表之间执行类似的连接查询,而无需组合键,因此如果您有硬件,您应该能够增加该值。啊,我没有访问该机器的权限。我正在使用Bluehost VPS服务。我可以进入WHM并更改设置,如果您有任何配置提示足够简单,可以添加注释的话@clickstefan
-- pre-requirements

CREATE TABLE seq_numbers (
    number INT NOT NULL
) ENGINE = MYISAM;


DELIMITER $$CREATE PROCEDURE InsertSeq(IN MinVal INT, IN MaxVal INT)
    BEGIN
        DECLARE i INT;
        SET i = MinVal;
        START TRANSACTION;
        WHILE i <= MaxVal DO
            INSERT INTO seq_numbers VALUES (i);
            SET i = i + 1;
        END WHILE;
        COMMIT;
    END$$
DELIMITER ;
CALL InsertSeq(1,200000)
;

ALTER TABLE seq_numbers ADD PRIMARY KEY (number)
;

--  create tables

-- DROP TABLE IF EXISTS `matches`
CREATE TABLE `matches` (
`match_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`start_time` int(10) unsigned NOT NULL,
PRIMARY KEY (`match_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8
;

CREATE TABLE `heroes` (
`hero_id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
`name` char(40) NOT NULL,
PRIMARY KEY (`hero_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8
;

CREATE TABLE `matches_heroes` (
`match_id` bigint(20) unsigned NOT NULL,
`hero_id` smallint(6) unsigned NOT NULL,
PRIMARY KEY (`match_id`,`hero_id`),
KEY (match_id),
KEY (hero_id),
CONSTRAINT `matches_heroes_ibfk_2` FOREIGN KEY (`hero_id`) REFERENCES `heroes` (`hero_id`),
CONSTRAINT `matches_heroes_ibfk_1` FOREIGN KEY (`match_id`) REFERENCES `matches` (`match_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=MyISAM DEFAULT CHARSET=utf8
;
-- insert DATA
-- 100
INSERT INTO heroes(name)
SELECT SUBSTR(CONCAT(char(RAND()*25+65),char(RAND()*25+97),char(RAND()*25+97),char(RAND()*25+97),char(RAND()*25+97),char(RAND()*25+97),char(RAND()*25+97),char(RAND()*25+97),char(RAND()*25+97),char(RAND()*25+97),char(RAND()*25+97),char(RAND()*25+97)),1,RAND()*9+4) as RandomName
FROM seq_numbers WHERE number <= 100

-- 200000
INSERT INTO matches(start_time)
SELECT rand()*1000000
FROM seq_numbers WHERE number <= 200000

-- 2000000
INSERT INTO matches_heroes(hero_id,match_id)
SELECT a.hero_id, b.match_id
FROM heroes as a
INNER JOIN matches as b ON 1=1
LIMIT 2000000

-- warm-up database, load INDEXes in ram (optional, works only for MyISAM tables)
LOAD INDEX INTO CACHE matches_heroes,matches,heroes


-- get random hero_id
SET @randHeroId=(SELECT hero_id FROM matches_heroes ORDER BY rand() LIMIT 1);


-- test 1 

SELECT SQL_NO_CACHE @randHeroId,COUNT(*) AS match_count
FROM matches as a 
INNER JOIN matches_heroes as b ON a.match_id = b.match_id
WHERE b.hero_id = @randHeroId
; -- Time: 0.039s


-- test 2: adding some complexity 
SET @randName = (SELECT `name` FROM heroes WHERE hero_id = @randHeroId LIMIT 1);

SELECT SQL_NO_CACHE @randName, COUNT(*) AS match_count
FROM matches as a 
INNER JOIN matches_heroes as b ON a.match_id = b.match_id
INNER JOIN heroes as c ON b.hero_id = c.hero_id
WHERE c.name = @randName
; -- Time: 0.037s
SELECT SQL_NO_CACHE COUNT(*) AS match_count 
FROM matches INNER JOIN matches_heroes ON matches.match_id = matches_heroes.match_id 
WHERE hero_id = 5` 
SELECT SQL_NO_CACHE COUNT(*) AS match_count 
FROM matches_heroes 
WHERE hero_id = 5`