为什么子查询连接比直接连接快很多

Why is subquery join much faster than direct join

我有 2 tables(页面和评论),每个大约 130 000 行。

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

如果我执行正常的左外连接,需要惊人的超过 750 秒 到 运行。 (130k^2 = 17B)。而如果我执行相同的连接,但对 table 使用子查询,则只需要 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. 加入第一个 table 作为子查询,时间太多

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。加入第二个 table 作为子查询,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 版中,所有 上述查询需要"too much time" 来执行。

在MySql5.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 中)

查询 1 有 "explode-implode" 综合症。首先它执行 JOIN;这会增加行数。然后它做了一个GROUP BY收缩。

还有

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

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

查询 2 应该没有您发现的那么快 -- 它需要构建两个临时表("derived" 表),索引其中一个,然后执行外部查询。 (可能足够新的 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)

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

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

在 5.7 中,您有两个选项可以缩短查询时间:

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

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

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

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