具有多个连接的 MySQL 查询的低效执行计划

Posted

技术标签:

【中文标题】具有多个连接的 MySQL 查询的低效执行计划【英文标题】:Inefficient execution plan on MySQL query with multiple joins 【发布时间】:2015-06-16 17:08:22 【问题描述】:

我遇到了 mysql 的性能问题;似乎我的请求的执行计划远非最佳,但我不知道 MySQL 为什么选择它,也不知道如何更改它。我在最小的环境中重现了这个问题,这里是查询:

SELECT member.id, member_cache.id, section.id, topic.id
FROM topic
INNER JOIN (section
    INNER JOIN (member
        LEFT JOIN (member_cache) ON member_cache.id = member.id
    ) ON member.id = section.last_member
) ON section.id = topic.section
WHERE topic.last_time IS NOT NULL
ORDER BY topic.last_time DESC
LIMIT 0, 1

以下是此查询中使用的表:

CREATE TABLE `member` (`id` int(10) unsigned NOT NULL)
    ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `member_cache` (`id` int(10) unsigned NOT NULL)
    ENGINE=MyISAM DEFAULT CHARSET=utf8;
CREATE TABLE `section` (`id` int(10) unsigned NOT NULL, `last_member` int(10) unsigned NOT NULL DEFAULT '0')
    ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `topic` (`id` int(10) unsigned NOT NULL, `section` int(10) unsigned NOT NULL, `last_time` int(10) unsigned NOT NULL)
    ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

ALTER TABLE `member` ADD PRIMARY KEY (`id`);
ALTER TABLE `member_cache` ADD PRIMARY KEY (`id`);
ALTER TABLE `section`ADD PRIMARY KEY (`id`);
ALTER TABLE `topic` ADD PRIMARY KEY (`id`), ADD KEY `section__last_time` (`section`,`last_time`), ADD KEY `last_time` (`last_time`);

现在这里是执行计划,通过“EXPLAIN ”获得:

+----+-------------+--------------+--------+------------------------------+--------------------+---------+-------------------------------+------+---------------------------------+
| id | select_type | table        | type   | possible_keys                | key                | key_len | ref                           | rows | Extra                           |
+----+-------------+--------------+--------+------------------------------+--------------------+---------+-------------------------------+------+---------------------------------+
|  1 | SIMPLE      | section      | ALL    | PRIMARY                      | NULL               | NULL    | NULL                          | 2188 | Using temporary; Using filesort |
|  1 | SIMPLE      | member       | eq_ref | PRIMARY                      | PRIMARY            | 4       | temporary.section.last_member |    1 | Using index                     |
|  1 | SIMPLE      | member_cache | eq_ref | PRIMARY                      | PRIMARY            | 4       | temporary.section.last_member |    1 | Using index                     |
|  1 | SIMPLE      | topic        | ref    | section__last_time,last_time | section__last_time | 4       | temporary.section.id          |  106 | Using index condition           |
+----+-------------+--------------+--------+------------------------------+--------------------+---------+-------------------------------+------+---------------------------------+

如您所见,它首先扫描整个“节”表,使用临时表并导致糟糕的性能。我真的不明白为什么会发生这种情况,因为“topic.last_time”(用于 WHERE 子句)和“section.id”(用于第一个 INNER JOIN)上都存在一个索引。我也做了几次测试,结果很不稳定:

如果我在“topic”表上添加明确的“FORCE INDEX”语句,那么显然 MySQL 正确使用了索引“topic.last_time”和“section.id”,最终得到更快的结果,如下所示(但我无法从我正在使用的 SQL 查询生成库中生成这种 MySQL 特定扩展) 如果我用“LEFT JOIN”替换第一个“INNER JOIN”(针对“section”表),我会得到相同的结果,可能是因为它阻止了 MySQL 反转 JOIN 的操作数(但 LEFT JOIN 不是我想要的)想表达); 更奇怪的是:如果我从表“topic”中删除索引“section__last_time”,那么我也会得到相同的结果。我真的不明白为什么这个索引对执行计划有影响? (无论如何我需要它来进行其他查询,所以我无法删除它)

这是我应用上述三个更改中的任何一个后的执行计划:

+----+-------------+--------------+--------+---------------+-----------+---------+-------------------------------+------+-------------+
| id | select_type | table        | type   | possible_keys | key       | key_len | ref                           | rows | Extra       |
+----+-------------+--------------+--------+---------------+-----------+---------+-------------------------------+------+-------------+
|  1 | SIMPLE      | topic        | index  | last_time     | last_time | 4       | NULL                          |    1 | Using where |
|  1 | SIMPLE      | section      | eq_ref | PRIMARY       | PRIMARY   | 4       | temporary.topic.section       |    1 | NULL        |
|  1 | SIMPLE      | member       | eq_ref | PRIMARY       | PRIMARY   | 4       | temporary.section.last_member |    1 | Using index |
|  1 | SIMPLE      | member_cache | eq_ref | PRIMARY       | PRIMARY   | 4       | temporary.section.last_member |    1 | Using index |
+----+-------------+--------------+--------+---------------+-----------+---------+-------------------------------+------+-------------+

我还尝试“优化表”所有表或切换到 InnoDB 引擎,但这些都没有改变任何东西。问题在 MySQL 版本 5.5.35 和 5.6.15 上重现;我还上传了测试环境here的快照,上面的查询很容易复现。

你知道什么可以解释这个执行计划吗?

【问题讨论】:

【参考方案1】:

考虑在 section.last_member、section.id 上添加索引。

ALTER TABLE section ADD KEY(last_member, id);

如果它们是 innodb,你可以省略 ID,因为它已经是一个 PK。

【讨论】:

确实有效,谢谢!你知道为什么在没有这个额外索引的情况下选择了全表扫描的执行计划吗?我真的不明白这种低效的解决方案如何被 MySQL 视为最佳方法。 (我想知道如何使您的解决方案适应我的真实场景) 旧版本的 mysql 有非常简单的计划优化器。如果没有二级索引,节表将与主题 OK 连接,但成员变为非索引连接,从而导致全表扫描(我认为)。 嗯,没有任何额外的索引,它可以通过topic.last_time(索引)然后section.id(PK),member.id(PK)和member_cache.id(PK)来解决;如果我明确地强制使用这些索引之一,而不必更新架构,这实际上会发生。我仍然不明白,但无论如何感谢您的帮助:)

以上是关于具有多个连接的 MySQL 查询的低效执行计划的主要内容,如果未能解决你的问题,请参考以下文章

MySql 定位和分析执行效率的方法

具有多个连接的 MySQL 查询执行时间过长

具有多个查询的NodeJS mysql连接池

Mysql学会查看sql的执行计划

对两个 MySQL 查询执行左外连接?

SQL执行与优化