CategoryResourceRepost/极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/09丨子查询:子查询的种类都有哪些,如何提高子查询的性能?.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

183 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="09丨子查询子查询的种类都有哪些如何提高子查询的性能" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2a/2b/2a6fb6c44af8f0e99035a09672c0232b.mp3"></audio>
上节课我讲到了聚集函数以及如何对数据进行分组统计可以说我们之前讲的内容都是围绕单个表的SELECT查询展开的实际上SQL还允许我们进行子查询也就是嵌套在查询中的查询。这样做的好处是可以让我们进行更复杂的查询同时更加容易理解查询的过程。因为很多时候我们无法直接从数据表中得到查询结果需要从查询结果集中再次进行查询才能得到想要的结果。这个“查询结果集”就是今天我们要讲的子查询。
通过今天的文章,我希望你可以掌握以下的内容:
1. 子查询可以分为关联子查询和非关联子查询。我会举一个NBA数据库查询的例子告诉你什么是关联子查询什么是非关联子查询
1. 子查询中有一些关键词可以方便我们对子查询的结果进行比较。比如存在性检测子查询也就是EXISTS子查询以及集合比较子查询其中集合比较子查询关键词有IN、SOME、 ANY和ALL这些关键词在子查询中的作用是什么
1. 子查询也可以作为主查询的列我们如何使用子查询作为计算字段出现在SELECT查询中呢
## 什么是关联子查询,什么是非关联子查询
子查询虽然是一种嵌套查询的形式,不过我们依然可以依据子查询是否执行多次,从而将子查询划分为关联子查询和非关联子查询。
子查询从数据表中查询了数据结果,如果这个数据结果只执行一次,然后这个数据结果作为主查询的条件进行执行,那么这样的子查询叫做非关联子查询。
同样,如果子查询需要执行多次,即采用循环的方式,先从外部查询开始,每次都传入子查询进行查询,然后再将结果反馈给外部,这种嵌套的执行方式就称为关联子查询。
单说概念有点抽象我们用数据表举例说明一下。这里我创建了NBA球员数据库SQL文件你可以从[GitHub](https://github.com/cystanford/sql_nba_data)上下载。
文件中一共包括了5张表player表为球员表team为球队表team_score为球队比赛表player_score为球员比赛成绩表height_grades为球员身高对应的等级表。
其中player表也就是球员表一共有37个球员如下所示
<img src="https://static001.geekbang.org/resource/image/ba/ef/ba91ef95f95bca52a83682a4310918ef.png" alt=""><br>
team表为球队表一共有3支球队如下所示
<img src="https://static001.geekbang.org/resource/image/a6/e9/a65d5b04d416bf35f1ea16da5f05cee9.png" alt="">
team_score表为球队比赛成绩表一共记录了两场比赛的成绩如下所示
<img src="https://static001.geekbang.org/resource/image/11/93/1156804730d29e0c1367fbf31002d693.png" alt=""><br>
player_score表为球员比赛成绩表记录了一场比赛中球员的表现。这张表一共包括19个字段代表的含义如下
<img src="https://static001.geekbang.org/resource/image/af/3a/afc5c950c6fdf9b9c399748bf340843a.png" alt=""><br>
其中shoot_attempts代表总出手的次数它等于二分球出手和三分球出手次数的总和。比如2019年4月1日韦恩·艾灵顿在底特律活塞和印第安纳步行者的比赛中总出手次数为19总命中10三分球13投4中罚球4罚2中因此总分score=(10-4)×2+4×3+2=26也就是二分球得分12+三分球得分12+罚球得分2=26。
需要说明的是,通常在工作中,数据表的字段比较多,一开始创建的时候会知道每个字段的定义,过了一段时间再回过头来看,对当初的定义就不那么确定了,容易混淆字段,解决这一问题最好的方式就是做个说明文档,用实例举例。
比如shoot_attempts是总出手次数这里的总出手次数=二分球出手次数+三分球出手次数,不包括罚球的次数),用上面提到的韦恩·艾灵顿的例子做补充说明,再回过头来看这张表的时候,就可以很容易理解每个字段的定义了。
我们以NBA球员数据表为例假设我们想要知道哪个球员的身高最高最高身高是多少就可以采用子查询的方式
```
SQL: SELECT player_name, height FROM player WHERE height = (SELECT max(height) FROM player)
```
运行结果1条记录
<img src="https://static001.geekbang.org/resource/image/13/b2/133c583fe0317081d13ae99ec17123b2.png" alt=""><br>
你能看到,通过`SELECT max(height) FROM player`可以得到最高身高这个数值结果为2.16然后我们再通过player这个表看谁具有这个身高再进行输出这样的子查询就是非关联子查询。
如果子查询的执行依赖于外部查询通常情况下都是因为子查询中的表用到了外部的表并进行了条件关联因此每执行一次外部查询子查询都要重新计算一次这样的子查询就称之为关联子查询。比如我们想要查找每个球队中大于平均身高的球员有哪些并显示他们的球员姓名、身高以及所在球队ID。
首先我们需要统计球队的平均身高,即`SELECT avg(height) FROM player AS b WHERE a.team_id = b.team_id`然后筛选身高大于这个数值的球员姓名、身高和球队ID
```
SELECT player_name, height, team_id FROM player AS a WHERE height &gt; (SELECT avg(height) FROM player AS b WHERE a.team_id = b.team_id)
```
运行结果18条记录
<img src="https://static001.geekbang.org/resource/image/a5/ac/a53fc11e20a44453176bea25d3e789ac.png" alt="">
这里我们将player表复制成了表a和表b每次计算的时候需要将表a中的team_id传入从句作为已知值。因为每次表a中的team_id可能是不同的所以是关联子查询。如果是非关联子查询那么从句计算的结果是固定的才可以。
## EXISTS子查询
关联子查询通常也会和EXISTS一起来使用EXISTS子查询用来判断条件是否满足满足的话为True不满足为False。
比如我们想要看出场过的球员都有哪些并且显示他们的姓名、球员ID和球队ID。在这个统计中是否出场是通过player_score这张表中的球员出场表现来统计的如果某个球员在player_score中有出场记录则代表他出场过这里就使用到了EXISTS子查询即`EXISTS (SELECT player_id FROM player_score WHERE player.player_id = player_score.player_id)`,然后将它作为筛选的条件,实际上也是关联子查询,即:
```
SQLSELECT player_id, team_id, player_name FROM player WHERE EXISTS (SELECT player_id FROM player_score WHERE player.player_id = player_score.player_id)
```
运行结果19条记录
<img src="https://static001.geekbang.org/resource/image/a6/d3/a6a447a53b9cc158db5f4ffb905aaed3.png" alt=""><br>
同样NOT EXISTS就是不存在的意思我们也可以通过NOT EXISTS查询不存在于player_score表中的球员信息比如主表中的player_id不在子表player_score中判断语句为`NOT EXISTS (SELECT player_id FROM player_score WHERE player.player_id = player_score.player_id)`。整体的SQL语句为
```
SQL: SELECT player_id, team_id, player_name FROM player WHERE NOT EXISTS (SELECT player_id FROM player_score WHERE player.player_id = player_score.player_id)
```
运行结果18条记录
<img src="https://static001.geekbang.org/resource/image/fb/e0/fb0a34e0144a6c0ba9a48d1da4dfade0.png" alt="">
## 集合比较子查询
集合比较子查询的作用是与另一个查询结果集进行比较我们可以在子查询中使用IN、ANY、ALL和SOME操作符它们的含义和英文意义一样
<img src="https://static001.geekbang.org/resource/image/d3/2c/d3867c22616cbdf88ed83865604e8e2c.png" alt="">
还是通过上面那个例子假设我们想要看出场过的球员都有哪些可以采用IN子查询来进行操作
```
SELECT player_id, team_id, player_name FROM player WHERE player_id in (SELECT player_id FROM player_score WHERE player.player_id = player_score.player_id)
```
你会发现运行结果和上面的是一样的那么问题来了既然IN和EXISTS都可以得到相同的结果那么我们该使用IN还是EXISTS呢
我们可以把这个模式抽象为:
```
SELECT * FROM A WHERE cc IN (SELECT cc FROM B)
```
```
SELECT * FROM A WHERE EXIST (SELECT cc FROM B WHERE B.cc=A.cc)
```
实际上在查询过程中在我们对cc列建立索引的情况下我们还需要判断表A和表B的大小。在这里例子当中表A指的是player表表B指的是player_score表。如果表A比表B大那么IN子查询的效率要比EXIST子查询效率高因为这时B表中如果对cc列进行了索引那么IN子查询的效率就会比较高。
同样如果表A比表B小那么使用EXISTS子查询效率会更高因为我们可以使用到A表中对cc列的索引而不用从B中进行cc列的查询。
了解了IN查询后我们来看下ANY和ALL子查询。刚才讲到了ANY和ALL都需要使用比较符比较符包括了&gt;=&lt;&gt;=&lt;=)和(&lt;&gt;)等。
如果我们想要查询球员表中比印第安纳步行者对应的team_id为1002中任意一个球员身高高的球员信息并且输出他们的球员ID、球员姓名和球员身高该怎么写呢首先我们需要找出所有印第安纳步行者队中的球员身高即`SELECT height FROM player WHERE team_id = 1002`然后使用ANY子查询即
```
SQL: SELECT player_id, player_name, height FROM player WHERE height &gt; ANY (SELECT height FROM player WHERE team_id = 1002)
```
运行结果35条记录
<img src="https://static001.geekbang.org/resource/image/45/da/4547b4671d2727eb5075e0c050eac4da.png" alt=""><br>
运行结果为35条你发现有2个人的身高是不如印第安纳步行者的所有球员的。
同样如果我们想要知道比印第安纳步行者对应的team_id为1002中所有球员身高都高的球员的信息并且输出球员ID、球员姓名和球员身高该怎么写呢
```
SQL: SELECT player_id, player_name, height FROM player WHERE height &gt; ALL (SELECT height FROM player WHERE team_id = 1002)
```
运行结果1条记录
<img src="https://static001.geekbang.org/resource/image/b9/b5/b910c7a40a8cfbde7d47409afe5171b5.png" alt=""><br>
我们能看到比印第安纳步行者所有球员都高的球员在player这张表一共37个球员中只有索恩·马克。
需要强调的是ANY、ALL关键字必须与一个比较操作符一起使用。因为如果你不使用比较操作符就起不到集合比较的作用那么使用ANY和ALL就没有任何意义。
## 将子查询作为计算字段
我刚才讲了子查询的几种用法实际上子查询也可以作为主查询的计算字段。比如我想查询每个球队的球员数也就是对应team这张表我需要查询相同的team_id在player这张表中所有的球员数量是多少。
```
SQL: SELECT team_name, (SELECT count(*) FROM player WHERE player.team_id = team.team_id) AS player_num FROM team
```
运行结果3条记录
<img src="https://static001.geekbang.org/resource/image/b3/ae/b39cee43eb0545592e54c5ce533cd8ae.png" alt=""><br>
你能看到在player表中只有底特律活塞和印第安纳步行者的球员数据所以它们的player_num不为0而亚特兰大老鹰的player_num等于0。在查询的时候我将子查询`SELECT count(*) FROM player WHERE player.team_id = team.team_id`作为了计算字段通常我们需要给这个计算字段起一个别名这里我用的是player_num因为子查询的语句比较长使用别名更容易理解。
## 总结
今天我讲解了子查询的使用,按照子查询执行的次数,我们可以将子查询分成关联子查询和非关联子查询,其中非关联子查询与主查询的执行无关,只需要执行一次即可,而关联子查询,则需要将主查询的字段值传入子查询中进行关联查询。
同时在子查询中你可能会使用到EXISTS、IN、ANY、ALL和SOME等关键字。在某些情况下使用EXISTS和IN可以得到相同的效果具体使用哪个执行效率更高则需要看字段的索引情况以及表A和表B哪个表更大。同样IN、ANY、ALL、SOME这些关键字是用于集合比较的SOME是ANY的别名当我们使用ANY或ALL的时候一定要使用比较操作符。
最后,我讲解了如何使用子查询作为计算字段,把子查询的结果作为主查询的列。
SQL中子查询的使用大大增强了SELECT查询的能力因为很多时候查询需要从结果集中获取数据或者需要从同一个表中先计算得出一个数据结果然后与这个数据结果可能是某个标量也可能是某个集合进行比较。
<img src="https://static001.geekbang.org/resource/image/67/48/67dffabba0619fa4d311929c5d1c0f48.png" alt=""><br>
我今天讲解了子查询的使用其中讲到了EXISTS和IN子查询效率的比较当查询字段进行了索引时主表A大于从表B使用IN子查询效率更高相反主表A小于从表B时使用EXISTS子查询效率更高同样如果使用NOT IN子查询和NOT EXISTS子查询在什么情况下哪个效率更高呢
最后请你使用子查询编写SQL语句得到场均得分大于20的球员。场均得分从player_score表中获取同时你需要输出球员的ID、球员姓名以及所在球队的ID信息。
欢迎在评论区写下你的思考,也欢迎点击请朋友读把这篇文章分享给你的朋友或者同事。