☰
数据库实验二SQL查询全解析:从连接子查询到避坑自查指南
2026/10/13 9:33:39 网站建设 项目流程

数据库这门课,实验一还风和日丽,实验二就开始让人挠头了。我最近帮同学复盘某高校《数据库原理及应用》实验二(个人版),发现大多数人并不是不会写SQL,而是不知道从哪儿下手、报错之后怎么排查。这份“个人版”的意思,就是我自己补完整的一份带注释版本,不是老师发的标准答案,而是把每一句查询的来龙去脉都写清楚。内容适合两类人:一类是正在做同样实验、卡在某个查询出不来结果的同学;另一类是已经交了作业但还想搞明白“为什么这样写”的初学者。我会从基础查询讲起,把连接、子查询和最后提交前的自查清单全部过一遍,重点是那些我在实验里真实遇到、并且花了挺长时间才定位的坑。

1. 实验二到底考什么:从建表思维切换到查询思维

1.1 为什么实验一轻松、实验二开始懵

实验一通常是建库建表,语句相对固定,照着模板敲CREATE TABLE基本不会出错。实验二则完全不同,它会直接扔给你一批业务问题,比如“找出平均成绩高于80分的学生名单”“列出每个学生的选课数量”,你得自己组织逻辑、选择语法。这种从“照着写”到“想清楚再写”的切换,才是实验二真正的门槛。

我见过不少同学在实验二里反复栽跟头,表面上是语法不熟,实际上是把中文问题翻译成SQL语句的过程卡住了。举个例子,“列出没有选过任何课程的学生”这句话看着简单,但“没选过”三个字在SQL里要做集合差运算,要么用NOT EXISTS,要么用NOT IN,还要考虑空值问题。一个自然语言很容易表达的需求,落到SQL里会有多种解法,每种解法的边界条件还不一样,这就是思维切换带来的落差。

1.2 这次实验的具体目标与三张基础表

参考不少高校的编排,这类实验二一般围绕学生、课程、选课三张表展开,要求掌握四类查询:单表条件查询、聚合分组统计、多表连接查询、子查询与集合查询。先看表结构,我在实验里用的结构是这样的(基本是教材经典设计):

表名字段说明
studentsno, sname, sex, age, dept学生表,sno为主键
coursecno, cname, credit课程表,cno为主键
scsno, cno, grade选课表,(sno,cno)联合主键,grade允许NULL

我给这几张表塞了少量样例数据,方便验证查询结果。学生表大概6行,课程表4行,选课表10行,其中有一两个学生没选课,有一两门课没有学生选,还有一条选课记录的成绩是NULL。这样设计数据是故意的,因为实验二最容易错的恰恰是空值和零行——数据全填得满满当当,很多坑根本暴露不出来。后面讲到的几个踩坑案例,都跟这组数据相关。

2. 用三张表跑通基础SELECT:我的第一版查询与注释

2.1 单表查询的完整示例

我从最简单的单表查询讲起。实验二的第一类题目通常让你练SELECT、WHERE、ORDER BY、DISTINCT。下面几个是我当时反复写过的典型问题。

第一个,查询所有学生的学号和姓名。这个很简单,但我强烈建议不要写SELECT *,显式列出字段能让读代码的人第一时间知道你关心哪些数据。

SELECT sno, sname FROM student;

第二个,查询年龄在20到22岁之间的学生。这里要说清楚BETWEEN是闭区间,包含20和22本身。有同学看到结果里包含了边界年龄,觉得奇怪,其实这是SQL标准的定义。

SELECT sno, sname, age FROM student WHERE age BETWEEN 20 AND 22;

第三个,查询姓“张”的学生。如果数据库编码和字段设置正常,用LIKE '张%'就能搞定。这里要提醒的是,LIKE和等值判断在性能上不一样,等值判断能走索引,前导通配符的LIKE基本不能走索引,数据量小的时候无所谓,数量一大就明显。

SELECT sno, sname FROM student WHERE sname LIKE '张%';

第四个,查询有哪些学生选了课(不重复列出学号)。DISTINCT是很多人第一次接触的语法。需要提醒的是,DISTINCT是对整个结果集去重,不是只对某一列去重,所以SELECT DISTINCT sno, cno和SELECT DISTINCT sno意义完全不同。

SELECT DISTINCT sno FROM sc;

2.2 聚合分组怎么用不犯错

单表查询只是热身,聚合分组才是实验二的第一道分水岭。COUNT、SUM、AVG、MAX、MIN配合GROUP BY使用,容易踩的坑集中在两个地方:WHERE与HAVING的区别、COUNT(*)与COUNT(列名)的区别。

先看一道典型题:统计每个系的学生人数。

SELECT dept, COUNT(*) AS stu_cnt FROM student GROUP BY dept;

GROUP BY的核心规则是:SELECT后面出现的非聚合列,必须出现在GROUP BY里。我用一个生活化的比喻解释:GROUP BY就像是把一堆学生按系分到不同的房间,COUNT(*)就是在每个房间点人数。你既然按系分房,就不可能在一个房间里逐一问每个学生的姓名,因为这个房间里有好几十个人。

另一道题:找出平均成绩大于80分的课程。

SELECT cno, AVG(grade) AS avg_grade FROM sc GROUP BY cno HAVING AVG(grade) > 80;

WHERE和HAVING的分工很容易混淆,其实只要记住一句话:WHERE发生在分组之前,HAVING发生在分组之后。WHERE不能写聚合函数,因为分组之前还没有组的概念;HAVING专门过滤分组后的结果。有同学在WHERE里写AVG(grade)>80直接报错,SQL的语法解释器不会允许聚合函数出现在WHERE里,这不是数据库的bug,而是逻辑顺序本身就不成立。

再说COUNT。COUNT(*)统计的是行数,COUNT(grade)统计的是grade非NULL的行数。如果某个学生缺考,grade是NULL,COUNT(grade)不会把它算进去。实验题如果只问“选了课的人数”,用COUNT(DISTINCT sno)更准确,因为同一名学生选多门课会产生多条记录,直接COUNT(sno)会把人数数重。这里藏着一个经典陷阱:统计选课人数时,有同学直接COUNT(sno),一个学生选了三门课就被数了三次,结果自然不对。

3. 多表连接最容易翻车的地方:三个真实报错复盘

3.1 笛卡尔积膨胀的排查过程

多表连接是实验二的重头戏,也是出错重灾区。我第一个有印象的现象是:查询结果的行数莫名其妙爆炸。题目要求“列出所有选了课的学生姓名、课程名和成绩”,我第一版写的SQL长这样:

SELECT student.sname, course.cname, sc.grade FROM student, course, sc WHERE student.sno = sc.sno;

这条SQL运行不报错,但结果里每个学生和每门课程都组合了一遍,课程名全是重复的。原因很直接:我漏掉了sc.cno = course.cno这个条件。三张表连接,至少需要两个连接条件,缺失一个,就会有一对表产生笛卡尔积。当时的排查过程是:先单独执行SELECT COUNT(*) FROM sc,得到10条;再跑错误SQL,发现结果有40条,明显是10乘4的产物。于是立刻检查连接条件,一眼就看到了漏掉的部分。补上之后,结果回到正常的10条。

SELECT student.sname, course.cname, sc.grade FROM student, course, sc WHERE student.sno = sc.sno AND sc.cno = course.cno;

这个经验可以总结成一句口诀:n张表做连接,至少要写n-1个连接条件,写完数一下条件个数,能少一半低级错误。

3.2 外连接方向搞反

另一道题是“列出所有课程以及它们的选课人数,没有学生选的课程也要保留”。一开始我用内连接:

SELECT course.cname, COUNT(sc.sno) FROM course JOIN sc ON course.cno = sc.cno GROUP BY course.cname;

结果只有3行,而课程表里有4门课。空课的选课人数是0,但在INNER JOIN里,因为选课表sc没有对应记录,整行直接消失了。正确的做法是让course表当左表,用LEFT JOIN:

SELECT course.cname, COUNT(sc.sno) FROM course LEFT JOIN sc ON course.cno = sc.cno GROUP BY course.cname;

这里还有个细节,外连接里ON和WHERE的语义差异非常隐蔽。LEFT JOIN的结果集先按ON条件匹配,已经匹配的行不会因为ON条件不满足而被删除;但WHERE是在连接结果生成之后过滤。举例来说,LEFT JOIN后如果WHERE里包含“右表某字段 = 某值”的条件,NULL行会被过滤掉,LEFT JOIN就退化成INNER JOIN的效果。所以想让所有左表行都保留,过滤条件尽量放在ON里;想让最终结果只保留满足某个条件的行,才放在WHERE里。

3.3 自连接:查“和A同学同系的其他同学”

自连接是很多人第一次觉得SQL“绕”的地方。题目是“找出与A同学同系的其他学生”,如果不用子查询,可以用自连接:

SELECT a.sname AS student_name, b.sname AS classmate_name FROM student a JOIN student b ON a.dept = b.dept AND a.sno != b.sno WHERE a.sname = 'A同学';

自连接的核心是同一张表出现两次,起两个不同的别名,然后像两张普通表一样做连接。我把这个过程类比成“把一张表复印成两份,再在复印件之间连线”。写的时候最容易忘的是a.sno != b.sno,忘了就去掉了“其他”两个字,把自己也算进来了。另外要说一句:实际查询里如果数据量大,自连接最好配合索引和过滤条件,否则一张几万行的表自己和自己连接,计算量会非常可观。

4. 子查询和集合逻辑:为什么我的结果总是多一行少一行

4.1 IN与EXISTS的选择:一个NULL引发的诡异空结果

子查询最典型的题目是“没有选任何课程的学生名单”。我第一反应是写NOT IN版本:

SELECT sno, sname FROM student WHERE sno NOT IN (SELECT sno FROM sc);

在实验数据正常情况下,这个写法运行没问题。但后来我为了复现一个经典坑,专门构造了一个包含NULL的临时查询,把学生学号和NULL做UNION,再拿NOT IN去筛,结果整个查询返回空——这就是SQL三值逻辑的典型表现。SQL里NULL与任何值比较结果都是“未知”,NOT IN需要所有比较都为“真”才返回TRUE,只要子查询结果里混入一行NULL,整个判断就会变成“未知”,行就不会被选中。如果数据是从Excel导入的,某行学号缺失就会变成NULL,你在线上系统里遇到莫名其妙的空结果,很多时候就是这类问题。

稳妥的写法是NOT EXISTS:

SELECT s.sno, s.sname FROM student s WHERE NOT EXISTS ( SELECT 1 FROM sc WHERE sc.sno = s.sno );

NOT EXISTS走的是“逐行检查是否存在”的逻辑,不会因为NULL产生整片空白。这个案例是我觉得实验二里最有价值的一个坑,它说明了一个问题:SQL的“正常”和“符合直觉”之间是有差距的,你必须把空值当成一等公民来对待。

4.2 ANY/ALL与聚合函数的等价改写

再来看一组查询:找出比计科系所有学生年龄都小的学生。这种题目初学者容易写出复杂逻辑,但实际上用ALL就能解决:

SELECT sno, sname, age FROM student WHERE age < ALL ( SELECT age FROM student WHERE dept = '计科' );

ALL的意思是“小于其中每一个”,等价于小于计科系里最小的年龄。所以也可以写成:

SELECT sno, sname, age FROM student WHERE age < ( SELECT MIN(age) FROM student WHERE dept = '计科' );

两种写法结果一样,但用MIN的写法更容易被索引优化,可读性也更好。同理,> ANY等价于> MIN,> ALL等价于> MAX。把这些等价关系背下来,很多看着复杂的题目可以瞬间简化。

说到多一行少一行的问题,有个常见错误是忘了加“其他学生”的排除条件。比如题目要求“找出比计科系所有学生都小的非计科系学生”,如果不在WHERE里加dept != '计科',自己系的学生也会被选进去,行数就多了。返回行数不对的时候,先检查是不是过滤条件写少了,这是最快的排查方向。

5. 提交前一定要过一遍的自查清单与书写习惯

5.1 功能正确之外的效率细节

实验二交上去之前,我通常会做一轮检查,重点不是语法对不对,而是逻辑合不合理、结果会不会被扣分。

第一,能不写SELECT *就不要写。实验数据量小看不出差别,但写SELECT会让结果集里出现一堆无关字段,不管是给别人看还是自己排查,都很干扰。第二,注意字符串比较和排序规则。不同数据库对中文排序不一样,有的按拼音,有的按汉字编码,实验里如果要求按姓名排序,最好先确认数据库的排序规则,不然结果可能和预期不一致。第三,留意常用函数在不同数据库里的差异。比如取当前时间,MySQL用NOW(),有些数据库用GETDATE(),实验环境如果是另一种数据库,函数要跟着改。第四,养成给列起别名的习惯。聚合结果列叫COUNT(),在结果集里显示不友好,用AS取一个可读的名字,报告截图也好看。

提示:能用普通查询解决的问题,不要急着套窗口函数或者复杂子查询。实验二阶段,老师更看重你能不能把基础概念讲清楚,而不是炫技。

5.2 个人版实验报告怎么写最省事

既然是“个人版”实验,我自己的整理方式是这样的:每一道题,按四个小节组织——题目原文、我的SQL、运行结果、关键解释。题目原文是老师的原始需求,SQL是最终版本,运行结果贴关键几行截图,关键解释用一两句话说明“这里为什么用GROUP BY而不写WHERE”之类的理由。这四个小节写下来,不仅复习方便,老师看的时候也能快速理解你的思路。

我还习惯把踩过的错题单独列一个表格,放在报告最后,格式就三列:报错现象、可能原因、解决办法。比如笛卡尔积、NOT IN空值、外连接方向搞反这些坑,全部记下来。这份错题表看起来没什么技术含量,却是最实用的东西,期末复习的时候翻一遍,比重新翻教材高效得多。

最后再分享一个小技巧:我每次写完一段SQL,都会先把表里的样例数据手算一遍预期结果,再跑代码。数据库不会骗人,但它只会执行你写出来的逻辑,而不是你脑子里想的逻辑。手算一遍,很多答案对不对你自己心里就有数了。这个习惯帮我少跑了很多冤枉路,实验二里那些看起来莫名其妙的报错,多数时候都是逻辑边界问题,早发现早解决。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询