[英]Join vs. sub-query
我是一个老派的 MySQL 用户,并且一直更喜欢JOIN
不是子查询。 但是现在每个人都使用子查询,我讨厌它; 我不知道为什么。
我缺乏理论知识来判断是否有任何差异。 子查询是否与JOIN
一样好,因此没有什么可担心的?
子查询是解决“从 A 获取事实,以来自 B 的事实为条件”形式的问题的逻辑正确方法。 在这种情况下,将 B 放在子查询中比执行连接更合乎逻辑。 从实际意义上讲,它也更安全,因为您不必因与 B 的多次匹配而对从 A 获取重复事实保持谨慎。
然而,实际上,答案通常归结为性能。 一些优化器在给定连接与子查询时会吃柠檬,而另一些优化器则会以另一种方式吸食柠檬,这是特定于优化器、特定于 DBMS 版本和特定于查询的。
从历史上看,显式连接通常会获胜,因此连接的既定智慧更好,但优化器一直在变得更好,因此我更喜欢首先以逻辑连贯的方式编写查询,然后在性能限制允许的情况下进行重组。
在大多数情况下JOIN
比子查询更快,并且子查询更快是非常罕见的。
在JOIN
RDBMS 可以创建一个更适合您的查询的执行计划,并且可以预测应该加载哪些数据进行处理并节省时间,这与子查询不同,它将运行所有查询并加载所有数据来执行加工。
子查询的好处是它们比JOIN
更具可读性:这就是大多数 SQL 新手更喜欢它们的原因; 这是最简单的方法; 但是在性能方面,JOINS 在大多数情况下更好,即使它们也不难阅读。
摘自 MySQL 手册( 13.2.10.11 Rewriting Subqueries as Joins ):
LEFT [OUTER] JOIN 可以比等效的子查询更快,因为服务器可能能够更好地优化它——这一事实不仅仅针对 MySQL 服务器。
所以子查询可能比LEFT [OUTER] JOIN
慢,但在我看来,它们的优势是可读性稍高。
使用 EXPLAIN 查看您的数据库如何对您的数据执行查询。 这个答案中有一个巨大的“取决于”......
当 PostgreSQL 认为一个比另一个更快时,它可以将子查询重写为连接或连接到子查询。 这一切都取决于数据、索引、相关性、数据量、查询等。
在 2010 年,我会加入这个问题的作者,并且会强烈投票支持JOIN
,但是有了更多的经验(尤其是在 MySQL 中),我可以声明:是的,子查询可以更好。 我在这里阅读了多个答案; 一些声明的子查询更快,但缺乏很好的解释。 我希望我能提供一个(非常)迟到的答案:
首先说最重要的:子查询有不同的形式
第二个重要声明:尺寸很重要
如果您使用子查询,您应该了解DB-Server 如何执行子查询。 特别是如果子查询被评估一次或每一行! 另一方面,现代 DB-Server 能够进行很多优化。 在某些情况下,子查询有助于优化查询,但更新版本的 DB-Server 可能会使优化过时。
SELECT moo, (SELECT roger FROM wilco WHERE moo = me) AS bar FROM foo
请注意,对来自foo
每个结果行执行子查询。
如果可能,请避免这种情况; 它可能会大大减慢您对大型数据集的查询速度。 但是,如果子查询没有对foo
引用,它可以由数据库服务器优化为静态内容,并且只能评估一次。
SELECT moo FROM foo WHERE bar = (SELECT roger FROM wilco WHERE moo = me)
如果幸运的话,DB 会在内部将其优化为JOIN
。 如果没有,您的查询在大型数据集上将变得非常非常缓慢,因为它将为foo
每一行执行子查询,而不仅仅是像 select-type 中的结果。
SELECT moo, bar
FROM foo
LEFT JOIN (
SELECT MIN(bar), me FROM wilco GROUP BY me
) ON moo = me
这很有趣。 我们将JOIN
与子查询结合起来。 在这里,我们得到了子查询的真正优势。 想象一个数据集在wilco
有数百万行但只有几个不同的me
。 我们现在有一个较小的临时表要加入,而不是加入一个巨大的表。 这可能会导致查询速度更快,具体取决于数据库大小。 您可以使用CREATE TEMPORARY TABLE ...
和INSERT INTO ... SELECT ...
获得相同的效果,这可能会为非常复杂的查询提供更好的可读性(但可以在可重复的读取隔离级别锁定数据集)。
SELECT VARIANCE(moo)
FROM (
SELECT moo, CONCAT(roger, wilco) AS bar
FROM foo
HAVING bar LIKE 'SpaceQ%'
) AS temp_foo
GROUP BY moo
您可以在多个级别嵌套子查询。 如果您必须对结果进行分组或更改,这可以帮助处理庞大的数据集。 通常 DB-Server 会为此创建一个临时表,但有时您不需要对整个表进行一些操作,只需要对结果集进行一些操作。 这可能会提供更好的性能,具体取决于表的大小。
子查询不能替代JOIN
,您不应该像这样使用它们(尽管可能)。 以我的拙见,子查询的正确使用是用作CREATE TEMPORARY TABLE ...
的快速替换。 一个好的子查询以一种在JOIN
的ON
语句中无法完成的方式减少数据集。 如果子查询具有关键字GROUP BY
或DISTINCT
,并且最好不在选择字段或 where 语句中,那么它可能会大大提高性能。
首先,要先比较两者,您应该将查询与子查询区分开来:
对于第一类查询,一个好的 RDBMS 会将连接和子查询视为等效,并将生成相同的查询计划。
如今,即使是 mysql 也是如此。
尽管如此,有时它不会,但这并不意味着连接总是会赢 - 我在 mysql 中使用子查询时遇到了一些情况来提高性能。 (例如,如果有什么阻止 mysql 规划器正确估计成本,并且如果规划器没有看到 join-variant 和 subquery-variant 相同,那么子查询可以通过强制某个路径来优于连接)。
结论是,如果您想确定哪一个会表现得更好,您应该针对连接和子查询变体测试您的查询。
对于第二类,比较没有意义,因为这些查询不能使用连接重写,在这些情况下,子查询是完成所需任务的自然方式,您不应该歧视它们。
我认为引用的答案中没有强调的是重复问题和特定(使用)案例可能产生的有问题的结果。
(虽然马塞洛·坎托斯确实提到过)
我将引用斯坦福大学关于 SQL 的 Lagunita 课程中的示例。
+------+--------+------+--------+
| sID | sName | GPA | sizeHS |
+------+--------+------+--------+
| 123 | Amy | 3.9 | 1000 |
| 234 | Bob | 3.6 | 1500 |
| 345 | Craig | 3.5 | 500 |
| 456 | Doris | 3.9 | 1000 |
| 567 | Edward | 2.9 | 2000 |
| 678 | Fay | 3.8 | 200 |
| 789 | Gary | 3.4 | 800 |
| 987 | Helen | 3.7 | 800 |
| 876 | Irene | 3.9 | 400 |
| 765 | Jay | 2.9 | 1500 |
| 654 | Amy | 3.9 | 1000 |
| 543 | Craig | 3.4 | 2000 |
+------+--------+------+--------+
(针对特定大学和专业的申请)
+------+----------+----------------+----------+
| sID | cName | major | decision |
+------+----------+----------------+----------+
| 123 | Stanford | CS | Y |
| 123 | Stanford | EE | N |
| 123 | Berkeley | CS | Y |
| 123 | Cornell | EE | Y |
| 234 | Berkeley | biology | N |
| 345 | MIT | bioengineering | Y |
| 345 | Cornell | bioengineering | N |
| 345 | Cornell | CS | Y |
| 345 | Cornell | EE | N |
| 678 | Stanford | history | Y |
| 987 | Stanford | CS | Y |
| 987 | Berkeley | CS | Y |
| 876 | Stanford | CS | N |
| 876 | MIT | biology | Y |
| 876 | MIT | marine biology | N |
| 765 | Stanford | history | Y |
| 765 | Cornell | history | N |
| 765 | Cornell | psychology | Y |
| 543 | MIT | CS | N |
+------+----------+----------------+----------+
让我们试着找出申请CS
专业的学生的GPA分数(不分大学)
使用子查询:
select GPA from Student where sID in (select sID from Apply where major = 'CS');
+------+
| GPA |
+------+
| 3.9 |
| 3.5 |
| 3.7 |
| 3.9 |
| 3.4 |
+------+
此结果集的平均值为:
select avg(GPA) from Student where sID in (select sID from Apply where major = 'CS');
+--------------------+
| avg(GPA) |
+--------------------+
| 3.6800000000000006 |
+--------------------+
使用连接:
select GPA from Student, Apply where Student.sID = Apply.sID and Apply.major = 'CS';
+------+
| GPA |
+------+
| 3.9 |
| 3.9 |
| 3.5 |
| 3.7 |
| 3.7 |
| 3.9 |
| 3.4 |
+------+
此结果集的平均值:
select avg(GPA) from Student, Apply where Student.sID = Apply.sID and Apply.major = 'CS';
+-------------------+
| avg(GPA) |
+-------------------+
| 3.714285714285714 |
+-------------------+
很明显,第二次尝试在我们的用例中产生了误导性的结果,因为它计算平均值的重复次数。 同样明显的是,将distinct
与基于连接的语句一起使用不会消除该问题,因为它会错误地保留3.9
分数的三分之一。 正确的情况是考虑到3.9
分的两 (2)次出现,因为我们实际上有两 (2)名学生的分数符合我们的查询标准。
似乎在某些情况下,除了任何性能问题之外,子查询是最安全的方法。
许多包含子查询的 Transact-SQL 语句也可以表述为连接。 其他问题只能通过子查询提出。 在 Transact-SQL 中,包含子查询的语句与不包含子查询的语义等效版本之间通常没有性能差异。 但是,在某些必须检查存在性的情况下,连接会产生更好的性能。 否则,必须为外部查询的每个结果处理嵌套查询,以确保消除重复项。 在这种情况下,连接方法会产生更好的结果。
所以如果你需要类似的东西
select * from t1 where exists select * from t2 where t2.parent=t1.id
尝试使用 join 代替。 在其他情况下,它没有区别。
我说:为子查询创建函数可以消除混乱的问题,并允许您为子查询实现额外的逻辑。 所以我建议尽可能为子查询创建函数。
代码混乱是一个大问题,几十年来,业界一直在努力避免它。
根据我对两种情况的观察,如果一个表的记录少于 100,000 条,那么连接将快速运行。
但是在表有超过 100,000 条记录的情况下,子查询是最好的结果。
我有一张有 500,000 条记录的表,我在查询下面创建了它,它的结果时间就像
SELECT *
FROM crv.workorder_details wd
inner join crv.workorder wr on wr.workorder_id = wd.workorder_id;
结果:13.3 秒
select *
from crv.workorder_details
where workorder_id in (select workorder_id from crv.workorder)
结果:1.65 秒
从旧的 Mambo CMS 在一个非常大的数据库上运行:
SELECT id, alias
FROM
mos_categories
WHERE
id IN (
SELECT
DISTINCT catid
FROM mos_content
);
0 秒
SELECT
DISTINCT mos_content.catid,
mos_categories.alias
FROM
mos_content, mos_categories
WHERE
mos_content.catid = mos_categories.id;
~3 秒
EXPLAIN 显示他们检查的行数完全相同,但一次需要 3 秒,而一次几乎是即时的。 故事的道德启示? 如果性能很重要(什么时候不重要?),请尝试多种方式,看看哪种方式最快。
和...
SELECT
DISTINCT mos_categories.id,
mos_categories.alias
FROM
mos_content, mos_categories
WHERE
mos_content.catid = mos_categories.id;
0 秒
同样,相同的结果,相同数量的检查行。 我的猜测是 DISTINCT mos_content.catid 比 DISTINCT mos_categories.id 花费的时间长得多。
子查询通常用于将单行作为原子值返回,但它们也可用于使用 IN 关键字将值与多行进行比较。 在 SQL 语句中几乎任何有意义的点都允许使用它们,包括目标列表、WHERE 子句等。 一个简单的子查询可以用作搜索条件。 例如,在一对表之间:
SELECT title
FROM books
WHERE author_id = (
SELECT id
FROM authors
WHERE last_name = 'Bar' AND first_name = 'Foo'
);
请注意,对子查询的结果使用普通值运算符要求仅返回一个字段。 如果您有兴趣检查一组其他值中是否存在单个值,请使用 IN:
SELECT title
FROM books
WHERE author_id IN (
SELECT id FROM authors WHERE last_name ~ '^[A-E]'
);
这与说 LEFT-JOIN 显然不同,在这种情况下,即使连接条件在表 B 中找不到任何匹配的记录,您也只想连接表 A 和 B 中的内容,等等。
如果您只是担心速度,您将不得不检查您的数据库并编写一个好的查询,看看性能是否有任何显着差异。
有人说“当某些 RDBMS 认为一个比另一个更快时,它可以将子查询重写为连接或连接到子查询。”但这种说法适用于简单的情况,当然不适用于带有子查询的复杂查询,这些查询实际上会导致性能上的问题。
MySQL 版本:5.5.28-0ubuntu0.12.04.2-log
我还认为 JOIN 总是比 MySQL 中的子查询更好,但 EXPLAIN 是一种更好的判断方式。 这是一个示例,其中子查询比 JOIN 更有效。
这是我的查询,包含 3 个子查询:
EXPLAIN SELECT vrl.list_id,vrl.ontology_id,vrl.position,l.name AS list_name, vrlih.position AS previous_position, vrl.moved_date
FROM `vote-ranked-listory` vrl
INNER JOIN lists l ON l.list_id = vrl.list_id
INNER JOIN `vote-ranked-list-item-history` vrlih ON vrl.list_id = vrlih.list_id AND vrl.ontology_id=vrlih.ontology_id AND vrlih.type='PREVIOUS_POSITION'
INNER JOIN list_burial_state lbs ON lbs.list_id = vrl.list_id AND lbs.burial_score < 0.5
WHERE vrl.position <= 15 AND l.status='ACTIVE' AND l.is_public=1 AND vrl.ontology_id < 1000000000
AND (SELECT list_id FROM list_tag WHERE list_id=l.list_id AND tag_id=43) IS NULL
AND (SELECT list_id FROM list_tag WHERE list_id=l.list_id AND tag_id=55) IS NULL
AND (SELECT list_id FROM list_tag WHERE list_id=l.list_id AND tag_id=246403) IS NOT NULL
ORDER BY vrl.moved_date DESC LIMIT 200;
解释显示:
+----+--------------------+----------+--------+-----------------------------------------------------+--------------+---------+-------------------------------------------------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+--------------------+----------+--------+-----------------------------------------------------+--------------+---------+-------------------------------------------------+------+--------------------------+
| 1 | PRIMARY | vrl | index | PRIMARY | moved_date | 8 | NULL | 200 | Using where |
| 1 | PRIMARY | l | eq_ref | PRIMARY,status,ispublic,idx_lookup,is_public_status | PRIMARY | 4 | ranker.vrl.list_id | 1 | Using where |
| 1 | PRIMARY | vrlih | eq_ref | PRIMARY | PRIMARY | 9 | ranker.vrl.list_id,ranker.vrl.ontology_id,const | 1 | Using where |
| 1 | PRIMARY | lbs | eq_ref | PRIMARY,idx_list_burial_state,burial_score | PRIMARY | 4 | ranker.vrl.list_id | 1 | Using where |
| 4 | DEPENDENT SUBQUERY | list_tag | ref | list_tag_key,list_id,tag_id | list_tag_key | 9 | ranker.l.list_id,const | 1 | Using where; Using index |
| 3 | DEPENDENT SUBQUERY | list_tag | ref | list_tag_key,list_id,tag_id | list_tag_key | 9 | ranker.l.list_id,const | 1 | Using where; Using index |
| 2 | DEPENDENT SUBQUERY | list_tag | ref | list_tag_key,list_id,tag_id | list_tag_key | 9 | ranker.l.list_id,const | 1 | Using where; Using index |
+----+--------------------+----------+--------+-----------------------------------------------------+--------------+---------+-------------------------------------------------+------+--------------------------+
与 JOIN 相同的查询是:
EXPLAIN SELECT vrl.list_id,vrl.ontology_id,vrl.position,l.name AS list_name, vrlih.position AS previous_position, vrl.moved_date
FROM `vote-ranked-listory` vrl
INNER JOIN lists l ON l.list_id = vrl.list_id
INNER JOIN `vote-ranked-list-item-history` vrlih ON vrl.list_id = vrlih.list_id AND vrl.ontology_id=vrlih.ontology_id AND vrlih.type='PREVIOUS_POSITION'
INNER JOIN list_burial_state lbs ON lbs.list_id = vrl.list_id AND lbs.burial_score < 0.5
LEFT JOIN list_tag lt1 ON lt1.list_id = vrl.list_id AND lt1.tag_id = 43
LEFT JOIN list_tag lt2 ON lt2.list_id = vrl.list_id AND lt2.tag_id = 55
INNER JOIN list_tag lt3 ON lt3.list_id = vrl.list_id AND lt3.tag_id = 246403
WHERE vrl.position <= 15 AND l.status='ACTIVE' AND l.is_public=1 AND vrl.ontology_id < 1000000000
AND lt1.list_id IS NULL AND lt2.tag_id IS NULL
ORDER BY vrl.moved_date DESC LIMIT 200;
输出是:
+----+-------------+-------+--------+-----------------------------------------------------+--------------+---------+---------------------------------------------+------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+--------+-----------------------------------------------------+--------------+---------+---------------------------------------------+------+----------------------------------------------+
| 1 | SIMPLE | lt3 | ref | list_tag_key,list_id,tag_id | tag_id | 5 | const | 2386 | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | l | eq_ref | PRIMARY,status,ispublic,idx_lookup,is_public_status | PRIMARY | 4 | ranker.lt3.list_id | 1 | Using where |
| 1 | SIMPLE | vrlih | ref | PRIMARY | PRIMARY | 4 | ranker.lt3.list_id | 103 | Using where |
| 1 | SIMPLE | vrl | ref | PRIMARY | PRIMARY | 8 | ranker.lt3.list_id,ranker.vrlih.ontology_id | 65 | Using where |
| 1 | SIMPLE | lt1 | ref | list_tag_key,list_id,tag_id | list_tag_key | 9 | ranker.lt3.list_id,const | 1 | Using where; Using index; Not exists |
| 1 | SIMPLE | lbs | eq_ref | PRIMARY,idx_list_burial_state,burial_score | PRIMARY | 4 | ranker.vrl.list_id | 1 | Using where |
| 1 | SIMPLE | lt2 | ref | list_tag_key,list_id,tag_id | list_tag_key | 9 | ranker.lt3.list_id,const | 1 | Using where; Using index |
+----+-------------+-------+--------+-----------------------------------------------------+--------------+---------+---------------------------------------------+------+----------------------------------------------+
rows
的比较说明了区别,带有 JOIN 的查询使用Using temporary; Using filesort
Using temporary; Using filesort
。
当然,当我运行这两个查询时,第一个在 0.02 秒内完成,第二个即使在 1 分钟后也没有完成,所以 EXPLAIN 正确解释了这些查询。
如果我在list_tag
表上没有 INNER JOIN 即如果我删除
AND (SELECT list_id FROM list_tag WHERE list_id=l.list_id AND tag_id=246403) IS NOT NULL
从第一个查询开始,相应地:
INNER JOIN list_tag lt3 ON lt3.list_id = vrl.list_id AND lt3.tag_id = 246403
从第二个查询开始,然后 EXPLAIN 为两个查询返回相同数量的行,并且这两个查询运行速度同样快。
子查询具有动态计算聚合函数的能力。 例如,找到这本书的最低价格并获得所有以该价格出售的书籍。 1) 使用子查询:
SELECT titles, price
FROM Books, Orders
WHERE price =
(SELECT MIN(price)
FROM Orders) AND (Books.ID=Orders.ID);
2) 使用 JOIN
SELECT MIN(price)
FROM Orders;
-----------------
2.99
SELECT titles, price
FROM Books b
INNER JOIN Orders o
ON b.ID = o.ID
WHERE o.price = 2.99;
只有当第二个连接表的数据明显多于主表时,才会看到差异。 我有过如下经历...
我们有一个包含 10 万个条目的用户表,他们的会员数据(友谊)大约有 30 万个条目。 这是一个加入声明,目的是获取朋友和他们的数据,但延迟很大。 但是在成员资格表中只有少量数据的情况下它工作正常。 一旦我们将其更改为使用子查询,它就可以正常工作。
但与此同时,连接查询正在处理条目比主表少的其他表。
所以我认为 join 和 sub 查询语句工作正常,这取决于数据和情况。
如今,许多数据库都可以优化子查询和连接。 因此,您只需使用解释检查您的查询并查看哪个更快。 如果性能没有太大差异,我更喜欢使用子查询,因为它们简单易懂。
我只是在考虑同样的问题,但我在 FROM 部分使用子查询。 我需要从大表连接和查询,“从”表有2800万条记录但结果只有128条这么小的结果大数据! 我正在使用 MAX() 函数。
首先我使用 LEFT JOIN 因为我认为这是正确的方法,mysql 可以优化等。第二次只是为了测试,我重写以针对 JOIN 进行子选择。
LEFT JOIN 运行时间:1.12s SUB-SELECT 运行时间:0.06s
子选择比连接快 18 倍! 就在 chokito adv 中。 子选择看起来很糟糕,但结果......
这取决于多个因素,包括您正在运行的特定查询、数据库中的数据量。 子查询首先运行内部查询,然后再次从结果集中过滤出实际结果。 而在 join 中运行并一次性产生结果。
最好的策略是您应该同时测试连接解决方案和子查询解决方案以获得优化的解决方案。
我不是关系数据库专家,所以对此持保留态度。
关于子查询与连接的一般想法是评估较大查询所采用的路径。
为了执行较大的查询,必须首先执行每个单独的子查询,然后将结果集存储为较大查询与之交互的临时表。
这个临时表没有索引,所以任何比较都需要扫描整个结果集。
相比之下,当您使用连接时,所有索引都在使用中,因此比较需要遍历索引树(或哈希表),这在速度方面成本更低。
现在,我不知道最流行的关系引擎的较新版本是否执行反向评估,并且只是在临时表中加载必要的元素,作为优化方法。
如果您想使用 join 加快查询速度:
对于“内部联接/联接”,不要使用 where 条件,而是在“ON”条件下使用它。 例如:
select id,name from table1 a
join table2 b on a.name=b.name
where id='123'
Try,
select id,name from table1 a
join table2 b on a.name=b.name and a.id='123'
对于“左/右联接”,不要在“ON”条件下使用,因为如果使用左/右联接,它将获取任何一张表的所有行。因此,在“On”中不使用它。 所以,尝试使用“Where”条件
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.