【问题标题】:Why is subquery join much faster than direct join为什么子查询联接比直接联接快得多
【发布时间】:2019-12-22 07:55:48
【问题描述】:

我有 2 个表(页面和 cmets),每个表大约 130 000 行。

我想列出没有任何 cmets 的页面(外键是 cmets.page_id)

如果我执行正常的左外连接,它会花费惊人的超过 750 秒来运行。 (130k^2 = 17B)。而如果我执行相同的连接,但对表使用子查询,则只需 1 秒

服务器版本:5.6.44-log - MySQL 社区服务器 (GPL):

查询 1. 普通连接,750+ 秒

SELECT p.id
FROM `pages` AS p
LEFT JOIN  `comments` AS c
    ON p.id = c.page_id
WHERE c.page_id IS NULL
GROUP BY 1

查询 2. 将第一个表作为子查询加入,时间过长

SELECT p.id
FROM (
    SELECT id FROM `pages`
) AS p
LEFT JOIN  `comments` AS c
    ON p.id = c.page_id
WHERE c.page_id IS NULL
GROUP BY 1

查询 3. 以子查询形式加入第二个表,1.6 秒

SELECT p.id
FROM `pages` AS p
LEFT JOIN (
   SELECT * FROM `comments`
) AS c
    ON p.id = c.page_id
WHERE c.page_id IS NULL
GROUP BY 1

查询 4。加入 2 个子查询,1 秒

SELECT p.id
FROM (
    SELECT id FROM `pages`
) AS p
LEFT JOIN (
   SELECT * FROM `comments`
) AS c
    ON p.id = c.page_id
WHERE c.page_id IS NULL
GROUP BY 1

查询5.加入2个子查询,只选择1列,0.2秒

SELECT p.id
FROM (
    SELECT id FROM `pages`
) AS p
LEFT JOIN (
   SELECT page_id FROM `comments`
) AS c
    ON p.id = c.page_id
WHERE c.page_id IS NULL
GROUP BY 1

查询 6. 时间过长

SELECT p.id
    FROM `pages` AS p
    WHERE NOT EXISTS( SELECT page_id FROM `comments`
                        WHERE page_id = p.id );;

现在,在 MySql 5.7 版中,所有上述查询都需要 “太多时间” 来执行。

在MySql 5.7中,查询1和4解释相同:

id  select_type  table    partitions     type    possible_keys  key         key_len  ref    rows        filtered    Extra  
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1    SIMPLE         p       NULL        index       PRIMARY    PRIMARY      4       NULL    147626      100.00      Using index; Using temporary; Using filesort  
1    SIMPLE         c       NULL        ALL         NULL        NULL        NULL    NULL    147790      10.00       Using where; Not exists; Using join buffer (Block Nested Loop)

在 MySql 5.6 中,不幸的是我现在无法得到查询 1 的解释(花费太多时间),但查询 4 ​​如下:

id  select_type table       type    possible_keys   key     key_len     ref     rows        Extra   
---------------------------------------------------------------------------------------------------------------------------
1   PRIMARY     <derived2>  ALL     NULL            NULL        NULL    NULL    147626      Using temporary; Using filesort 
1   PRIMARY     <derived3>  ref     <auto_key0>     <auto_key0>  4      p.id    10          Using where; Not exists    
3   DERIVED     comments    ALL     NULL            NULL        NULL    NULL    147790      NULL   
2   DERIVED     pages       index   NULL            PRIMARY     4       NULL    147626      Using index

表格:

CREATE TABLE `pages` (
 `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
 `identifier` varchar(250) NOT NULL DEFAULT '',
 `reference` varchar(250) NOT NULL DEFAULT '',
 `url` varchar(1000) NOT NULL DEFAULT '',
 `moderate` varchar(250) NOT NULL DEFAULT 'default',
 `is_form_enabled` tinyint(1) unsigned NOT NULL DEFAULT '1',
 `date_modified` datetime NOT NULL,
 `date_added` datetime NOT NULL,
 PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=147627 DEFAULT CHARSET=utf8


CREATE TABLE `comments` (
 `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
 `user_id` int(10) unsigned NOT NULL DEFAULT '0',
 `page_id` int(10) unsigned NOT NULL DEFAULT '0',
 `website` varchar(250) NOT NULL DEFAULT '',
 `town` varchar(250) NOT NULL DEFAULT '',
 `state_id` int(10) NOT NULL DEFAULT '0',
 `country_id` int(10) NOT NULL DEFAULT '0',
 `rating` tinyint(1) unsigned NOT NULL DEFAULT '0',
 `reply_to` int(10) unsigned NOT NULL DEFAULT '0',
 `comment` text NOT NULL,
 `reply` text NOT NULL,
 `ip_address` varchar(250) NOT NULL DEFAULT '',
 `is_approved` tinyint(1) unsigned NOT NULL DEFAULT '1',
 `notes` text NOT NULL,
 `is_admin` tinyint(1) unsigned NOT NULL DEFAULT '0',
 `is_sent` tinyint(1) unsigned NOT NULL DEFAULT '0',
 `sent_to` int(10) unsigned NOT NULL DEFAULT '0',
 `likes` int(10) unsigned NOT NULL DEFAULT '0',
 `dislikes` int(10) unsigned NOT NULL DEFAULT '0',
 `reports` int(10) unsigned NOT NULL DEFAULT '0',
 `is_sticky` tinyint(1) unsigned NOT NULL DEFAULT '0',
 `is_locked` tinyint(1) unsigned NOT NULL DEFAULT '0',
 `is_verified` tinyint(1) unsigned NOT NULL DEFAULT '0',
 `date_modified` datetime NOT NULL,
 `date_added` datetime NOT NULL,
 PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=147879 DEFAULT CHARSET=utf8

问题

  1. 为什么会这样? MySql 在幕后做了什么?

  2. 这种情况只发生在 MySql 中,还是任何其他 Sql 中也发生?

  3. 如何编写快速查询来获得我需要的内容? (在 v 5.6、5.7 中)

【问题讨论】:

  • 您是否尝试在慢查询中删除GROUP BY
  • @DonJoe 是的,没有区别
  • GROUP BY 在这种情况下毫无意义。
  • 请为两个表提供SHOW CREATE TABLE,为两个选择提供EXPLAIN SELECT ...,RAM的大小,MySQL的版本号。
  • “为什么会发生这种情况?MySql 在幕后做了什么?” 广泛回答 “这种情况是否只发生在 MySql 或任何其他 Sql 中?好吧?” 宽泛地回答,因为其他 RDMS 中的 SQL 优化器完全不同...... “我怎样才能编写一个快速查询来获得我需要的东西?(在 v 5.6、5.7 中)” 如果您同时索引comments(page_id),则很可能您可以使用@RickJames 查询...

标签: mysql join subquery query-performance


【解决方案1】:

您的长时间运行查询的问题是您在 cmets 表的 page_id 列上缺少索引。因此,对于 pages 表中的每一行,您需要检查 cmets 表的所有行。由于您使用的是 LEFT JOIN,因此这是唯一可能的连接顺序。在 5.6 中发生的情况是,当您在 FROM 子句(又名派生表)中使用子查询时,MySQL 将在用于派生表结果的临时表上创建一个索引(EXPLAIN 输出中的 auto_key0)。只选择一列时速度更快的原因是临时表会更小。

在 MySQL 5.7 中,如果可能,此类派生表将自动合并到主查询中。这样做是为了避免额外的临时表。但是,这意味着您不再有用于连接的索引。 (详情请参阅this blog post。)

在 5.7 中,您有两种选择来改进查询时间:

  1. 您可以在 cmets(page_id) 上创建索引
  2. 您可以通过将子查询重写为无法合并的查询来防止子查询被合并。不会合并具有聚合、LIMIT 或 UNION 的子查询(有关详细信息,请参阅the blog post)。一种方法是在子查询中添加一个 LIMIT 子句。为了不从结果中删除任何行,限制必须大于表中的行数。

在 MySQL 8.0 中,您还可以使用优化器提示来避免合并。在你的情况下,这就像

SELECT /*+ NO_MERGE(c) */ ... FROM

有关如何使用此类提示的示例,请参阅this presentation 的幻灯片 34-37。

【讨论】:

  • 优化是否也适用于 MyISAM?
  • 是的,我不认为这取决于存储引擎。
【解决方案2】:

查询 1 具有“explode-implode”综合症。首先它执行JOIN;这会爆炸行数。然后它会执行GROUP BY 来收缩。

还有

每页的 cmets 数量等都会对您的查询产生影响。

SELECT * 获取所有列,而它只需要知道LEFT JOIN 是否成功。 (您观察到了。)此外,您没有保留任何列,因为您正在寻找 missing 行。

查询 2 不应该像您发现的那样快——它需要构建两个临时表(“派生”表),索引其中一个,然后执行外部查询。 (可能一个足够新的 MySQL 版本会缩短其中的一些工作;旧版本因效率低下而臭名昭著。)

查询 3:

试试

SELECT p.id
    FROM `pages` AS p
    WHERE NOT EXISTS( SELECT 1 FROM `comments`
                        WHERE page_id = p.id );

还有:

  • 使用 InnoDB,而不是 MyISAM。
  • comments需要INDEX(page_id)

【讨论】:

  • 为了使 NOT EXISTS 查询高效,我认为您需要 MySQL 8.0.17 的反连接支持。否则,它将遭受 cmets(page_id) 上缺少索引的问题。
猜你喜欢
  • 1970-01-01
  • 2017-06-17
  • 1970-01-01
  • 1970-01-01
  • 2018-01-04
  • 2011-12-11
  • 1970-01-01
  • 2012-06-20
  • 1970-01-01
相关资源
最近更新 更多