使用 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 上的索引,包含idcreated_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_idarticle_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 查询的主要内容,如果未能解决你的问题,请参考以下文章

使用 order by 时,Mysql 查询运行非常慢

MySQL数据库order by 奇慢无比

使用 ORDER BY id 时 MySQL 查询慢

MySQL 查询慢,按限制按顺序分组

mysql-关键字

mysql查询ORDERBY效率低