【问题标题】:Why does adding an INNER JOIN make this query so slow?为什么添加 INNER JOIN 会使这个查询变得如此缓慢?
【发布时间】:2014-11-04 00:03:51
【问题描述】:

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

matches 表有 200,000 个匹配项...

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 表有 2,000,000 个关系(每场比赛 10 个随机英雄)...

CREATE TABLE `matches_heroes` (
`relation_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`match_id` bigint(20) unsigned NOT NULL,
`hero_id` smallint(6) unsigned NOT NULL,
PRIMARY KEY (`relation_id`),
KEY `match_id` (`match_id`),
KEY `hero_id` (`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=InnoDB AUTO_INCREMENT=3689891 DEFAULT CHARSET=utf8

以下查询需要 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 子句没有帮助,但如果我也取出 INNER JOIN,如下所示:

SELECT SQL_NO_CACHE COUNT(*) AS match_count FROM matches

...只需要 0.05 秒。看来 INNER JOIN 的成本很高。我没有太多的连接经验。这是正常的还是我做错了什么?

更新 #1:这是 EXPLAIN 结果。

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:听了你们的意见后,我认为它工作正常,而且速度很快。如果您不同意,请告诉我。感谢所有的帮助。我真的很感激。

【问题讨论】:

  • 你有 hero_id 的索引吗? EXPLAIN() 告诉您关于该查询的什么信息?
  • 看起来hero_id = 5 需要进行表扫描。也许您可以在该字段上添加索引。
  • 您在字段matches_heroes.hero_idmatches_heroes.match_id 上有没有索引的外键。我建议您将索引添加到matches_heroes.hero_id 并重复查询。
  • @Andomar, @Cornelius, @user1516873 我看过他的执行计划,他在那里有钥匙:sqlfiddle.com/#!2/14a8ba/1KEY match_id (match_id), KEY hero_id (hero_id),Create table, key is an alias for index
  • @clickstefan:你是对的

标签: mysql sql


【解决方案1】:

使用COUNT(matches.match_id) 而不是count(*),因为在使用连接时最好不要使用*,因为它会进行额外的计算。使用连接中的列是确保您不请求任何其他操作的最佳方式。(在 MySql InnerJoin 上不是问题,我的错)。

您还应该确认您已对所有键进行碎片整理,并且有足够的可用内存供索引加载到内存中

更新 1:


尝试为match_id,hero_id 添加组合索引,因为它应该会提供更好的性能。

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


更新 2:


我对接受的答案不满意,mysql 对于只有 2 个磨机记录的速度就这么慢,我在我的 ubuntu PC(i7 处理器,带有标准 HDD)上运行了基准测试。

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

结论:测试结果快了大约 20 倍,测试前我的服务器负载大约是 80%,因为它不是专用的 mysql 服务器,并且正在运行其他 cpu 密集型任务,所以如果你运行整个脚本(从上面)并获得较低的结果可能是因为:

  1. 你有一个共享主机,负载太大。在这种情况下,您无能为力:要么向当前主机投诉,要么购买更好的主机/虚拟机,要么尝试其他主机
  2. 您配置的key_buffer_size(对于MyISAM)或innodb_buffer_pool_size(对于innoDB)太小,最佳大小应该超过150MB
  3. 您的可用内存不足,您需要大约 100 - 150 mb 的内存才能将索引加载到内存中。解决方案:释放一些内存或购买更多内存

请注意,通过使用测试脚本,新数据的生成排除了索引碎片问题。 希望这会有所帮助,并询问您是否在测试时遇到问题。


obs:


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` 

所以你不需要加入,如果这是你需要的计数,但我猜这只是一个例子。

【讨论】:

  • 我看到你删除了你的第一段,因为它与内部连接问题无关。我想补充一点,它甚至是错误的。 COUNT(*) 只计算记录,而 COUNT(matches.match_id) 计算matches.match_id 不为空的记录。所以COUNT(matches.match_id) 可能会导致额外的工作,反之亦然。尽可能使用 COUNT(*);仅当您真的只想计算非空值或计算不同值(使用关键字 DISTINCT)时才使用 COUNT(something)。
  • 我可以在 phpMyAdmin 中“对密钥进行碎片整理”吗?我会尝试 ADD KEY 建议,如果它不起作用,我会将 EXPLAIN 内容放在上面的帖子中。
  • 哦,我。你是说双索引会更好的设计吗?我不确定是否需要relation_id。我应该将其取出并使用匹配和英雄ID组合作为主键吗? @Thorsten Kettner
  • @ThorstenKettner 是的,在这种情况下你是对的,但是一些 sql 实现上的 count(*) 没有优化,并且发现使用连接中使用的列可以获得最佳性能,尽管你说在某些情况下它可以改变结果
  • OPTIMIZE 似乎没有做任何事情。但是,当我重新创建数据库时,它工作得很好!说真的,谢谢大佬的帮助!我已经假设它可能是最快的,并且会有一个可能永远无法修复的缓慢的网站。真的很感激。从负分变成最佳答案。 ^_^
【解决方案2】:

所以你说读取 200,000 条记录的表比读取 2,000,000 条记录的表,找到所需的记录,然后将它们全部在 200,000 条记录的表中找到匹配的记录要快吗?

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

所以在我看来,这里发生的事情没有任何问题。

【讨论】:

  • 是的,我知道它应该更慢。似乎当它们被索引时,底层数据库逻辑将能够非常快速地找到所需的 hero_ids,然后很快地计算与它们相关的匹配。我不知道数据库是如何工作的,但我认为这是树或地图在编程中的工作方式,这将是 / 2 / 2 / 2 / 2 的简单问题,直到找到指向 hero_id = 5 的索引,然后计算它指向的匹配项(在这种情况下,无论如何都等于 hero_id = 5 的原始计数)。但是,是的,我想这可能比我预期的要花更多的时间。
  • 我同意,从查询优化的角度来看,您现在所能做的就是优化您的服务器配置。例如,我的 i7 Linux PC 可以在大约 1.3 秒内在 3900 万和 1700 万表之间执行类似的连接查询,而无需组合键,因此如果您有硬件,您应该能够增加该值。
  • 啊,我无法访问机器。我正在使用 Bluehost VPS 服务。如果您有任何简单到足以发表评论的配置提示,我可以进入 WHM 并更改设置。 @clickstefan
猜你喜欢
  • 1970-01-01
  • 2014-02-05
  • 1970-01-01
  • 2019-10-09
  • 1970-01-01
  • 1970-01-01
  • 2019-04-22
  • 1970-01-01
  • 2017-09-18
相关资源
最近更新 更多