简介:数据库实验5嵌套查询.doc是一份数据库课程实验报告,面向正在学习SQL查询的初学者,重点讲解统计查询和嵌套查询的语法与实操。压缩包仅含1个doc文档,大小约642KB,内容覆盖实验目的、统计查询、连接查询、嵌套查询、知识点总结与实验思考,章节编排适合按步骤对照操作。文档完整收录了CPXS数据库上的13类典型查询实例,既有统计客户数目、求库存量总和、查询最高单价等基础练习,也包含查询上海客户订购数量大于200套的订单明细、查找与美美公司在同一城市的客户、检索单价比A01产品更高记录的嵌套查询,可帮助读者理解子查询、连接查询和谓词的实际运用场景。读者可通过这些实例加深对SELECT语句、GROUP BY分组、HAVING过滤和子查询等关键语法的理解。该报告在CSDN已有930人学习浏览,适合数据库实验作业参考、期末复习或SQL入门自学,是一份可直接对照练习的实用资料。
1. 嵌套查询实验:你以为随手写个 IN 就算会了?
做过几次数据库实验的人,多半觉得嵌套查询是“最没技术含量”的一关:不就是子查询套在外面查询里嘛,WHERE 后面跟个 IN、跟个 EXISTS 就完事了。可真让你去跑实验5,你会撞上很多说不清的现象——明明子查询单独执行没问题,合到一起结果就空了;用了 EXISTS 性能上去了,但条件一换结果就错位;更常见的是查询语句写得冗长,教授一眼就看穿你不懂执行逻辑。这个实验真正要练的不是“写出一条能出结果”的 SQL,而是训练你把每条嵌套查询翻译成“数据库底层到底先跑谁、后跑谁、每行跑几次”。这篇文章从原理讲到可复现的建库、样例数据和五类高频写法,再把执行顺序、空值、关联条件这些最容易翻车的点逐个拆开。适合正在做数据库实验的学生,也适合想给自己补一轮子查询基础的开发者和准备面试的从业者。
2. 嵌套查询的前置认知:子查询的类型与执行逻辑
2.1 嵌套查询是什么?三条边界线先划清楚
嵌套查询,也叫子查询或内层查询,指的是在一个 SELECT、INSERT、UPDATE 或 DELETE 语句内部再嵌套一个完整的查询语句。外层的查询叫父查询,内层的叫子查询。动手写之前,需要把三个常见混淆点划清楚。
第一个混淆点是嵌套查询和连接查询的关系。很多同学以为能用 JOIN 解决的问题就不需要子查询,反过来也成立。实际上两者在很多场景下可以互换,但执行路径完全不同:连接查询在绝大多数数据库里会先做笛卡尔积再过滤(优化器会有优化),子查询则常常先执行内层、把结果集物化或作为流式条件,再驱动外层。选择哪个不只看结果对不对,还要看数据量和索引情况。
第二个混淆点是相关子查询和非相关子查询。非相关子查询的内层查询独立于外层,先执行一次,得到一个固定结果集,外层拿这个结果集做条件判断。相关子查询的内层引用外层表的列,外层每处理一行,内层可能都要重新执行一次。这个区别不搞清楚,后面查性能瓶颈和排查结果出错都会无从下手。
第三个混淆点是子查询出现的位置。多数人只见过 WHERE 后面的子查询,但子查询可以出现在 SELECT 列表(标量子查询)、FROM 后面(派生表)、HAVING 子句、甚至 JOIN 的 ON 条件里。实验5 如果只写 WHERE 子查询,覆盖度是不够的,至少要练到 WHERE、FROM、SELECT 三个位置,才能应对不同题型的组合。
2.2 执行顺序与物化机制:为什么单独跑子查询没问题,合起来就翻车
理解嵌套查询的关键在于搞清楚数据库执行引擎是怎么处理内层和外层的。以某数据库系统为例,非相关子查询通常先被优化器识别,确定它可以独立物化——也就是把内层查询的结果先算出并缓存或流式输出,然后外层再基于这份结果执行过滤。相关子查询则不一样,内层条件和外层行绑定,引擎会为外层每一行(或者每一个分组)重新计算内层条件。
这里有一个常见翻车点:单独执行子查询时,返回 300 行结果,看着没问题;嵌套进外层后变成 0 行。原因多半是子查询结果里包含 NULL。IN 的语义遇到 NULL 时,如果左边的值和右边的结果集匹配不上,并且结果集里有 NULL,整个条件的真值会变成 UNKNOWN,WHERE 只保留 TRUE 的行,于是结果被过滤光。这种问题在执行顺序层面看不出来,必须从三值逻辑层面去排查。
接下来需要考虑的情况是物化结果的排序是否影响外层。子查询的结果作为集合使用时不关心顺序,但作为派生表(FROM 子句)时,如果外层要分页或取 Top N,内层不排序会导致结果不稳定。这也是实验里常被忽略的规范:在子查询内部 fin 排序,然后外层再包一层查询做分页,不能指望数据库自动保留子查询的排列顺序。
理解了这两点,就可以进入实际操作环节。下面一章把整个实验的数据库环境、建表语句和样例数据铺好,保证后面的查询示例可以原样在你的机器上跑通。
3. 把实验环境搭起来:建库建表与样例数据的准备
3.1 用一段 SQL 脚本初始化实验库:三个表的设计思路
实验5 的嵌套查询需要一个足够“别扭”的库结构——表不能太少,字段之间的关联不能太直白,否则嵌套查询完全没有发挥空间。常见做法是准备三张表:学生表、课程表、选课表。学生表里有学号、姓名、专业、出生年份;课程表里有课程号、课程名、学分、授课教师;选课表记录学号、课程号、成绩。这三张表构成经典的选课模型,既能做 IN 和 EXISTS 的对比,也能方便验证 ANY/ALL 和标量子查询。
下面给出可直接执行的建表脚本。注意用到的数据库环境是 MySQL 8.x,字符集和排序规则统一设置为 utf8mb4 和 utf8mb4_unicode_ci,避免中文字段比较出问题:
CREATE DATABASE IF NOT EXISTS lab5 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE lab5; CREATE TABLE student ( sid CHAR(10) PRIMARY KEY, sname VARCHAR(20) NOT NULL, major VARCHAR(30), birth_year INT ); CREATE TABLE course ( cid CHAR(6) PRIMARY KEY, cname VARCHAR(40) NOT NULL, credit DECIMAL(3,1), teacher VARCHAR(20) ); CREATE TABLE sc ( sid CHAR(10), cid CHAR(6), score DECIMAL(5,1), PRIMARY KEY (sid, cid), FOREIGN KEY (sid) REFERENCES student(sid), FOREIGN KEY (cid) REFERENCES course(cid) );这段脚本里几个设计决策值得说明。学号用 CHAR(10) 而不是 INT,是因为学号通常有前导零,用整型会丢格式;成绩用 DECIMAL(5,1) 而不是 INT,是为了能存 59.5 这种半分制成绩,也符合多数高校的计分规则。外键在这里不只是形式,后面做 EXISTS 相关子查询时,外键能保证测试数据的一致性。
对于装了旧版本数据库或者不想开外键的同学,可以在建表时不写 FOREIGN KEY 部分,只保留 PRIMARY KEY,这样导入数据时更自由。但注意如果外键约束缺失,脏数据会导致某些嵌套查询结果看起来“不合理”,排查时优先检查这一层。
3.2 灌入样例数据:数据量要小,但要把坑埋够
样例数据的核心原则是“小但全”:行数控制在十几条,但要包含选课缺勤(没有成绩的记录)、同一门课多人选修、某人选了多门课、某个学生一门课都没选、某门课没人选这些边界情况。如果数据全是你好我好大家好的标准记录,很多嵌套查询的边界条件根本测不出来。
INSERT INTO student (sid, sname, major, birth_year) VALUES ('20230001', '张一', '计算机', 2005), ('20230002', '李二', '计算机', 2004), ('20230003', '王三', '软件工程', 2005), ('20230004', '赵四', '数据科学', 2006), ('20230005', '钱五', '计算机', 2004), ('20230006', '孙六', '网络工程', 2005), ('20230007', '周七', '软件工程', 2005), ('20230008', '吴八', '数据科学', 2006); INSERT INTO course (cid, cname, credit, teacher) VALUES ('C001', '数据库原理', 3.0, '刘老师'), ('C002', '操作系统', 4.0, '陈老师'), ('C003', '数据结构', 3.5, '王老师'), ('C004', '计算机网络', 3.0, '李老师'), ('C005', '软件工程', 2.5, '赵老师'); INSERT INTO sc (sid, cid, score) VALUES ('20230001', 'C001', 88.0), ('20230001', 'C002', 76.5), ('20230002', 'C001', 91.0), ('20230003', 'C001', 66.0), ('20230003', 'C003', 73.5), ('20230004', 'C002', 59.0), ('20230004', 'C004', 82.0), ('20230005', 'C001', 45.0), ('20230005', 'C004', 92.5), ('20230006', 'C003', 88.0), ('20230007', 'C002', 70.0), ('20230007', 'C005', 60.0), ('20230008', 'C005', NULL);数据里埋了三个明显的坑位:学生钱五有成绩低于 60 的记录;学生吴八在选课表里有记录但成绩是 NULL;学生孙六只选了一门课;周七没选数据库原理。后面写“查选修了全部课程的学生”“查平均分大于某值的学生”这类题时,这些边界记录就是验证逻辑的关键。
往 MySQL 里执行这份数据前,建议把 SQL 文件放在本地,用 source 命令整体导入,避免在图形化客户端一行行复制漏掉分号。导入完成后,先用 SELECT COUNT(*) 分别查三张表,确认是 8 名学生、5 门课、13 条选课记录,再开始写查询。
4. 五种高频嵌套查询的写法与参数说明
4.1 IN 子查询:最直观,但对 NULL 最敏感的写法
IN 子查询是实验最开始练习的内容。目标题型通常是“查询选修了课程号为 C001 的学生姓名”或者“查询与张一在同一专业的学生”。用 IN 写,内层返回一个列的值集合,外层判断每一行的值是否在这个集合里。
-- 查询选修了数据库原理(C001)的学生学号和姓名 SELECT sid, sname FROM student WHERE sid IN ( SELECT sid FROM sc WHERE cid = 'C001' );这条查询的执行路径是:先执行内层,从选课表里筛出 cid 等于 C001 的学号集合;然后外层遍历学生表,逐行判断学号是否在这个集合中。如果 sc 表里对应的 cid 不存在,内层返回空集,IN 的判断结果全部为 FALSE,外层无输出——这是想要的行为。
但要注意一个变体。如果把内层改成查询“没有选修任何课程的学生”,一些初学者会写成WHERE sid NOT IN (SELECT sid FROM sc),这看起来合理,但如果 sc.sid 列里存在 NULL,NOT IN 的结果是整个查询变成 0 行,而不是返回没选课的人。这条在实验里就是经典的反直觉坑。想验证的话,可以在 sc 表里插入一条 sid 为 NULL 的记录再跑这条查询,结果会让所有记录消失。
需要记住的参数语义是:IN 等价于= ANY,NOT IN 等价于<> ALL。但 NOT IN 遇到 NULL 时的行为和 ANY/ALL 不同,具体差异在避坑章节单独展开。
4.2 EXISTS 相关子查询:外层驱动内层,场景覆盖更广
EXISTS 是嵌套查询里的重点题型,几乎每个实验都会有一道“查询选修了所有课程的学生”或“查询没有选修任何课程的学生”。EXISTS 的核心是:内层查询不返回具体列值,只返回真值——只要内层查询结果集非空,EXISTS 条件即成立;内层为空,则不成立。相关子查询的形式是内层引用外层表的列。
-- 查询没有选修任何课程的学生 SELECT sid, sname FROM student s WHERE NOT EXISTS ( SELECT 1 FROM sc WHERE sc.sid = s.sid );这里内层的sc.sid = s.sid是关联条件,把外层 student 表的每一行带入内层判断。执行过程通俗讲:数据库先取学生表的第1行,拿它的学号去选课表里查找有无匹配记录,有则 NOT EXISTS 为假,过滤掉;没有则保留。接着处理第2行,重复这个过程。
很多教材会强调 SELECT 后面写 1 而不是*,原因是 EXISTS 只关心结果集是否非空,不关心具体列,写 1 在语义上更准确,执行计划里也更有可能走半连接优化。从实验评分角度看,写成SELECT *也不会错,但导师如果看代码规范,通常会提示改成 1 或某一列常量。
EXISTS 适合的场景是判断“存在性”和“全称量词转换”。比如查选修了全部课程的学生,经典做法是双重 NOT EXISTS:找一个学生,不存在某门课是他没选的。这个写法在 4.4 专门展开。初学者容易把 EXISTS 和内层返回多列混在一起,实际上 EXISTS 根本不关心里面选了几列,只要非空就行。
4.3 标量子查询与 FROM 子查询:两个容易被忽视的位置
嵌套查询不只出现在 WHERE 里。标量子查询出现在 SELECT 列表里,返回一个单一值,就像查询结果里多了一列计算字段。FROM 子查询则是把内层查询的结果当作一张临时表,外层再基于这张表做聚合或过滤。
-- 查询每门课的课程名及其最高分(用 FROM 子查询) SELECT c.cname, t.max_score FROM course c JOIN ( SELECT cid, MAX(score) AS max_score FROM sc WHERE score IS NOT NULL GROUP BY cid ) t ON c.cid = t.cid;这条语句的执行顺序是:先执行括号内的分组聚合,得到每个课程号的最高分临时表 t;再把 course 表和 t 做连接,取出课程名称。这里有一个细节:内层里写了WHERE score IS NOT NULL,目的是避免把 NULL 成绩当作有效数据参与 MAX 计算。虽然 MAX 函数本身会忽略 NULL,但我们在 GROUP BY 分组前先过滤,可以让结果的语义更加明确——尤其在需要统计“有效选课人数”时,这个过滤是必须的。
标量子查询的典型写法是这样的:
-- 查询学生姓名和选课门数(标量子查询) SELECT sname, (SELECT COUNT(*) FROM sc WHERE sc.sid = student.sid ) AS course_count FROM student;注意这里内层引用了外层的 student.sid,是一个相关的标量子查询。外层有多少行,内层就执行多少次。如果学生表有 8 行,内层 COUNT 会被执行 8 次。数据量小的时候没问题,量大了性能会很差,这是它在生产环境里不受欢迎的原因。实验里只要数据量控制住,用它来理解相关子查询的执行频率非常直观。
4.4 全称量词的转化:用双重 NOT EXISTS 查“选修了全部课程”
很多实验题里有一道压轴题:“查询选修了全部课程的学生姓名。”直接用自然语言翻译成 SQL 是困难的,因为 SQL 没有直接的“全称量词”语法。常见解法是把这个命题取逆否:找一个学生,如果不存在任何一门课程是他没选的,那他就选修了全部课程。翻译成 SQL 就是双重 NOT EXISTS。
SELECT sid, sname FROM student s WHERE NOT EXISTS ( SELECT 1 FROM course c WHERE NOT EXISTS ( SELECT 1 FROM sc WHERE sc.sid = s.sid AND sc.cid = c.cid ) );这个查询分三层理解。最内层判断“这个学生是否选了这门课”,通过学号和课程号两个条件同时匹配;中间层的逻辑是“是否存在一门课该学生没选”,也就是最内层查询为空;最外层的 NOT EXISTS 再把结果取反,变成“不存在任何一个没选的课”,等价于“全选了”。
用实验里的样例数据跑,结果应该是学生张一、赵四。张一选了 C001、C002,赵四选了 C002、C004,但他们各自并没有选满全部 5 门课。等一下——这里有个重要的语义陷阱:课程表里一共 5 门课,没有学生选了全部 5 门,所以查询结果应该为空集,但如果写成查询“选了课程表中的全部课程”而不是“选了 sc 表中出现过全部课程”,就能看出两套结果。
两种语义的差别在于:课程表里的课程是所有可选课,选课表里的课程是有学生选过的课。如果某个课程无人选,它依然在 course 表里,学生是不可能“选修全部课程”的。因此双重 NOT EXISTS 是正确语义。如果某同学写成COUNT(DISTINCT sc.cid) = (SELECT COUNT(*) FROM course),思路也对,但在空值处理和分组逻辑上更容易出错,这里优先推荐 NOT EXISTS 写法。
4.5 ANY 与 ALL 子查询:比较符配上极值查询
ANY 和 ALL 在实验中的典型应用是“查询比某一专业任意学生年龄都大的学生”或“查询比所有计算机专业学生成绩都高的选课记录”。它们的语法位置和 IN 相似,但比较符不同。
-- 查询比计算机专业任意一个学生出生年份早的学生(即年龄更大) SELECT sid, sname, birth_year FROM student WHERE birth_year < ANY ( SELECT birth_year FROM student WHERE major = '计算机' );内层先查出所有计算机专业学生的出生年份集合,例如 {2005, 2005, 2004, 2004}。然后外层逐行比较:只要某个学生的出生年份小于集合里的任何一个值,条件成立。这里要注意比较方向:出生年份越小,年龄越大。< ANY的意思是“小于其中任意一个”,等价于 “小于集合中的最大值”。反过来> ALL等价于“大于集合中的最大值”。
一个常见做法是把 ANY/ALL 改写成聚合函数形式:< ANY改成< MAX,> ALL改成> MAX。实验结果可以验证两个写法返回一样的结果,但聚合写法往往执行效率更高,因为优化器更容易利用索引。实验报告的题目如果要求两种写法都给出,这种等价改写就是很好的加分点。
在使用 ANY 和 ALL 时,必须小心子查询结果包含 NULL。如果内层结果集里存在 NULL,ANY 的结果可能为 TRUE(只要有一个真值就行),而 ALL 只要遇到 NULL 就会把整个比较变为 UNKNOWN。比如score > ALL (SELECT score FROM sc WHERE ...),如果该子查询结果里包含 NULL,那所有外层行的条件都是未知,最后查询结果可能就是 0 行。实验里这个问题非常隐蔽,建议在写 ALL 时内层加WHERE score IS NOT NULL,提前规避。
5. 嵌套查询实验的避坑与排查指南
5.1 坑一:NOT IN 遇上 NULL,全表结果消失
现象:写SELECT ... WHERE sid NOT IN (SELECT sid FROM sc)想找没选课的学生,结果一条都不返回。
原因:这是三值逻辑问题。当子查询结果 sid 集合中包含 NULL 时,NOT IN 的语义要求外层行的 sid 不等于集合里的每一个值。但只要和 NULL 比较,结果就是 UNKNOWN,WHERE 只保留 TRUE。也就是说,集合里只要有一个 NULL,所有行的 NOT IN 判断都可能变成 UNKNOWN,于是整条查询结果为空。
解决:两个方向。一是给 sc 表的 sid 加 NOT NULL 约束,从源头杜绝;二是改写为 NOT EXISTS,因为 EXISTS 对 NULL 免疫:
SELECT sid, sname FROM student s WHERE NOT EXISTS ( SELECT 1 FROM sc WHERE sc.sid = s.sid );这条在 4.2 已经验证过,结果会正确返回吴八(成绩为 NULL 但存在选课记录)之外没有选课记录的学生。需要反思的不仅是写法,而是真正理解 SQL 的三值逻辑:任何与 NULL 的比较产物都不是 TRUE 或 FALSE,而是 UNKNOWN。以后凡是写带 NOT 的子查询,第一反应先检查内层相关列有没有 NULL 风险。
5.2 坑二:关联条件漏写,内层查询被当成独立查询执行
现象:写相关子查询时,内层 WHERE 里只写了内层表自己的条件,漏了外层表.列名 = 内层表.列名的关联。结果查询没有报错,但返回行数比预期多出很多,或者结果完全不符合题目。
原因:漏写关联条件后,EXISTS 会怎样?看这段有问题的写法:
SELECT sname FROM student s WHERE EXISTS ( SELECT 1 FROM sc WHERE cid = 'C001' );内层查询没有引用外层 s 的任何列,于是 EXISTS 只是一个固定的真值判断——只要 sc 表里存在 C001 的记录,整个 EXISTS 恒为真。结果是返回所有学生姓名,而题目可能要求“选修了 C001 的学生”。这个结果看起来“像是查出来了”,但多了一堆没选课的人。
解决:写相关子查询的固定习惯是:内层 WHERE 里先写关联条件,再写自己的过滤条件。可以按下面顺序组织,减少漏写概率:
SELECT sname FROM student s WHERE EXISTS ( SELECT 1 FROM sc WHERE sc.sid = s.sid AND sc.cid = 'C001' );这在做实验时怎么排查?拿一条错误结果出来,先随机挑一行返回记录,用它的学号手动去 sc 表查询有没有对应选课记录。如果没有,基本就是关联条件漏了。另一个办法是把 EXISTS 改成连接查询,看结果是否一致。
5.3 坑三:内层子查询的排序和分组被外层“吃掉”
现象:FROM 子查询里写了 ORDER BY,然后外层再做过滤或分页,得到的结果顺序不稳定,甚至排序完全失效。比如说“查每门课最高分的学生学号”,内层排了序,外层一包,顺序就乱了。
原因:数据库不保证子查询的排序结果在派生表里得到保留,因为没有语义要求。除非外层也有 ORDER BY,否则结果集的顺序由优化器决定。这是 SQL 标准的行为,不是数据库 bug。
解决:不要在内层 ORDER BY 指望它影响最终结果顺序,外层必须重新 ORDER BY。如果要做“每组取最大”这类需求,建议用窗口函数替代子查询排序,例如:
SELECT cid, sid, score FROM ( SELECT sc.*, ROW_NUMBER() OVER (PARTITION BY cid ORDER BY score DESC) AS rn FROM sc ) t WHERE t.rn = 1;这条里 PARTITION BY cid 是分组窗口,ORDER BY score DESC 在分组内排名,外层再过滤 rn = 1 取每组最高。它避免了子查询排序和派生表的顺序不确定问题,语义也更好。
5.4 坑四:EXISTS 写对了但性能极差,没有走半连接
现象:数据量只有几百行时,EXISTS 查询秒出。但把数据量放大到几万行后,相关子查询变成逐行扫描,耗时从几十毫秒涨到几秒甚至更久。
原因:相关子查询的执行方式天然是“外层行数 × 内层查找开销”。如果没有合适的索引,内层每次关联都在做全表扫描。外层 8 行时无所谓,外层 8 万行时就会爆炸。
解决:给关联列和外层过滤列建索引。以本实验的模型为例:
CREATE INDEX idx_sc_sid ON sc(sid); CREATE INDEX idx_sc_cid ON sc(cid); CREATE INDEX idx_sc_sid_cid ON sc(sid, cid);建完索引后,用 EXPLAIN 看执行计划,EXISTS 相关子查询正常情况下会显示为半连接(semi join)。如果看不到,检查数据库优化器是否被参数限制,或者查询写成了外面套一层视图的复杂形式。实验数据量不大时性能差异不明显,但养成看执行计划的习惯,后面做数据分析或生产环境排查时会省很多事。
5.5 坑五:成绩表里是 NULL 却被当成 0 分参与比较
现象:“查询成绩低于平均分的学生”这类题,错误结果经常少一条或甚至多一条。因为 sc 表里那条成绩为 NULL 的选课记录,在某些聚合计算里被忽略,在另外一些比较里又行为诡异。
原因:AVG、MAX、MIN 等聚合函数会忽略 NULL,COUNT(*) 不会忽略。如果先用AVG(score)求出平均分,再用score < 平均分做比较,NULL 成绩的行不会进入结果集,因为 NULL 与任何数比较都是 UNKNOWN。如果反过来写NOT score >= 平均分,结果仍然不含 NULL 行。所以缺失成绩的学生既不能算“低于平均”,也不算“高于平均”,而是独立的存在。
解决:明确需求后补过滤。如果要统计“成绩有效”的选课记录,先写WHERE score IS NOT NULL;如果要统计“所有选课记录”的平均分,聚合函数内部自然忽略 NULL 即可。警惕点在于两条查询结果不同时,要能解释差异来源。建议在实验报告里专门写一段“空值说明”,说明 NULL 成绩的行如何被排除、为什么排除。
6. 嵌套查询的验证与优化进阶:从跑通到跑好
嵌套查询写完能出结果,只是实验的及格线。往上走一步,需要掌握两类能力:验证结果正确性和改写优化。验证正确性的一个习惯性做法是用“集合语义”来检查——把嵌套查询改写为连接查询,看两个结果集是否完全一致。拿 4.1 的 IN 查询举例,等价写法是:
SELECT DISTINCT s.sid, s.sname FROM student s JOIN sc ON s.sid = sc.sid WHERE sc.cid = 'C001';IN 会自动去重,而 JOIN 不会,所以这里必须写 DISTINCT 才能保证结果一致。这个对比过程本身就是理解嵌套查询和连接查询差异的最好训练。常见改写方案还有:IN 改 EXISTS、相关子查询改窗口函数、双重 NOT EXISTS 改分组计数比对。分组计数比对的写法是:
SELECT sid FROM sc GROUP BY sid HAVING COUNT(DISTINCT cid) = (SELECT COUNT(*) FROM course);这组写法和双重 NOT EXISTS 在这个实验的数据里会得到同样结果,但语义略不同:前者要求“该学生选修的课程数等于课程总数”,后者要求“不存在的课程数为 0”。如果课程表里有无人选修的课,两种写法结果仍会一致,但如果选课表里存在同一学生重复选同一门课(理论上外键约束禁止),写法就会分化。写实验报告时可以并行给出并解释细微差别,这是拿高分的关键。
索引的进阶用法也要提:给 sc 表建好联合索引 (sid, cid) 后,EXISTS 内层的关联查找会走索引,外层驱动方式从逐个探测变成批量探测。用 EXPLAIN 观察 type 字段从 ALL 变成 ref 或 eq_ref,这就是物理层面优化生效的证据。可以做一个数据量放大实验:把 sc 表复制到 10 万行(相同的选课模式重复多次),对比建索引前后查询耗时,记录到实验报告里作为优化论证。
另一个进阶技巧是理解半连接和物化的取舍。某些场景下优化器会把 NOT IN 改写为 anti join,效果和 NOT EXISTS 一致;但优化器不是万能的,复杂嵌套层级多了之后自动改写可能失败。此时手动改写是可控的兜底方案。我的个人习惯是:子查询不超过两层时用可读性好的写法;超过两层时先跑通,再用 EXPLAIN 分析,最后根据执行计划决定是否改写。不迷信“EXISTS 一定比 IN 快”,在数据量小、有合适索引的情况下,给优化器空间,让它自己选。
回到实验本身,给正在做实验5的同学一句经验:不要同时打开多个结果窗口反复比对。把每次查询的预期结果先写注释,再执行 SQL,结果不一致时优先检查关联条件和 NULL 行为——我当年在这个实验上花时间最多的地方不是写 SQL,而是理解为什么单独执行结果和嵌套执行结果会不一样。后来养成的习惯是:任何嵌套查询写完后,里层子查询单独拉出来执行一次,看结果集形状是否符合预期,再看外层。坚持这个习惯之后,嵌套查询的错误率至少降了一半。希望帮到你。
本文还有配套的精品资源,点击获取