为什么子查询联接比直接联接要快得多

时间:2019-08-15 22:45:50

标签: mysql join subquery query-performance

我有2个表(页面和注释),每个表约 13万行。

我想列出没有任何评论的页面(外键是comments.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. 如何编写快速查询以获取所需的信息? (在第5.6、5.7版中)

2 个答案:

答案 0 :(得分:3)

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

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

您可以通过以下两种方法来缩短5.7中的查询时间:

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

在MySQL 8.0中,您还可以使用优化程序提示来避免合并。就您而言,就像

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

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

答案 1 :(得分:2)

查询1具有“爆炸内爆”综合症。首先,它执行JOIN;这使行数激增。然后它执行GROUP BY缩小。

每页的评论数等将对您的查询产生影响。

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)