简介南华大学数据库原理实验报告围绕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 ... USINGSQL 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」。比如找没选任何课的学生用左连接之后筛右表主键为 NULLSELECT 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(*)是 3AVG的分母是 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整个废掉了。从那以后我每次写带否定语义的查询都强制先把子查询单独跑一遍、确认没有空值再往外层套这个动作后来救了我很多次。希望帮到你。本文还有配套的精品资源点击获取