简介:这份数据库实验报告面向高校数据库课程学习者,聚焦数据统计查询与嵌套查询的实操训练,帮助读者掌握SELECT语句、统计函数、连接查询及子查询的综合运用。文档以CPXS数据库为背景,涵盖COUNT、SUM、MAX、MIN等函数统计、GROUP BY与HAVING分组筛选、INNER JOIN等多表连接,以及子查询、派生表等嵌套查询的典型题型,并配有实验目的、内容与思考总结,便于对照练习与复盘。资源包内共1个doc文件,大小约642KB,结构完整,可直接用于课程实验提交或自学参考。目前已有930人学习下载,适合正在学习数据库查询语言、需要巩固嵌套查询与统计查询思路的学生和自学者使用。
1. 从一份“数据库实验5嵌套查询.doc”说起:嵌套查询到底在查什么
如果你手头正躺着一份名为“数据库实验5嵌套查询.doc”的实验文档,大概率你面对的是这样一道题:用一条 SQL 把“成绩高于全班平均分的学生”“选修了某门课的所有人”“没有出现在选课表里的学生”这类问题查出来。它不像增删改查那样直白,也不像连接查询那样一眼能看懂表之间的关系,而是把一个查询的结果当成另一个查询的条件来用。这就是嵌套查询,也叫子查询。
它解决的核心问题是:当筛选条件本身需要先算一遍才能得到时,单层查询写不出来,嵌套查询就能派上用场。适合谁?正在做数据库课程实验的学生、刚接触 SQL 的开发者、以及写了几年 CRUD 但一遇到“比平均分高”“不在某集合里”就卡壳的工程师。热搜里“sql的连接查询和子查询”“select”“统计查询”这些词,本质上都在问同一件事:一条 SQL 到底能表达多复杂的逻辑。嵌套查询就是答案的一半。
2. 嵌套查询的执行逻辑与三类写法:先搞懂谁先跑
2.1 子查询不是“先写先跑”,执行顺序由位置决定
很多人第一次写嵌套查询会翻车,是因为脑子里默认“SQL 从上往下执行”。实际上数据库优化器关心的不是书写顺序,而是子查询出现在哪里、能不能被改写。常见位置有三类:
- WHERE 子句里的子查询:作为过滤条件,返回单值或一列值。
- FROM 子句里的子查询:作为一张临时表,必须给别名。
- SELECT 子句里的子查询:作为一列的计算结果,通常配合聚合函数。
理解执行顺序,最实用的判断方法是看子查询能不能“独立跑”。把子查询单独拎出来执行一次,如果能得到结果,那它大概率是非相关子查询,数据库可以先算它,再拿结果去过滤外层。如果子查询里引用了外层的列,单独跑会报错,那就是相关子查询,外层每扫一行,子查询就要跑一次。
提示:相关子查询在数据量大时性能会明显下降,因为它是“逐行触发”的。实验里数据少感觉不到,真实项目里要警惕。
2.2 比较运算符、IN、EXISTS:三种最常用的嵌套写法
先建两张实验表,后面所有例子都基于它们:
-- 学生表 CREATE TABLE student ( sno VARCHAR(10) PRIMARY KEY, -- 学号 sname VARCHAR(20), -- 姓名 sdept VARCHAR(20) -- 院系 ); -- 成绩表 CREATE TABLE score ( sno VARCHAR(10), -- 学号 cno VARCHAR(10), -- 课程号 grade DECIMAL(5,1) -- 成绩 );写法一:比较运算符 + 聚合子查询。查成绩高于全体平均分的学生:
SELECT sno, cno, grade FROM score WHERE grade > (SELECT AVG(grade) FROM score);这里的子查询返回单个值,所以可以直接用>。逻辑说明:内层先算出平均分,外层再逐行比较。参数上唯一要注意的是,如果子查询可能返回多行,用比较运算符就会报错,必须换成IN或加LIMIT。
写法二:IN + 多值子查询。查选修了“数据库”这门课的学生:
SELECT sno, sname FROM student WHERE sno IN ( SELECT sno FROM score WHERE cno = 'C001' );IN适合子查询返回一列多行的场景。逻辑上等价于“把子查询结果当成一个集合,外层判断是否属于这个集合”。参数说明:IN后面的子查询只能返回一列,返回多列会直接报错,这是实验里最常见的翻车点之一。
写法三:EXISTS + 相关子查询。查至少有一门课及格的学生:
SELECT sno, sname FROM student s WHERE EXISTS ( SELECT 1 FROM score sc WHERE sc.sno = s.sno AND sc.grade >= 60 );EXISTS只关心子查询有没有返回行,不关心返回什么,所以写SELECT 1就够了。它的执行逻辑是:外层每取一个学生,就拿学号去子查询里找有没有及格记录,找到就保留。这种写法在“存在性判断”场景下通常比IN更高效,尤其是子查询表很大的时候。
2.3 相关子查询和非相关子查询:性能差异从哪来
把上面三种写法按“是否引用外层列”分类:
| 类型 | 是否引用外层列 | 执行方式 | 典型场景 |
|---|---|---|---|
| 非相关子查询 | 否 | 子查询先独立执行一次 | 比平均分高、IN 集合 |
| 相关子查询 | 是 | 外层每行触发一次子查询 | EXISTS、逐行对比 |
非相关子查询只跑一次,结果可以缓存;相关子查询理论上跑 N 次。实验数据只有几十行时两者没区别,但换成十万行,相关子查询可能就是秒级和分钟级的差距。我一般会先写EXISTS保证语义正确,再用EXPLAIN看执行计划,如果发现子查询被反复扫描,就考虑改写成连接查询。
3. 把嵌套查询落到实验报告里:从建表到出结果的完整步骤
3.1 建表与造数据:实验环境的最小闭环
实验文档通常只给题目,不给数据。自己造数据有个好处:边界情况可控。下面这段脚本建表并插入能覆盖“平均分”“无选课”“多门课”三种情况的数据:
-- 插入学生 INSERT INTO student VALUES ('S001', '张三', '计算机'), ('S002', '李四', '计算机'), ('S003', '王五', '数学'), ('S004', '赵六', '数学'); -- 赵六没有选课 -- 插入成绩 INSERT INTO score VALUES ('S001', 'C001', 85.0), ('S001', 'C002', 90.0), ('S002', 'C001', 55.0), ('S002', 'C002', 60.0), ('S003', 'C001', 78.0);逻辑说明:赵六故意不插入成绩,用来验证“没有选课的学生”这类反连接查询。参数上,grade用DECIMAL(5,1)而不是FLOAT,避免平均分出现84.99999这种浮点误差,实验报告里数字对不上时先检查字段类型。
3.2 逐题拆解:把自然语言翻译成嵌套结构
实验题一般长这样:“查询成绩高于该课程平均分的学生学号、课程号和成绩”。注意这里不是全体平均分,而是每门课的平均分,这就必须用相关子查询:
SELECT sno, cno, grade FROM score sc1 WHERE grade > ( SELECT AVG(grade) FROM score sc2 WHERE sc2.cno = sc1.cno -- 关键:按课程号关联 );逻辑说明:外层每取一条成绩记录,内层就按这条记录的课程号算一次该课平均分,再比较。参数上,sc1和sc2是同一张表的两个别名,必须区分开,否则数据库不知道你引用的是外层还是内层。
再比如“查询没有选修 C001 课程的学生”:
SELECT sno, sname FROM student WHERE sno NOT IN ( SELECT sno FROM score WHERE cno = 'C001' );这里有个血泪经验:如果子查询结果里包含NULL,NOT IN会返回空结果。因为NULL参与比较的结果是未知,整个条件就不成立。解决办法是用NOT EXISTS改写:
SELECT sno, sname FROM student s WHERE NOT EXISTS ( SELECT 1 FROM score sc WHERE sc.sno = s.sno AND sc.cno = 'C001' );3.3 用 EXPLAIN 验证:子查询到底跑了几次
实验报告里如果只贴 SQL 和结果,说服力有限。加一步执行计划分析,立刻拉开差距。以 MySQL 为例:
EXPLAIN SELECT sno, cno, grade FROM score sc1 WHERE grade > ( SELECT AVG(grade) FROM score sc2 WHERE sc2.cno = sc1.cno );看输出里的type和rows列。如果子查询被标记为DEPENDENT SUBQUERY,说明它是相关子查询,外层每行都会触发。如果数据量大,可以考虑改写成连接 + 分组:
SELECT sc1.sno, sc1.cno, sc1.grade FROM score sc1 JOIN ( SELECT cno, AVG(grade) AS avg_grade FROM score GROUP BY cno ) t ON sc1.cno = t.cno WHERE sc1.grade > t.avg_grade;逻辑说明:把相关子查询改成 FROM 子句里的派生表,先按课程分组算好平均分,再连接回原表。参数上,派生表必须起别名(这里是t),否则报错。这种改写在大数据量下通常更快,因为聚合只算了一次。
4. 嵌套查询避坑:5 个实验里最容易翻车的地方
4.1 子查询返回多行却用了比较运算符
现象:执行WHERE grade > (SELECT grade FROM score WHERE cno='C001')报错Subquery returns more than 1 row。原因:>、<、=这类比较运算符只接受单值,而子查询返回了一列多行。解决:要么在子查询里加聚合函数(AVG、MAX),要么把外层改成IN、ANY、ALL。实验里先确认子查询单独跑返回几行,再决定用哪种。
4.2 NOT IN 遇到 NULL 直接返回空结果
现象:WHERE sno NOT IN (SELECT sno FROM score)明明有学生没选课,却查不出任何行。原因:score表的sno列如果存在NULL,NOT IN的比较结果全部为未知,条件不成立。解决:子查询里加WHERE sno IS NOT NULL,或者直接用NOT EXISTS。这是 SQL 里最经典的“玄学”之一,实验报告里写清楚能加分。
4.3 相关子查询忘了写关联条件,退化成笛卡尔积
现象:查询跑了几分钟没结果,或者结果行数异常多。原因:内层子查询引用了外层别名,但 WHERE 里漏了关联条件,导致每行都跟全表比较。解决:检查子查询的 WHERE 是否包含内层表.列 = 外层表.列。用EXPLAIN看rows列,如果数字接近两表行数乘积,基本就是漏了关联。
4.4 FROM 子句里的派生表没起别名
现象:SELECT * FROM (SELECT sno FROM score)报语法错误。原因:FROM 后面的子查询必须有一个别名,这是 SQL 标准要求,MySQL、Oracle、PostgreSQL 都一样。解决:加AS t或直接空格加别名。实验里如果用的是 SQL Server,报错信息会更明确,但本质相同。
4.5 嵌套层数过深导致可读性和性能双输
现象:三层以上嵌套,自己过两天都看不懂,改一个条件牵一发动全身。原因:把本可以用连接或 CTE 表达的逻辑硬塞进嵌套。解决:超过两层就考虑用WITH(CTE)拆开,或者改写成连接查询。实验题一般两层够用,但真实项目里三层嵌套基本是维护噩梦。
5. 从实验到实战:嵌套查询的进阶用法与验证习惯
实验做完,真正的价值在于知道什么时候不该用嵌套查询。我自己的习惯是:先写嵌套保证语义正确,再用EXPLAIN看执行计划,如果出现DEPENDENT SUBQUERY且外层行数大,就改写成连接或 CTE。下面这个 CTE 写法,可读性和性能通常都更好:
WITH course_avg AS ( SELECT cno, AVG(grade) AS avg_grade FROM score GROUP BY cno ) SELECT sc.sno, sc.cno, sc.grade FROM score sc JOIN course_avg ca ON sc.cno = ca.cno WHERE sc.grade > ca.avg_grade;逻辑说明:WITH先把每门课的平均分算好,存成临时结果集,再和原表连接。参数上,CTE 只在当前查询有效,不会持久化,适合一次性分析。相比相关子查询,它把“逐行触发”变成了“一次聚合 + 一次连接”,数据量越大优势越明显。
验证方法上,我一般会做两件事:一是用COUNT(*)对比嵌套写法和连接写法的结果行数是否一致;二是故意插入边界数据(比如成绩为NULL、学号不存在于学生表),看两种写法是否都返回预期结果。这一步能提前暴露NOT IN和NULL的坑。
还有一个容易被忽略的技巧:在 SELECT 子句里用标量子查询做列计算。比如查每个学生的选课门数:
SELECT s.sno, s.sname, (SELECT COUNT(*) FROM score sc WHERE sc.sno = s.sno) AS course_count FROM student s;这种写法在报表场景很常见,但要注意子查询必须返回单值,否则报错。如果某个学生没有选课,COUNT(*)返回 0 而不是NULL,这正是我们想要的。
最后说个我自己的教训:早年做实验时,我把“查询没有选修 C001 的学生”写成了NOT IN,数据里恰好没有NULL,跑通了就交了上去。后来换了一批数据,结果直接为空,排查了半天才想起NULL的坑。从那以后,凡是涉及“不在集合里”的判断,我一律用NOT EXISTS,再也没翻过车。嵌套查询不难,难的是对边界条件的敬畏。希望帮到你。
本文还有配套的精品资源,点击获取