news 2026/10/31 11:49:41

数据库原理实验SQL复习:建表约束、查询连接与易错点

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
数据库原理实验SQL复习:建表约束、查询连接与易错点

简介:南华大学数据库原理实验报告围绕SQL Server环境下的数据库建库、建表与查询操作展开,面向大数据技术、计算机等专业修读数据库原理课程的学生,帮助其完成课程实验、理解表间关系并巩固SQL语言应用。资源包共1个doc文件,压缩包约5.38MB,内含实验题目、实验要求、实验代码与实验总结,结构按认识DBMS、简单SQL查询、高级SQL查询、数据更新等模块组织。已有377人学习,说明其对课程实验具有较强参考价值。报告具体覆盖利用管理工具和SQL语句创建“学生选课”数据库及学生、课程、选修三张表,定义主码、外码、默认值与空值约束,并训练单表筛选、排序、分组、多表连接及间接先行课等查询;同时涉及数据插入、更新、删除与完整性约束。读者可据此对照实验步骤、复用代码并复盘查询思路,适合课程作业参考与期末复习。

1. 数据库原理实验的知识地图

数据库原理实验这门课,很多人第一反应是背 SELECT 语法,但把四个实验摊开看,它其实是一条闭环:先用管理工具和 SQL 语句建出一个能自我约束的选课库,再用单表、多表、嵌套组合三层查询把它问清楚,最后用更新语句验证约束到底有没有真的生效。前半程的建表决定了后半程查询的下限,主外码和默认值写歪了,后面几十条查询全在补救。适合正在整理实验报告、准备上机考试,或者想回头把 SQL 从「能跑」练到「写对」的人。下面按四个实验的知识模块逐块拆,重点落在容易写错的地方和一套能直接照着走的复习路线。

2. 建表这一关:三张表的字段、主外码与默认值

实验一看起来最没技术含量,实际是整个课程的地基。实验报告里同时出现了两套命名——图形界面建的「学生200905 / 课程200905 / 选修200905」和脚本建的「Stu200905 / Cou200905 / SC200905」,复习时很容易在脑子里混成一锅。名字怎么叫无所谓,关键是三张表的字段、类型、约束有没有一致地落下去,因为实验二、三的每一条查询都建立在这套结构上。

2.1 三张表是怎么从选课场景里长出来的

把场景翻译成表结构,走的是一条固定链路:先找实体,再定属性,最后处理联系。「学生」和「课程」是两个独立实体,各自有属性;「选修」不是实体而是联系,并且因为一个学生能选多门课、一门课能被多个学生选,它是多对多联系,多对多必须落成一张独立的表,否则成绩这个属性无处安放。

字段类型的选择也不是随手写。学号用定长CHAR(8),因为所有学号长度一致、且经常作为连接键,定长比变长省事;姓名、课程名用VARCHAR,长度按实际留出余量;出生日期用DATE而不是字符串,否则后面算年龄、比大小都要做转换。最容易被忽略的是空值决策:备注可以空,因为很多学生确实没有备注;姓名不能空,它是业务上必须存在的信息;分数可以空,表示「选了但还没考」。

主码的选法同样有讲究。学生表选学号,课程表选课程号,都是单属性主码;选修表既不能选学号也不能选课程号,必须用(学号, 课程号)组成复合主码。复合主码在这里不只是唯一性约束,它还顺手把「同一个学生重复选同一门课」这种脏数据挡在门外。实验要求给性别和学分定义默认值,性别默认「男」、学分默认 4,这类默认值只有在插入语句省略该列时才生效,写语句时如果显式传了 NULL,默认值不会救场。

注意:默认值和「允许为空」是两件事。默认值只在省略列时补位,允许为空则意味着可以显式写入 NULL,两者可以同时存在,也可以互相独立。

2.2 建库建表的语句骨架与三条约束线

用脚本重建一遍是最有效的复习方式,因为它逼你把每个约束写出来。下面这段是课程表与选修表的核心部分:

CREATE DATABASE StuCou200905; GO USE StuCou200905; GO CREATE TABLE 学生200905 ( 学号 CHAR(8) NOT NULL PRIMARY KEY, 姓名 VARCHAR(20) NOT NULL, 性别 CHAR(2) NOT NULL DEFAULT '男' CHECK (性别 IN ('男','女')), 出生日期 DATE NULL, 院系名称 VARCHAR(30) NULL, 备注 VARCHAR(100) NULL ); CREATE TABLE 课程200905 ( 课程号 CHAR(3) NOT NULL PRIMARY KEY, 课程名 VARCHAR(30) NOT NULL, 先行课 CHAR(3) NULL, 学分 DECIMAL(3,1) NOT NULL DEFAULT 4 CHECK (学分 > 0) ); CREATE TABLE 选修200905 ( 学号 CHAR(8) NOT NULL, 课程号 CHAR(3) NOT NULL, 分数 SMALLINT NULL CHECK (分数 BETWEEN 0 AND 100), CONSTRAINT PK_SC PRIMARY KEY (学号, 课程号) ); -- 自参照外码必须建表之后再补,否则表还没建完就引用自己,语句直接报错 ALTER TABLE 课程200905 ADD CONSTRAINT FK_课程_先行课 FOREIGN KEY (先行课) REFERENCES 课程200905(课程号); ALTER TABLE 选修200905 ADD CONSTRAINT FK_SC_学生 FOREIGN KEY (学号) REFERENCES 学生200905(学号); ALTER TABLE 选修200905 ADD CONSTRAINT FK_SC_课程 FOREIGN KEY (课程号) REFERENCES 课程200905(课程号);

这段脚本里有三条完整性线同时生效。实体完整性由PRIMARY KEY保证,主码列既不能空也不能重复;参照完整性由FOREIGN KEY保证,选修表里的学号必须在学生表里存在;用户定义完整性由CHECK和DEFAULT保证,分数不能是 101 分,性别不能写「未知」。DECIMAL(3,1)里的 3 是总位数、1 是小数位,所以学分最大能存 99.9,够用但不要写成DECIMAL(3,0),否则 3.5 学分会被四舍五入成 4。

自参照外码的写法是实验一里最值得单独记一笔的点。课程表的先行课指向课程表自己的课程号,形成「一门课的先修课还是课」的递归结构。如果直接在CREATE TABLE里写这个外码,建表语句执行到一半就失败,所以标准做法是先建表、再ALTER TABLE补约束。图形界面里点出来的关系图之所以能成功,是因为工具在后台帮你拆成了两步。

2.3 图形界面和脚本两条路,差别在哪

实验一特意要求两条路各走一遍,这个设计不是凑工作量。图形界面建表直观,能在设计器里看见字段、类型、是否允许空三列并排,适合第一次建立「表长什么样」的直觉;但它生成的约束名是系统自动编号的,比如DF__学生200905__性别__1A14E395,换个库名就变,脚本没法复用。

脚本建表麻烦一点,换来的是三样东西:可重复、可版本化、可审查。同一个脚本在任何一台装了 SQL Server 的机器上跑出来结构一致,出了问题能直接 diff 两版脚本找差异。做实验报告时如果只贴截图,阅卷的人看不出你的约束到底建没建,贴脚本反而更省事。

提示:图形界面改完表结构后,右键表可以「编写表脚本为 CREATE」,用它反查自己点出来的结构,比凭记忆回忆点了什么更靠谱。

2.4 数据更新:约束到底有没有生效,全靠这一步

实验四的 INSERT、UPDATE、DELETE 表面上是四条 DML 语句,实际是对实验一约束的验收。插入顺序有硬性要求:必须先插学生和课程,再插选修,因为选修表的外码引用前两张表。如果反过来,参照完整性会直接把语句挡回来,报错信息通常是「与 FOREIGN KEY 约束冲突」。

批量插入一次搞定多行,比一条条写省事:

INSERT INTO 学生200905 (学号, 姓名, 性别, 出生日期, 院系名称) VALUES ('0602001', '李伟', '男', '2000-03-12', '计算机系'), ('0602002', '王芳', '女', '1999-11-05', '计算机系'), ('0602003', '李娜', '女', '1998-07-21', '计算机系'), ('0602004', '张强', '男', '2000-01-30', '数学系');

列名列表不能省,省了就得按建表时的字段顺序一个不落地给全,一旦表结构后来加过列,老语句立刻报错。性别列在这条语句里被省略了,插入后会自动变成默认值「男」,这是默认值生效的唯一场景。

更新操作的风险点在 WHERE。少写 WHERE 就是全表更新,而且 SQL Server 不会给你任何提示。稳妥的做法是先在事务里跑一遍,用 SELECT 确认影响范围,再决定提交还是回滚:

BEGIN TRAN; UPDATE 选修200905 SET 分数 = 分数 + 5 WHERE 学号 = '0602001' AND 课程号 = 'C01'; -- 先看清楚动了哪几行,再决定 COMMIT 还是 ROLLBACK SELECT * FROM 选修200905 WHERE 学号 = '0602001'; ROLLBACK;

删除顺序和插入相反,先删子表再删父表。如果直接删学生表里还有选课记录的那个学生,外码约束会拦下来;想要一步到位就得在建外码时写ON DELETE CASCADE,但那意味着删一个学生会连带删掉他所有成绩,实验阶段不建议开这个口子,手动两步删更安全,也更符合「先看清楚再动手」的习惯。

3. 单表查询:WHERE 与 GROUP BY 的分工

实验二的单表部分有十道题,题目不难,难在每道题都藏着一个语法点。把这十道题按「考什么」归类,其实是四组:条件筛选、字符串与范围匹配、聚合分组、排序与取前若干行。搞清楚这四组的边界,单表查询就没有意外了。

3.1 书写顺序不等于执行顺序

SQL 的书写顺序是 SELECT、FROM、WHERE、GROUP BY、HAVING、ORDER BY,但数据库的执行顺序是 FROM、WHERE、GROUP BY、HAVING、SELECT、ORDER BY。这个错位解释了一大堆初学时的困惑。

最典型的是「WHERE 里为什么不能用聚合函数」。WHERE 在 GROUP BY 之前执行,那时候还没有分组,自然没有 COUNT 和 AVG 可用;要对聚合结果做筛选,只能放到 HAVING 里,因为 HAVING 在分组之后执行。另一个困惑是「SELECT 里起的别名为什么在 WHERE 里用不了」,因为 SELECT 排在 WHERE 之后,别名还没生成。

-- 错:WHERE 阶段还没有分组,COUNT 不存在 SELECT 学号 FROM 选修200905 WHERE COUNT(*) > 2 GROUP BY 学号; -- 对:分组完成后再筛 SELECT 学号, COUNT(*) AS 课程数 FROM 选修200905 GROUP BY 学号 HAVING COUNT(*) > 2;

字段别名在 ORDER BY 里却可以用,因为 ORDER BY 排在 SELECT 之后。这个差异不用背表格,记住执行顺序就能推出来。

3.2 条件写法:LIKE、BETWEEN、IN、IS NULL

四种条件各管一类场景,混用就会出问题。

LIKE处理模式匹配,%匹配任意长度,_匹配单个字符。「姓李的学生」写姓名 LIKE '李%',不能写姓名 = '李',那是精确匹配。BETWEEN '1999-01-01' AND '2000-12-31'是闭区间,两端都包含,日期类型直接用字符串字面量比较在 SQL Server 里能隐式转换,但更稳的写法是BETWEEN '1999-01-01' AND '2000-12-31'配合DATE类型列。

IN是等值判断的并列,课程号 IN ('C01','C02')等价于课程号 = 'C01' OR 课程号 = 'C02',题面写「选修了 C01 或 C02」,两种写法都对,但IN更好读。

判断空值只能用IS NULL,不能用= NULL。NULL 参与任何比较运算的结果都是 UNKNOWN 而不是 TRUE,先行课 = NULL永远筛不出任何行。「没有先行课的课程」必须写先行课 IS NULL,这是实验二第三题的全部考点。

-- 计算机系全体学生 SELECT 学号, 姓名, 性别, 出生日期, 院系名称, 备注 FROM 学生200905 WHERE 院系名称 = '计算机系'; -- 姓李的学生 SELECT 学号, 姓名 FROM 学生200905 WHERE 姓名 LIKE '李%'; -- 没有先行课的课程 SELECT 课程名 FROM 课程200905 WHERE 先行课 IS NULL; -- 有不及格记录的学号,DISTINCT 去掉一个学生多门挂科的重复 SELECT DISTINCT 学号 FROM 选修200905 WHERE 分数 < 60;

DISTINCT在第四题里不是装饰。一个学生可能挂了两门课,不去重的话学号会出现两次,和题面「学生的学号」的语义不符。什么时候该加 DISTINCT,判断标准是「结果的主语是实体还是记录」——问「哪些学生」就去重,问「哪些选课记录」就不去重。

3.3 聚合、GROUP BY 与 HAVING 的分工

聚合函数把多行压成一行,GROUP BY决定按什么维度压。COUNT(*)数行数,COUNT(列名)跳过该列为 NULL 的行,AVG(列名)的分母是「该列非 NULL 的行数」而不是总行数。这个差别在成绩统计里会直接导致平均分算错。

SELECT 列表里出现的非聚合列,必须全部出现在 GROUP BY 里,这是标准 SQL 的硬性要求。SQL Server 在默认配置下会宽松放过一些写法,但换到严格模式或者别的数据库就直接报错,实验阶段就按严格标准写,省得以后改。

-- 选修两门以上的学生 SELECT 学号, COUNT(*) AS 课程数 FROM 选修200905 GROUP BY 学号 HAVING COUNT(*) > 2; -- 选课门数大于等于 2 的学号、平均成绩、门数,按平均分降序 SELECT 学号, AVG(分数 * 1.0) AS 平均成绩, COUNT(*) AS 选课门数 FROM 选修200905 GROUP BY 学号 HAVING COUNT(*) >= 2 ORDER BY 平均成绩 DESC;

AVG(分数 * 1.0)里那个* 1.0是关键。分数列是SMALLINT,直接AVG在 SQL Server 里做的是整数除法,80 和 81 的平均会算成 80 而不是 80.5。乘 1.0 强制转成DECIMAL,小数部分才保得住。这种类型隐式转换导致的精度丢失,是实验报告里最不容易被发现、也最容易被扣分的细节。

两道题的措辞也值得对照:「选修了两门以上」和「选课门数大于等于 2」。中文里「以上」是否含本数本身有歧义,实验把两种表述并列摆出来,大概率就是想让你意识到>和>=的选择取决于题面对边界的定义,而不是随手写一个。复习时把这两句抄在一起对比,比背语法有用。

3.4 排序、TOP 与「字典序前两名」

「姓李的学生中姓名按字典顺序排序的前两个」这道题同时考了筛选、排序和限制行数三件事,顺序不能乱:先 WHERE 筛,再 ORDER BY 排,最后 TOP 取。

SELECT TOP 2 学号, 姓名 FROM 学生200905 WHERE 姓名 LIKE '李%' ORDER BY 姓名;

TOP 2必须和ORDER BY一起用才有意义。不带排序的 TOP 返回的是「碰巧先扫到的两行」,数据量一大结果就不可复现。

「字典序」这三个字还藏着一个坑:中文姓名的排序结果依赖数据库的排序规则。在按拼音排序的排序规则下,李娜排在李伟前面;换成按笔画或按 Unicode 码位排序,顺序可能反过来。实验环境里跑出来的结果和同学的不一致,八成不是谁写错了,而是排序规则不同。搞不清的时候可以查一下当前库的排序规则,心里有个数。

提示:年龄这类计算列不要写死年份。YEAR(GETDATE()) - YEAR(出生日期)是周岁近似算法,好处是随系统时间自动变,坏处是忽略了生日是否已过,只适合「粗略年龄」的场景,写报告时最好注明这一点。

4. 连接与子查询:从两表拼接到 EXISTS 嵌套

实验二的下半部分和整个实验三,考的是同一个能力的两个层次:多表连接是「把表横向拼起来再看」,嵌套子查询是「把一个查询的结果当成另一个查询的条件」。前者关注行怎么拼,后者关注条件怎么传。实验三还额外引入了集合运算,用 UNION、INTERSECT、EXCEPT 把两个结果集做交并差。

4.1 内连接的三种写法与它们的等价性

内连接只保留两边都能匹配上的行。同一个查询有三种常见写法:隐式连接把所有表塞进 FROM 用逗号隔开、条件写在 WHERE;显式INNER JOIN ... ON;以及用JOIN ... USING(SQL Server 不支持,属于方言差异,了解即可)。

-- 隐式连接 SELECT s.学号, s.姓名 FROM 学生200905 s, 选修200905 sc, 课程200905 c WHERE s.学号 = sc.学号 AND sc.课程号 = c.课程号 AND c.课程名 = '数据库原理' AND s.院系名称 = '计算机系'; -- 显式内连接,同一个结果 SELECT s.学号, s.姓名 FROM 学生200905 s JOIN 选修200905 sc ON s.学号 = sc.学号 JOIN 课程200905 c ON sc.课程号 = c.课程号 WHERE c.课程名 = '数据库原理' AND s.院系名称 = '计算机系';

两种写法结果一致,但隐式连接有一个致命弱点:一旦漏写连接条件,FROM 里的两张表会做笛卡尔积,行数瞬间爆炸,而且不报错。显式 JOIN 把连接条件和筛选条件分开,ON管怎么拼,WHERE管筛什么,漏写ON会立刻语法报错。三表以上的查询一律建议写显式 JOIN。

表别名在这里不是可选项。三张表都有学号或课程号,不写别名的话学号到底指哪张表的学号会引发「列名不明确」的错误,加了s.sc.c.前缀之后语义立刻清晰。

4.2 外连接:谁在左边谁不掉队

实验二有两道题专门考外连接:「所有学生的信息和他们的选课编号」和「所有课程的名字以及被选修情况」。题面里「所有」两个字就是信号——只要出现「所有 X 以及对应的 Y」,而某些 X 可能没有 Y,就必须用外连接,因为内连接会把没有匹配的 X 丢掉。

-- 所有学生,没选课的也要出现,课程号显示 NULL SELECT s.学号, s.姓名, s.性别, s.出生日期, s.院系名称, s.备注, sc.课程号 FROM 学生200905 s LEFT JOIN 选修200905 sc ON s.学号 = sc.学号; -- 所有课程及被选情况 SELECT c.课程名, sc.学号, sc.课程号, sc.分数 FROM 课程200905 c LEFT JOIN 选修200905 sc ON c.课程号 = sc.课程号;

LEFT JOIN保留左表全部行,右表匹配不上就补 NULL。反过来写RIGHT JOIN是保留右表,效果等价但可读性差,习惯上统一用左连接,把「必须全部出现」的那张表放左边。

外连接还有一个反向用法:查「没有 X 的 Y」。比如找没选任何课的学生,用左连接之后筛右表主键为 NULL:

SELECT s.学号, s.姓名 FROM 学生200905 s LEFT JOIN 选修200905 sc ON s.学号 = sc.学号 WHERE sc.学号 IS NULL;

这个套路叫反连接,写起来比NOT IN和NOT EXISTS直观,但要注意筛选条件必须写在 WHERE 里,写在 ON 里就变成了「连接时不管,但行还是要保留」,结果完全不同。

4.3 自连接:一条 SQL 查先行课的先行课

「查询每一门课的间接先行课」是实验二最有技术含量的一题。课程表里先行课是指向自己的外码,要拿「先行课的先行课」,只能把同一张表当成两张表用,起两个别名做连接。

SELECT c1.课程号, c1.课程名, c2.课程号 AS 间接先行课 FROM 课程200905 c1 JOIN 课程200905 c2 ON c1.先行课 = c2.课程号 WHERE c2.先行课 IS NOT NULL;

逻辑拆开看:c1是当前课程,c2是它的直接先行课,条件c1.先行课 = c2.课程号把两层关系接上;c2.先行课 IS NOT NULL保证c2自己也有先行课,这样筛出来的才是「间接」先行课,否则c2是链条起点,间接关系不存在。这条 WHERE 如果漏写,结果里会混进大量「直接先行课」的行,题面要求「有间接先行课才算」就落空了。

自连接的本质是给同一份数据赋予两个角色,别名就是角色名。理解了这一点,三层自连接(间接的间接)也就不难写,只是再加一个c3而已。

4.4 嵌套子查询:IN 与 EXISTS 的分工

子查询有两种形态。非相关子查询先独立算出一个结果集,再交给外层用;相关子查询则要逐行拿外层的值进去算,外层每一行都会触发一次内层执行。实验三的题目基本都在这两种里打转。

-- 非相关子查询:先查出数据库原理的课程号,再数选课人数 SELECT COUNT(*) AS 人数 FROM 选修200905 WHERE 课程号 = (SELECT 课程号 FROM 课程200905 WHERE 课程名 = '数据库原理'); -- 相关子查询:对每个学生,检查他有没有选过这门课 SELECT s.学号, s.姓名, s.性别, s.出生日期, s.院系名称, s.备注 FROM 学生200905 s WHERE NOT EXISTS ( SELECT 1 FROM 选修200905 sc WHERE sc.学号 = s.学号 AND sc.课程号 = (SELECT 课程号 FROM 课程200905 WHERE 课程名 = '数据库原理') );

NOT EXISTS的写法初看绕,但它是「没有选修」这类否定题的标准解法。内层子查询里的s.学号来自外层,这就是相关子查询的标记;SELECT 1只关心有没有行,不关心取什么值,写SELECT *也行,写 1 更清楚。语义是「对每个学生,如果找不到他的任何一条选课记录对应数据库原理,就把它留下」。

4.5 集合运算与三种等价改写

实验三的第六到第八题要求同一道题用两种方法实现,这个设计就是为了让你看见集合运算和嵌套子查询的等价关系。

-- 0602001 或 0602002 选过的课:UNION 版本 SELECT 课程号 FROM 选修200905 WHERE 学号 = '0602001' UNION SELECT 课程号 FROM 选修200905 WHERE 学号 = '0602002'; -- 同样结果,IN 版本 SELECT DISTINCT 课程号 FROM 选修200905 WHERE 学号 IN ('0602001', '0602002'); -- 两人共同选过的课:INTERSECT 版本 SELECT 课程号 FROM 选修200905 WHERE 学号 = '0602001' INTERSECT SELECT 课程号 FROM 选修200905 WHERE 学号 = '0602002'; -- EXISTS 版本 SELECT DISTINCT a.课程号 FROM 选修200905 a WHERE a.学号 = '0602001' AND EXISTS (SELECT 1 FROM 选修200905 b WHERE b.学号 = '0602002' AND b.课程号 = a.课程号); -- 0602001 选了但 0602002 没选:EXCEPT 版本 SELECT 课程号 FROM 选修200905 WHERE 学号 = '0602001' EXCEPT SELECT 课程号 FROM 选修200905 WHERE 学号 = '0602002'; -- NOT EXISTS 版本 SELECT DISTINCT a.课程号 FROM 选修200905 a WHERE a.学号 = '0602001' AND NOT EXISTS (SELECT 1 FROM 选修200905 b WHERE b.学号 = '0602002' AND b.课程号 = a.课程号);

UNION默认去重,加ALL才保留重复;INTERSECT和EXCEPT同样默认去重。集合运算左右两边的列数、列类型必须对得上,列名以左边为准。三层关系记法很直白:并集是「或」,交集是「且」,差集是「有我不有你」。

写法选择上有个经验:集合运算读起来最接近题面的中文表述,写报告时优先用它;嵌套子查询在需要额外关联条件时更灵活,尤其是差集,EXCEPT只能做整行比较,一旦还要带上别的字段就得退回NOT EXISTS。

4.6 全称量词:比计算机系学生年龄都小

「查询其他系中比计算机系学生年龄都小的学生」是实验三里最考思维的一道。SQL 没有「对所有」这个关键字,只能用等价变换绕过去。年龄小等价于出生日期晚,所以命题变成「出生日期晚于计算机系所有学生的出生日期」,也就是「晚于其中最大的那个」。

-- 写法一:和最大值比 SELECT 学号, 姓名, 性别, 出生日期, 院系名称, 备注 FROM 学生200905 WHERE 院系名称 <> '计算机系' AND 出生日期 > (SELECT MAX(出生日期) FROM 学生200905 WHERE 院系名称 = '计算机系'); -- 写法二:ALL 量词 SELECT 学号, 姓名, 性别, 出生日期, 院系名称, 备注 FROM 学生200905 WHERE 院系名称 <> '计算机系' AND 出生日期 > ALL (SELECT 出生日期 FROM 学生200905 WHERE 院系名称 = '计算机系');

两种写法等价,> ALL翻译过来就是「比每一个都大」,也就是大于最大值。对应的> ANY是「比某一个大大即可」,等价于大于最小值。这里还有一个容易忽略的细节:如果计算机系没有学生,子查询返回空集,MAX返回 NULL,整个条件变成 UNKNOWN,结果集为空——逻辑上「比所有人年纪都小」在空集上确实无法成立,结果是对的,但你得知道它为什么是空的。

<>也不能写成NOT IN,这一点在下一章会展开。实验里还留了个隐患:如果某个学生所在院系字段是 NULL,院系名称 <> '计算机系'对 NULL 求值结果是 UNKNOWN,这个学生会被无声地排除掉。

5. 易错点盘点:五个最容易翻车的地方

实验报告的代码部分通常都能跑通,扣分基本都扣在「结果不对但看起来对了」的细节上。下面这五处是这类问题的高发区,也是复习时最该反复回看的地方。

5.1 空值相关的两个陷阱

易漏点一:NOT IN 遇到 NULL 直接返回空集。

为什么漏:NOT IN的直觉读法是「不在这个列表里」,符合日常语义。但 SQL 的三值逻辑里,x NOT IN (a, b, NULL)会被展开成x <> a AND x <> b AND x <> NULL,最后那一项是 UNKNOWN,整个 AND 链条永远不为 TRUE,结果集直接变空,而且不报错。「查询没有选修数据库原理的学生」如果用「学号 NOT IN (子查询查出选了课的学生)」,只要子查询结果里混入一个 NULL,整道题就白做。

怎么补:否定类查询一律优先用NOT EXISTS,它对 NULL 免疫,因为 EXISTS 只判断「有没有行」,不比较具体值。真要用 NOT IN,就在子查询里加WHERE 列名 IS NOT NULL把空值挡掉。写完否定查询后,随手数一下行数,如果返回 0 行而你以为应该有几十行,第一反应就该怀疑空值。

易漏点二:聚合函数的分母和类型。

为什么漏:AVG(分数)看起来天经地义。但 AVG 会跳过 NULL 行,分母是「有分数的行数」而不是「选课门数」。一个学生选了 3 门课、其中 1 门没考,COUNT(*)是 3,AVG的分母是 2,两个数放在同一行结果里,读者会以为它们对应同一批行,其实不是。整数除法的坑同样隐蔽,SMALLINT求平均会被截断成整数,80.5 变成 80,肉眼几乎看不出来。

怎么补:平均分别和行数混排,要么统一用AVG(ISNULL(分数,0))把空值补零,要么在结果里同时给出COUNT(分数)和COUNT(*)让分母显式化。涉及小数的聚合统一乘 1.0 或者CAST(... AS DECIMAL(5,2)),别指望数据库自动提升类型。

5.2 连接与分组的三个陷阱

易漏点三:该用外连接的地方用了内连接。

为什么漏:内连接的语法更熟,写起来更顺手,而且大多数题目用内连接也能出结果,只是少了几行。少的那几行恰恰是「没选课的学生」「没被选过的课程」,题面里「所有」两个字被眼睛自动过滤掉了。

怎么补:读题时把「所有」「每一门」「包括没有……的」这类词圈出来,出现就用外连接,把必须全部保留的表放左边。写完对照一下左表行数,外连接的结果行数不应该少于左表行数(除非有 WHERE 条件二次过滤,那又是另一个坑)。

易漏点四:外连接的筛选条件写错位置。

为什么漏:LEFT JOIN ... ON 条件 AND 筛选条件和LEFT JOIN ... ON 条件 WHERE 筛选条件长得几乎一样,结果却差一个数量级。写在 ON 里,不匹配的右表行仍然会被 NULL 补出来;写在 WHERE 里,NULL 补出来的行会被条件筛掉,外连接退化成了内连接。

怎么补:记住一条规则——ON管「怎么拼」,WHERE管「拼完之后筛什么」。凡是针对右表字段的过滤,想清楚是要「拼之前排除」还是「拼之后排除」,前者放 ON,后者放 WHERE。反连接(WHERE 右表主键 IS NULL)必须放 WHERE,这个不能错。

易漏点五:GROUP BY 之后 SELECT 里塞了非分组列。

为什么漏:SQL Server 在某些兼容级别下会容忍这种写法,随便取分组里的第一行值返回,不报错。等到换数据库或者升级兼容级别,同样的语句直接报「列无效,因为该列没有包含在聚合函数或 GROUP BY 子句中」。

怎么补:SELECT 列表里的每一列,要么在 GROUP BY 里,要么被聚合函数包住,没有第三种。想显示学生姓名又要按学号分组,正确做法是先分组聚合、再 JOIN 回学生表取姓名,而不是直接在 SELECT 里写姓名。这个习惯从实验阶段就养成,能省掉以后大量排查时间。

5.3 建表与约束的一个陷阱

易漏点六:自参照外码和删除顺序。

为什么漏:课程表的先行课外码指向自己,CREATE TABLE里直接写会在建表过程中失败。同理,删除数据时先删父表再删子表会触发外码冲突。这两处报错信息都不太友好,初学时容易以为是语法问题,反复改语句改不到点上。

怎么补:自参照外码统一「先建表、后 ALTER」两步走。删除顺序统一「先子后父」,即先删选修、再删学生和课程。需要一次性清理时用事务包起来,中途报错就整体回滚,不会留下删了一半的脏数据。

6. 复习清单:一遍走完的查漏补缺路线

临近考试或者要交报告前,最有效的复习不是从头再看一遍讲义,而是按「题干关键词 → 该用什么写法 → 该检查什么」这条链路快速过一遍。下面这张对照表是我自己整理出来反复用的,把实验二、三里出现过的题型全部覆盖进去了。

题干里的信号词该用的写法交卷前检查什么
「所有」「每一门」「包括没有…的」LEFT JOIN,主表放左边结果行数是否不少于左表行数
「没有」「未」「不曾」NOT EXISTS是否误用了 NOT IN;结果是否意外为 0 行
「或」「至少一个」IN 或 UNIONUNION 是否需要 ALL 保留重复
「同时」「既…又…」INTERSECT 或 EXISTS两边的列数和类型是否对齐
「比…都」「全部」> ALL或与 MAX 比较子查询为空时的结果是否符合预期
「前 N 个」「最新」TOP N + ORDER BY排序字段是否唯一,结果是否可复现
「平均」「总数」「门数」聚合 + GROUP BY分母是 COUNT(*) 还是 COUNT(列);是否需要乘 1.0
「两门以上」「大于等于 2」HAVING>还是>=,是否与题面边界一致
「姓名按字典序」ORDER BY当前库的排序规则是拼音还是码位

过完表格,再花二十分钟把四类语句各手写一条,不看资料。写完之后逐条问自己三个问题:这条语句在做什么、如果我删掉其中一个子句结果会怎么变、这条语句在什么数据下会返回空集。第三个问题尤其管用,它逼你想清楚三值逻辑和连接语义,而不是靠记忆拼语法。

还有一个小习惯值得养成:每写完一道查询,先单独跑一遍它的子查询,看看返回几行、有没有 NULL。子查询是嵌套题里最容易出问题的一层,把它单独拎出来验证,比对着最终结果瞎猜快得多。我当初做「没有选修数据库原理的学生」那道题,外层语句改了七八遍都不对,最后发现是子查询里多返回了一行 NULL,把NOT IN整个废掉了。从那以后我每次写带否定语义的查询,都强制先把子查询单独跑一遍、确认没有空值再往外层套,这个动作后来救了我很多次。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 11:49:37

9款智能论文写作工具全解析:从选题到答辩

1. 项目背景与需求分析 本科毕业论文写作是每个大学生必须经历的重要环节&#xff0c;但许多学生面临时间紧张、写作经验不足、格式规范复杂等问题。传统论文写作需要经历选题、文献检索、框架搭建、内容撰写、格式调整等多个环节&#xff0c;整个过程往往耗时数周甚至数月。 …

作者头像 李华
网站建设 2026/10/31 11:48:33

拓扑图正交连线算法与工程实践

1. 拓扑图正交连线功能的核心价值 在可视化网络架构设计领域&#xff0c;正交连线&#xff08;Orthogonal Link&#xff09;是连接拓扑节点的黄金标准。与普通的直线连接相比&#xff0c;正交连线通过严格的水平和垂直折线路径&#xff0c;实现了三大核心优势&#xff1a; 视觉…

作者头像 李华
网站建设 2026/10/31 11:48:19

前缀和与差分数组:高效处理区间查询与修改

1. 前缀和与差分数组的核心概念 前缀和和差分数组是算法竞赛中两个密切相关的数据结构&#xff0c;它们通过巧妙的数学关系相互转化&#xff0c;能够高效处理区间查询和修改问题。 1.1 前缀和的定义与应用场景 前缀和&#xff08;Prefix Sum&#xff09;是一种预处理技术&…

作者头像 李华
网站建设 2026/10/31 11:47:27

UML图解析:软件工程中的可视化建模工具

1. UML图概述&#xff1a;软件工程的通用语言 统一建模语言&#xff08;UML&#xff09;是软件工程领域的事实标准&#xff0c;它用图形化的方式描述系统结构和行为。我第一次接触UML是在2005年参与银行系统重构时&#xff0c;当时项目组用类图梳理了上千个业务实体关系&#x…

作者头像 李华
网站建设 2026/10/31 11:47:10

AI如何帮助芯片工程师挖掘隐性设计知识

1. 芯片工程师如何从AI那里“榨出“隐性知识&#xff1f;在芯片设计这个高度专业化的领域&#xff0c;工程师们常常面临一个困境&#xff1a;如何获取那些教科书上找不到、资深工程师不愿轻易传授的"隐性知识"&#xff1f;这些知识可能是一个特定工艺节点的设计技巧&…

作者头像 李华
网站建设 2026/10/31 11:47:06

1/2.3型传感器如何实现60fps全分辨率?堆叠背照与列并行ADC解析

简介&#xff1a;这份PDF是索尼IMX577-AACK-C CMOS图像传感器的官方技术规格书&#xff0c;面向从事摄像头模组开发、图像算法调试与硬件选型的工程师&#xff0c;尤其适合需要评估1/2.3型12.3兆像素方案的专业摄影摄像、医疗影像、工业检测与安防监控项目。资源包仅含1个PDF文…

作者头像 李华