使用 ORDERBY 时的 MySQL 慢 JOIN 查询
Posted
技术标签:
【中文标题】使用 ORDERBY 时的 MySQL 慢 JOIN 查询【英文标题】:MySQL Slow JOIN query when using ORDERBY 【发布时间】:2019-08-02 19:10:21 【问题描述】:我对这个查询有疑问:
SELECT a.*
FROM smartressort AS s
JOIN smartressort_to_ressort AS str
ON s.id = str.smartressort_id
JOIN article_to_ressort AS atr
ON str.ressort_id = atr.ressort_id
JOIN article AS a FORCE INDEX (source_created)
ON atr.article_id = a.id
WHERE
s.id = 1
ORDER BY
a.created_at DESC
LIMIT 25;
这个真的很慢,有时需要14秒。
解释一下:
1 SIMPLE s const PRIMARY PRIMARY 4 const 1 Using index; Using temporary; Using filesort
1 SIMPLE str ref PRIMARY,ressort_id PRIMARY 4 const 1 Using index
1 SIMPLE atr ref PRIMARY,article_id PRIMARY 4 com.nps.lvz-prod.str.ressort_id 1262 Using index
1 SIMPLE a ALL NULL NULL NULL NULL 146677 Using where; Using join buffer (flat, BNL join)
所以最后一个“all”类型真的很糟糕。 但我已经尝试强制使用索引,但没有运气。
文章表如下所示:
CREATE TABLE `article` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`node_id` varchar(255) NOT NULL DEFAULT '',
`object_id` varchar(255) DEFAULT NULL,
`headline_1` varchar(255) NOT NULL DEFAULT '',
`created_at` datetime(3) NOT NULL,
`updated_at` datetime(3) NOT NULL,
`teaser_text` longtext NOT NULL,
`content_text` longtext NOT NULL,
PRIMARY KEY (`id`),
KEY `article_nodeid` (`node_id`),
KEY `article_objectid` (`object_id`),
KEY `source_created` (`created_at`)
) ENGINE=InnoDB AUTO_INCREMENT=161116 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
当我删除 FORCE INDEX 时,解释变得更好,但查询仍然很慢。
说明无强制索引:
1 SIMPLE s const PRIMARY PRIMARY 4 const 1 Using index; Using temporary; Using filesort
1 SIMPLE str ref PRIMARY,ressort_id PRIMARY 4 const 1 Using index
1 SIMPLE atr ref PRIMARY,article_id PRIMARY 4 com.nps.lvz-prod.str.ressort_id 1262 Using index
1 SIMPLE a eq_ref PRIMARY PRIMARY 4 com.nps.lvz-prod.atr.article_id 1
对于另一个 smartressort id(3),它看起来像这样:
1 SIMPLE s const PRIMARY PRIMARY 4 const 1 Using index; Using temporary; Using filesort
1 SIMPLE str ref PRIMARY,ressort_id PRIMARY 4 const 13 Using index
1 SIMPLE atr ref PRIMARY,article_id PRIMARY 4 com.nps.lvz-prod.str.ressort_id 1262 Using index
1 SIMPLE a eq_ref PRIMARY PRIMARY 4 com.nps.lvz-prod.atr.article_id 1
这里我们有 13 个度假村对应一个 Smartressort。 行数:1x1x13x1262x1 = 16.406
1) 我可以做些什么来加快这个请求?
2) source_created
索引有什么问题?
【问题讨论】:
您可能遗漏了最关键的部分,即source_created
索引的定义。请包括在内。
@Cid 我认为这通常不会产生太大影响。
强制source_created
索引是没用的,你没有在join中使用索引列,只有在之后。
@Cid WHERE
子句应用于在连接发生之前,而不是之后:-)
问题解决了吗?解决方案是什么?
【参考方案1】:
您的查询中的SELECT *
很难看,这通常会成为索引杀手。它可以排除索引的使用,因为您定义的大多数索引不会涵盖SELECT *
要求的每一列。这个答案的方法是索引查询中的所有其他表,因此这会激励 mysql 只对 article
表进行一次扫描。
CREATE INDEX idx1 ON article_to_ressort (article_id, ressort_id);
CREATE INDEX idx2 ON smartressort_to_ressort (ressort_id, smartressort_id);
这两个索引应该可以加快加入过程。请注意,我没有为smartressort
表定义索引,假设它的id
列已经是主键。我可能会从article
表开始编写您的查询,然后向外加入,但这并不重要。
此外,强制索引通常不是一个好主意或没有必要。优化器通常可以确定何时最好使用索引。
【讨论】:
请注意,SELECT a.*
在这种特殊情况下并没有那么糟糕,OP 需要所有数据,因此无论如何都应该获取该行 - 通常你是对的,有些情况下所有数据都可以从索引提供 - 类似于您的新索引。
@gaborsch 那么 MySQL 必须扫描一些东西。如果它可以找到一种有效的方法来扫描article
表并处理连接,那么它应该这样做。
同意。也许article
上的索引,包含id
和created_at
将有助于ORDER BY
,避免访问所有行进行排序?
问题是,你描述的索引不会覆盖SELECT a.*
需要的所有列,所以不清楚MySQL是否会选择使用它。【参考方案2】:
SELECT many columns FROM tables ORDER BY something LIMIT few
是一个臭名昭著的性能反模式;它必须检索和排序一大堆行和列,只是为了丢弃结果集中除了几行之外的所有行。
诀窍是找出您在结果集中需要哪些article.id
值,然后仅检索这些值。这称为延迟连接。
这应该会为您提供一组id
值。可能不需要加入smartressort
表,因为smartressort_to_ressort
包含您需要的id
值。
SELECT a.id
FROM article a
JOIN article_to_ressort atr ON a.id = atr.article_id
JOIN smartressort_to_ressort str ON atr.ressort_id = str.ressort_id
WHERE str.smartressort_id = 1
ORDER BY a.created_at DESC
LIMIT 25
然后您可以将其用作子查询来获取所需的行。
SELECT a.*
FROM article a
WHERE a.id IN (
SELECT a.id
FROM article a
JOIN article_to_ressort atr ON a.id = atr.article_id
JOIN smartressort_to_ressort str ON atr.ressort_id = str.ressort_id
WHERE str.smartressort_id = 1
ORDER BY a.created_at DESC
LIMIT 25
)
ORDER BY a.created_at DESC
第二个 ORDER BY 确保文章中的行处于可预测的顺序。那么,您的索引优化工作只需要应用于子查询。
【讨论】:
@Macx 理想情况下,优化器应该首先访问a.created_at
表,因为您在它上面排序,所以基本上 O.Jones 在这里迫使 MySQL 优化器进入更好的计划.. MySQL 8.0 会自动优化您的查询并避免“使用临时”并且运行良好
我不能在子查询中使用限制。但这有效并且速度更快:SELECT ar.* FROM article ar WHERE ar.id IN ( SELECT atr.article_id from article_to_ressort atr JOIN smartressort_to_ressort str ON atr.ressort_id = str.ressort_id WHERE str.smartressort_id = 2 ) ORDER BY ar.created_at DESC LIMIT 25;
我还要加:CREATE INDEX idx3 ON article ( id , created_at )
【参考方案3】:
除了@TimBiegelsen 的出色回答,我建议修改您的source_created
索引:
...
KEY `source_created` (`id`, `created_at`)
好处是 MySQL 可以使用它进行排序,并且不需要获取所有 16406 行。它可能有帮助,也可能没有帮助,但值得一试(也许有明确的声明来使用它)
【讨论】:
【参考方案4】:首先:您可以从查询中删除smartressort
表,因为它不会向其中添加任何内容。
以下是您重写的查询。我们想要 smart ressort #1 的所有度假村,然后是这些度假村的所有文章。其中我们展示了最新的 25 个。
SELECT *
FROM article
WHERE id IN
(
SELECT article_id
FROM article_to_ressort
WHERE ressort_id IN
(
SELECT ressort_id
FROM smartressort_to_ressort
WHERE smartressort_id = 1
)
)
ORDER BY created_at DESC
LIMIT 25;
现在需要哪些索引来帮助 DBMS 解决这个问题?从内表 (smartressort_to_ressort
) 开始。我们使用给定的smartressort_id
访问所有记录,并且我们希望获得关联的ressort_id
。所以索引应该按这个顺序包含这两列。 article_to_ressort
及其 ressort_id
和 article_id
相同。最后,我们要根据找到的文章 ID 选择文章,并按照它们的created_at
排序。
CREATE INDEX idx1 ON smartressort_to_ressort (smartressort_id, ressort_id);
CREATE INDEX idx2 ON article_to_ressort (ressort_id, article_id);
CREATE INDEX idx3 ON article (id, created_at);
无论如何,这些索引只是对 DBMS 的一个提议。它可能会决定反对他们。对于article
表上的索引尤其如此。 DBMS 期望为一个smartressort_id
访问多少行,即IN
子句中可能有多少行?如果 DBMS 认为这可能占所有文章 ID 的 10% 左右,那么它可能已经决定按顺序读取表,而不是在索引中混淆这么多行。
【讨论】:
这要快得多(~35ms)。解释查询显示只使用了idx3
。行显示 1262 x 1 x 1 x 1262。【参考方案5】:
所以对我来说解决方案是这样的:
SELECT a.*
FROM article as a USE INDEX (source_created)
where a.id in (
SELECT atr.article_id
from smartressort_to_ressort str
JOIN article_to_ressort atr ON atr.ressort_id = str.ressort_id
WHERE str.smartressort_id = 1
)
ORDER BY a.created_at DESC
LIMIT 25;
这只需要大约 35 毫秒。 解释看起来像这样:
1 PRIMARY a index NULL source_created 7 NULL 1
1 PRIMARY <subquery2> eq_ref distinct_key distinct_key 4 func 1
2 MATERIALIZED str ref PRIMARY,ressort_id,idx1 PRIMARY 4 const 1 Using index
2 MATERIALIZED atr ref PRIMARY,article_id,idx2 PRIMARY 4 com.nps.lvz-prod.str.ressort_id 1262 Using index
即便如此,这个查询 Explain 对我来说看起来更好,但我不知道具体原因:
explain SELECT a.*, NOW()
FROM article as a USE INDEX (source_created)
where a.id in (SELECT atr.article_id
FROM smartressort AS s
JOIN smartressort_to_ressort AS str
ON s.id = str.smartressort_id
JOIN article_to_ressort AS atr
ON str.ressort_id = atr.ressort_id
WHERE s.id = 1
)
ORDER BY a.created_at DESC
LIMIT 25;
输出:
1 PRIMARY s const PRIMARY PRIMARY 4 const 1 Using index
1 PRIMARY a index NULL source_created 7 NULL 25
1 PRIMARY str ref PRIMARY,ressort_id,idx1 PRIMARY 4 const 1 Using index
1 PRIMARY atr eq_ref PRIMARY,article_id,idx2 PRIMARY 8 com.nps.lvz-prod.str.ressort_id,com.nps.lvz-prod.a.id 1 Using index; FirstMatch(a)
【讨论】:
既然上面写着MATERIALIZED
,您可能正在使用不同的版本进行测试。以上是关于使用 ORDERBY 时的 MySQL 慢 JOIN 查询的主要内容,如果未能解决你的问题,请参考以下文章