为什么子查询连接比直接连接快很多
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
问题
为什么会这样? MySql 在幕后做了什么?
这是否仅发生在 MySql 或任何其他 Sql 中?
如何编写快速查询来获取所需内容? (在 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 中,您有两个选项可以缩短查询时间:
- 您可以为评论创建索引(page_id)
- 您可以通过将子查询重写为无法合并的查询来防止合并子查询。具有聚合、LIMIT 或 UNION 的子查询将不会被合并(有关详细信息,请参阅 the blog post)。一种方法是在子查询中添加 LIMIT 子句。为了不从结果中删除任何行,限制必须大于 table.
中的行数
在MySQL 8.0 中,您还可以使用优化器提示来避免合并。在您的情况下,这类似于
SELECT /*+ NO_MERGE(c) */ ... FROM
有关如何使用此类提示的示例,请参阅 this presentation 的幻灯片 34-37。
我有 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
问题
为什么会这样? MySql 在幕后做了什么?
这是否仅发生在 MySql 或任何其他 Sql 中?
如何编写快速查询来获取所需内容? (在 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 中,您有两个选项可以缩短查询时间:
- 您可以为评论创建索引(page_id)
- 您可以通过将子查询重写为无法合并的查询来防止合并子查询。具有聚合、LIMIT 或 UNION 的子查询将不会被合并(有关详细信息,请参阅 the blog post)。一种方法是在子查询中添加 LIMIT 子句。为了不从结果中删除任何行,限制必须大于 table. 中的行数
在MySQL 8.0 中,您还可以使用优化器提示来避免合并。在您的情况下,这类似于
SELECT /*+ NO_MERGE(c) */ ... FROM
有关如何使用此类提示的示例,请参阅 this presentation 的幻灯片 34-37。