数据库系统概论(十)SQL 嵌套查询 超详细讲解(附带例题表格对比带你一步步掌握)

数据库系统概论(十)SQL 嵌套查询 超详细讲解(附带例题表格对比带你一步步掌握)


前言

  • 在前几期博客中,我们探讨了 SQL 连接查询和单表查询技术。
  • 从本节开始,我们将深入讲解 SQL 中嵌套查询的知识点。

我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343

我的数据库系统概论专栏
https://blog.csdn.net/2402_83322742/category_12911520.html?spm=1001.2014.3001.5482


一、什么是嵌套查询?

核心概念

嵌套查询就像"大问题拆分成小问题",把一个查询(小问题)藏在另一个查询(大问题)里,层层解决。

1. 基础组成:查询块

  • 查询块 :一个完整的 SELECT-FROM-WHERE 语句,比如:

    sql 复制代码
    SELECT Sno FROM SC WHERE Cno='81001'  -- 小问题:查选了81001课程的学号
  • 嵌套 :把小问题的结果,当作大问题的条件。大问题叫 外层查询(父查询) ,小问题叫 内层查询(子查询)

2. 嵌套的两种常见位置

(1)藏在 FROM 子句里(当子查询是一张"临时表")

示例

sql 复制代码
SELECT Sname  -- 大问题:查学生姓名
FROM Student, 
     (SELECT Sno FROM SC WHERE Cno='81001') AS SC1  -- 小问题:先查选了81001课的学号,当作临时表SC1
WHERE Student.Sno=SC1.Sno;  -- 用临时表SC1的学号,匹配学生表找姓名

逻辑 :先得到选了81001课的学号列表(SC1),再从学生表中找到这些学号对应的姓名。

(2)藏在 WHERE 子句里(用子查询结果作条件)

示例

sql 复制代码
SELECT Sname  -- 大问题:查学生姓名
FROM Student 
WHERE Sno IN (SELECT Sno FROM SC WHERE Cno='81002');  -- 小问题:查选了81002课的学号,用IN判断是否在其中

逻辑 :先查选了81002课的学号,再在学生表中筛选出学号在这个列表里的学生姓名。

3. 多层嵌套

特点 :子查询里还可以再套子查询(最多嵌套多少层?SQL标准没限制,但实际别写太复杂),像剥洋葱一样层层查询
示例

sql 复制代码
SELECT Sname  -- 大问题:查"所有课程都选了"的学生姓名
FROM Student 
WHERE NOT EXISTS (  -- 外层:不存在"该学生没选的课程"
    SELECT * 
    FROM Course 
    WHERE NOT EXISTS (  -- 内层:不存在"该课程没被该学生选"
        SELECT * 
        FROM SC 
        WHERE Sno= Student.Sno  -- 学生学号
          AND Cno= Course.Cno  -- 课程编号
    )
);

逻辑

  1. 外层 NOT EXISTS:找"不存在没选的课程"的学生。
  2. 内层 NOT EXISTS:对每门课程,检查"学生是否没选这门课"。
  3. 两层结合:如果所有课程都被学生选了(内层都不存在没选的情况),外层就会选中该学生。

4. 子查询的限制:不能用 ORDER BY

  • 原因:子查询只是给外层提供数据(比如学号列表),不需要排序,排序留给外层做。

  • 正确做法:

    sql 复制代码
    SELECT Sname 
    FROM Student 
    WHERE Sno IN (
        SELECT Sno FROM SC WHERE Cno='81002'  -- 内层不排序
    )
    ORDER BY Sname;  -- 排序放在外层

二、带有 IN 谓词的子查询

1. IN谓词的核心作用

一句话理解

IN 就像"在名单里",判断某个值是否在子查询返回的集合中。

场景举例

想查"选了'信息系统概论'这门课的学生",可以分两步:

  1. 先查这门课的课程号(比如81004)→ 得到一个"课程号名单"。
  2. 再查选了这个课程号的学生学号 → 得到"学号名单"。
  3. 最后从学生表中找出学号在"学号名单"里的学生 → 用 IN 判断是否在名单中。

2. 子查询 vs 连接查询

相同目标:查选修"信息系统概论"的学生学号和姓名。

(1)连接查询(表关联法)
sql 复制代码
SELECT Sno, Sname 
FROM Student 
JOIN SC ON Student.Sno = SC.Sno  -- 学生表和选课表用学号关联
JOIN Course ON SC.Cno = Course.Cno  -- 选课表和课程表用课程号关联
WHERE Course.Cname = '信息系统概论';  -- 直接通过表关联找到对应课程的学生

逻辑:把三张表"拼"在一起,像拼图一样直接找到符合条件的行。

(2)子查询(分步筛选法)
sql 复制代码
SELECT Sno, Sname 
FROM Student 
WHERE Sno IN (  -- 学号是否在"选了这门课的学号名单"里?
  SELECT Sno FROM SC 
  WHERE Cno IN (  -- 课程号是否在"信息系统概论的课程号名单"里?
    SELECT Cno FROM Course WHERE Cname='信息系统概论'  -- 先查课程号
  )
);

逻辑

  • 最内层:先查"信息系统概论"的课程号(比如81004)→ 得到课程号名单。
  • 中间层:再查选了这个课程号的学号 → 得到学号名单。
  • 最外层:从学生表中筛选学号在名单里的学生 → 用 IN 做判断。

对比总结

  • 连接查询:适合"直接拼表关联"的简单场景,性能可能更高。
  • 子查询:适合"分步解决问题"的逻辑,思路更清晰,尤其适合多层筛选。

3. 不相关子查询

定义 :子查询的条件不依赖父查询 ,可以独立运行,结果直接给父查询用。
特点

  • 子查询先执行,结果是一个固定的集合(比如学号列表、课程号列表)。
  • 父查询再用这个集合做条件筛选。

示例:查平均分高于85分的学生

sql 复制代码
SELECT Sno, Sname, Smajor  -- 父查询:从学生表找学生
FROM Student 
WHERE Sno IN (  
  SELECT Sno FROM SC  -- 子查询:先查平均分>85的学号
  GROUP BY Sno  -- 按学号分组,算每个学生的平均分
  HAVING AVG(grade) > 85  
);

执行步骤

  1. 子查询独立运行:按学号分组,算出平均分>85的学号列表(如{001, 003})。
  2. 父查询用 IN 判断学生学号是否在列表里,返回结果。

4. IN子查询的常见用法场景

场景描述 示例SQL(简化版)
查选了某课程的学生 SELECT 姓名 FROM 学生 WHERE 学号 IN (SELECT 学号 FROM 选课 WHERE 课程号='81002')
查某类课程的学生信息 多层IN嵌套,先查课程号,再查学号
查分组统计后的结果(如平均分) SELECT ... WHERE 学号 IN (SELECT 学号 FROM 选课 GROUP BY 学号 HAVING AVG(成绩)>85)

三、带有比较运算符的子查询

1. 带比较运算符的子查询是什么?

核心思路 :子查询的结果是一个单一值 (比如一个数字、一个名字),这时可以用 > < = 等比较运算符,把这个单一值和父查询的数据做对比。

举个例子

想查和"刘晨"同一个专业的学生。

  • 子查询先找出刘晨的专业:SELECT Smajor FROM Student WHERE Sname = '刘晨',结果是一个值(比如"计算机")。

  • 父查询用 = 比较,找出专业等于这个值的学生:

    sql 复制代码
    SELECT Sno, Sname, Smajor 
    FROM Student
    WHERE Smajor = (子查询结果)

注意!

子查询必须跟在比较符后面不能写反

❌ 错误写法:WHERE (子查询) = Smajor

✅ 正确写法:WHERE Smajor = (子查询)

2. 子查询的分类

(1)不相关子查询

  • 特点 :子查询的条件不依赖父查询,可以先独立运行子查询,再把结果传给父查询。

  • 执行顺序:先算子查询 → 得到结果 → 再用结果执行父查询。

  • 例子 :查刘晨同系的学生(子查询只需要查一次"刘晨的系")。

    sql 复制代码
    SELECT Sno, Sname, Sdept 
    FROM Student
    WHERE Sdept = (SELECT Sdept FROM Student WHERE Sname = '刘晨')

(2)相关子查询

  • 特点 :子查询的条件依赖父查询当前行的数据,需要父查询和子查询"联动"执行。

  • 执行顺序

    1. 父查询先取第一行数据,把相关值(比如学号)传给子查询;
    2. 子查询用这个值计算结果(比如该学号的平均分);
    3. 父查询用结果判断当前行是否符合条件,符合就保留;
    4. 重复步骤1-3,直到处理完所有行。
  • 例子 :查每个学生超过自己平均分的课程成绩。

    sql 复制代码
    SELECT Sno, Cno 
    FROM SC x  -- x是外层表的别名
    WHERE Grade >= (
      SELECT AVG(Grade) 
      FROM SC y  -- y是内层表的别名
      WHERE y.Sno = x.Sno  -- 内层用x的学号找对应数据
    )

    通俗理解

    • 先看第一个学生(比如学号20180001),子查询算出他的平均分是89;
    • 父查询检查他的每门课成绩是否≥89,符合条件的就选出来;
    • 再处理下一个学生,重复这个过程。

3. 经典案例:查每门课的最高分学生

需求:找出每门课成绩最高的学生的学号、课程号和成绩。
SQL写法

sql 复制代码
SELECT sno, cno, grade 
FROM sc x
WHERE grade = (
  SELECT MAX(grade) 
  FROM sc y
  WHERE y.cno = x.cno  -- 内层根据外层的课程号(x.cno)找最高分
)

执行逻辑

  1. 外层先取第一行数据(比如课程81001,学号20180001,成绩85);
  2. 子查询根据课程号81001,算出该课程的最高分是85;
  3. 比较当前成绩85是否等于最高分85,等于就保留;
  4. 处理下一行(比如课程81002,学号20180002,成绩98),子查询算出该课程最高分98,符合条件,保留;
  5. 以此类推,直到所有行处理完。

四、带有ANY(SOME)或ALL谓词的子查询

1. 什么是ANY和ALL?

  • ANY = 任意一个 (只要满足集合中的某一个值)

  • 类似"至少有一个行符合条件"。

  • ALL = 所有 (必须满足集合中的每一个值)

  • 类似"所有行都要符合条件"。

举个生活例子

  • 如果你说"我要选一门 比ANY数学课分数高 的课" → 只要有一门数学课分数比它低就行;
  • 如果你说"我要选一门 比ALL数学课分数高 的课" → 必须所有数学课分数都比它低!

2. ANY/ALL怎么用?

格式
比较运算符 + ANY/ALL + (子查询)

比如:

  • > ANY:大于子查询结果中的某个值
  • < ALL:小于子查询结果中的所有值

常见搭配表

需求 用ANY/ALL 等价于聚集函数 例子
等于集合中某个值 = ANY IN Sno IN (子查询) 等价于 Sno = ANY (子查询)
不等于集合中所有值 <> ALL NOT IN Sno NOT IN (子查询) 等价于 Sno <> ALL (子查询)
大于集合中最小的 > ANY > MIN 比任意一个值大 → 只要大于最小值就行
大于集合中最大的 > ALL > MAX 比所有值大 → 必须大于最大值
小于集合中最大的 < ANY < MAX 比某个值小 → 只要小于最大值就行
小于集合中最小的 < ALL < MIN 比所有值小 → 必须小于最小值

3. 经典案例

场景 :学生表中有计算机专业和其他专业,想找非计算机专业中年龄相关的学生。

表结构

Sname Smajor Sbirthdate
张三 计算机 2000-01-01
李四 信息安全 2001-03-08
王五 计算机 2000-12-31
赵六 数据科学 2002-06-12

案例1:查非计算机专业中,比计算机专业任意一个学生年龄小的学生(出生日期更晚)

需求翻译 :只要比计算机专业里至少一个人年龄小(即出生日期晚于计算机专业的某个人)。

sql 复制代码
SELECT Sname, Smajor, Sbirthdate
FROM Student
WHERE Smajor <> '计算机'  -- 排除计算机专业
  AND Sbirthdate > ANY (
    SELECT Sbirthdate FROM Student WHERE Smajor = '计算机'
  );

执行逻辑

  • 子查询先找出计算机专业的所有出生日期:2000-01-012000-12-31
  • > ANY 表示只要大于其中任意一个值 就行 → 比如赵六的2002-06-12大于2000-01-01,符合条件。

等价写法 :用MIN聚集函数

sql 复制代码
SELECT ...
WHERE Sbirthdate > (SELECT MIN(Sbirthdate) FROM Student WHERE Smajor='计算机');

(因为只要大于最小值,必然大于某个值)

案例2:查非计算机专业中,比计算机专业所有学生年龄都小的学生(出生日期更晚)

需求翻译 :必须比计算机专业里所有人年龄都小(出生日期晚于计算机专业的所有人)。

sql 复制代码
SELECT Sname, Smajor, Sbirthdate
FROM Student
WHERE Smajor <> '计算机'  
  AND Sbirthdate > ALL (
    SELECT Sbirthdate FROM Student WHERE Smajor = '计算机'
  );

执行逻辑

  • > ALL 表示必须大于所有值 → 计算机专业最大的出生日期是2000-12-31,只有赵六的2002-06-12大于它,所以只有赵六符合。

等价写法 :用MAX聚集函数

sql 复制代码
SELECT ...
WHERE Sbirthdate > (SELECT MAX(Sbirthdate) FROM Student WHERE Smajor='计算机');

(必须大于最大值,才能大于所有值)

4. ANY/ALL和聚集函数的等价表

需求 用ANY/ALL 等价的聚集函数写法
大于某个值 > ANY > MIN
大于所有值 > ALL > MAX
小于某个值 < ANY < MAX
小于所有值 < ALL < MIN
等于某个值(IN) = ANY IN
不等于所有值(NOT IN) <> ALL NOT IN

五、带有 EXISTS 谓词的子查询

1. EXISTS谓词的基本概念

核心作用 :判断子查询是否有结果 ,返回的是true或者false,并不关注子查询的具体内容。

  • EXISTS后的子查询能查出数据(结果不为空)时,就返回true,此时外层查询的条件得以成立;
  • 若子查询没有结果(结果为空),则返回false,外层查询的条件不成立。

语法格式

sql 复制代码
WHERE EXISTS (子查询);
WHERE NOT EXISTS (子查询); -- 子查询为空时返回true

特别说明

  • 子查询通常用SELECT *,这是因为我们只关心子查询有没有结果,而不关心具体查出来的内容是什么。

2. 经典案例

案例1:查询选修了81001号课程的学生姓名。

使用EXISTS的写法

sql 复制代码
SELECT Sname 
FROM Student 
WHERE EXISTS (
    SELECT * 
    FROM SC 
    WHERE Sno = Student.Sno AND Cno = '81001'
);

执行步骤

  1. 外层查询从Student表中取出一行数据,获取当前学生的学号,比如20180001
  2. 子查询开始执行,查看SC表中是否存在学号为20180001且课程号为81001的记录;
  3. 若存在这样的记录,子查询返回true,那么这个学生的姓名就会被添加到结果中;
  4. 若不存在,子查询返回false,该学生的姓名不会出现在结果里;
  5. 重复上述步骤,直到处理完Student表中的所有行。

案例2:查询没有选修81001号课程的学生姓名。

使用NOT EXISTS的写法

sql 复制代码
SELECT Sname 
FROM Student 
WHERE NOT EXISTS (
    SELECT * 
    FROM SC 
    WHERE Sno = Student.Sno AND Cno = '81001'
);

关键逻辑

  • 当子查询没有结果时,NOT EXISTS返回true,意味着这个学生没有选修该课程。

3. EXISTS与IN的对比

这两种方式在很多情况下可以实现相同的查询效果,但也存在一些差异。

场景 EXISTS IN
查询选修了课程的学生 WHERE EXISTS (SELECT * FROM SC WHERE SC.Sno = Student.Sno) WHERE Sno IN (SELECT Sno FROM SC)
适用情况 适合子查询结果集较大的情况,它是通过判断是否存在来筛选的 适合子查询结果集较小的情况,它需要先计算出子查询的所有结果

4. 用EXISTS实现全称量词查询

在SQL里没有直接表示全称量词()的操作符,不过可以借助NOT EXISTS来间接实现。

案例:查询选修了全部课程的学生姓名。

需求理解 :要找出这样的学生,对于所有课程而言,他们都有选修记录。
SQL写法

sql 复制代码
SELECT Sname 
FROM Student 
WHERE NOT EXISTS (
    -- 查找一门课程
    SELECT * 
    FROM Course 
    WHERE NOT EXISTS (
        -- 检查该学生是否选修了这门课
        SELECT * 
        FROM SC 
        WHERE Sno = Student.Sno AND Cno = Course.Cno
    )
);

逻辑拆解

  • 从外层看,对于每个学生,先假设存在一门他没选修的课程;
  • 中间的子查询负责遍历所有课程;
  • 最内层的子查询去验证该学生是否选修了这门课程;
  • 如果对于某门课程,学生没有选修记录,那么NOT EXISTS就会返回false,这个学生就不符合要求;
  • 只有当所有课程都被学生选修了,也就是对于所有课程,内层子查询都能查到记录,NOT EXISTS才会返回true,这个学生才会被查询出来。

以上就是这篇博客的全部内容,下一篇我们将继续探索更多精彩内容。

我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343

我的数据库系统概论专栏
https://blog.csdn.net/2402_83322742/category_12911520.html?spm=1001.2014.3001.5482

|--------------------|
| 非常感谢您的阅读,喜欢的话记得三连哦 |

相关推荐
计算机程序员小杨16 分钟前
计算机专业的你懂的:大数据毕设就选贵州茅台股票分析系统准没错|计算机毕业设计|数据可视化|数据分析
java·大数据
y1y1z20 分钟前
EasyExcel篇
java·excel
DokiDoki之父39 分钟前
多线程—飞机大战排行榜功能(2.0版本)
android·java·开发语言
高山上有一只小老虎1 小时前
走方格的方案数
java·算法
whatever who cares1 小时前
Java 中表示数据集的常用集合类
java·开发语言
秋千码途1 小时前
一道同分排名的SQL题
数据库·sql
JavaArchJourney1 小时前
TreeMap 源码分析
java
whitepure2 小时前
万字详解Java中的IO及序列化
java·后端
还梦呦2 小时前
2025年09月计算机二级Java选择题每日一练——第一期
java·开发语言
与火星的孩子对话2 小时前
Unity高级开发:反射原理深入解析与实践指南 C#
java·unity·c#·游戏引擎·lucene·反射