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

发布于:2025-06-02 ⋅ 阅读:(34) ⋅ 点赞:(0)

数据库系统概论(十)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 语句,比如:
    SELECT Sno FROM SC WHERE Cno='81001'  -- 小问题:查选了81001课程的学号
    
  • 嵌套:把小问题的结果,当作大问题的条件。大问题叫 外层查询(父查询),小问题叫 内层查询(子查询)

2. 嵌套的两种常见位置

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

示例

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 子句里(用子查询结果作条件)

示例

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

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

3. 多层嵌套

特点:子查询里还可以再套子查询(最多嵌套多少层?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

  • 原因:子查询只是给外层提供数据(比如学号列表),不需要排序,排序留给外层做。
  • 正确做法:
    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)连接查询(表关联法)
SELECT Sno, Sname 
FROM Student 
JOIN SC ON Student.Sno = SC.Sno  -- 学生表和选课表用学号关联
JOIN Course ON SC.Cno = Course.Cno  -- 选课表和课程表用课程号关联
WHERE Course.Cname = '信息系统概论';  -- 直接通过表关联找到对应课程的学生

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

(2)子查询(分步筛选法)
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分的学生

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 = '刘晨',结果是一个值(比如“计算机”)。
  • 父查询用 = 比较,找出专业等于这个值的学生:
    SELECT Sno, Sname, Smajor 
    FROM Student
    WHERE Smajor = (子查询结果)
    

注意!
子查询必须跟在比较符后面不能写反
❌ 错误写法:WHERE (子查询) = Smajor
✅ 正确写法:WHERE Smajor = (子查询)

2. 子查询的分类

(1)不相关子查询

  • 特点:子查询的条件不依赖父查询,可以先独立运行子查询,再把结果传给父查询。
  • 执行顺序:先算子查询 → 得到结果 → 再用结果执行父查询。
  • 例子:查刘晨同系的学生(子查询只需要查一次“刘晨的系”)。
    SELECT Sno, Sname, Sdept 
    FROM Student
    WHERE Sdept = (SELECT Sdept FROM Student WHERE Sname = '刘晨')
    

(2)相关子查询

  • 特点:子查询的条件依赖父查询当前行的数据,需要父查询和子查询“联动”执行。
  • 执行顺序
    1. 父查询先取第一行数据,把相关值(比如学号)传给子查询;
    2. 子查询用这个值计算结果(比如该学号的平均分);
    3. 父查询用结果判断当前行是否符合条件,符合就保留;
    4. 重复步骤1-3,直到处理完所有行。
  • 例子:查每个学生超过自己平均分的课程成绩。
    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写法

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:查非计算机专业中,比计算机专业任意一个学生年龄小的学生(出生日期更晚)

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

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聚集函数

SELECT ...
WHERE Sbirthdate > (SELECT MIN(Sbirthdate) FROM Student WHERE Smajor='计算机');

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

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

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

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聚集函数

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,外层查询的条件不成立。

语法格式

WHERE EXISTS (子查询);
WHERE NOT EXISTS (子查询); -- 子查询为空时返回true

特别说明

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

2. 经典案例

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

使用EXISTS的写法

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的写法

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写法

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

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

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到