☰
数据库原理实验全链路:从建库建表到组合查询
2026/10/31 3:09:15 网站建设 项目流程

简介:《数据库原理》实验报告面向南华大学大数据技术及相关专业学生,服务于数据库课程实验、期末复习与实验报告撰写参考。内容围绕 SQL Server Management Studio 的使用展开,覆盖认识 DBMS、图形界面与 SQL 语句建库建表、主码外码与默认值设置、单表查询和多表连接查询、高级 SQL 查询以及数据插入更新删除等基本操作,能帮助读者理解表间一对多、多对多关系并掌握 SELECT、JOIN、分组排序等查询思路。资源以 1 个 doc 文档交付,压缩包约 5.38MB,结构按实验题目、实验要求、实验代码与总结组织,便于对照练习和查漏补缺。已有 377 人学习下载,适合需要参考完整实验流程、代码写法与报告框架的数据库初学者使用。

1. 一份数据库原理实验报告,真正值钱的是那条从建库到组合查询的链路

课程群里流转的数据库原理实验报告,多数人扫一眼就存进网盘吃灰,等到期末要交才翻出来。我拆的这份不太一样:它把四个实验按依赖顺序串成了一条可执行的链路——先在某高校实验室的 SQL Server 环境里用 SSMS 点出库和表,再用 T-SQL 把同样的结构写一遍,接着是单表十题、多表八题、嵌套与组合六题,最后收在 INSERT/UPDATE/DELETE 上。对刚接触数据库的大数据技术专业学生,它是一份能照着敲的作业骨架;对已经工作、SQL 写得半生不熟的人,它是一份把主外码、连接、子查询这些概念重新对齐的清单。下面按我复现时的顺序拆,重点讲清楚每步为什么这么做、参数怎么改、哪里容易翻车。

2. 建库建表:三张表的主外码、默认值与图形化/脚本两条路

2.1 先把主码外码和默认值定死,再动手建表

实验一给了三张表的字段清单,看起来简单,但真正决定后面查询能不能跑通的,是建表阶段这几个约束有没有定对。学生表的主码是学号,课程表的主码是课程号,选修表的主码是(学号,课程号)这个组合——注意它不是单独一列,而是两列联合主码,这一点如果建错,后面“一个学生选同一门课两次”就拦不住。

外码方向也要先想清楚:选修表的学号引用学生表,课程号引用课程表,所以建表顺序必须是学生、课程在前,选修在后。反过来先建选修表,外码约束会直接报错。默认值这块,性别给'男'、学分给4,是实验要求里明确写的;实际做的时候我建议把默认值只加在确实有业务含义的列上,别给出生日期也塞一个默认值,否则插入脏数据时反而掩盖问题。

数据类型的选择有个容易忽略的点:学号用CHAR(8)还是VARCHAR(10),取决于学号是否定长。这份报告里学号形如0602001,定长 7 位,用CHAR(7)更省空间也更快比较;但如果后续要兼容带字母的学号,VARCHAR(10)更稳。我一般会按“当前数据定长就用 CHAR,可能变长就用 VARCHAR”来选,不追求一步到位。

2.2 SSMS 图形化建库建表的操作顺序

图形化那条路适合第一次接触 SQL Server 的人建立空间感,但顺序错了会反复返工。我复现时的操作顺序是这样的:

第一步,对象资源管理器里右键“数据库”→“新建数据库”,名称填学生选课200905,文件路径保持默认,点确定。这里不建议改文件增长方式,课程实验的数据量用默认配置完全够。

第二步,展开新建的库,右键“表”→“新建表”,在列编辑器里逐列填名称、类型、是否允许 Null。填完后选中“学号”那一行,点工具栏的主键按钮;如果是联合主键,按住 Ctrl 同时选中两列再点主键。

第三步,保存表时 SSMS 会弹窗让你填表名,填学生200905。这里有个坑:表名以中文开头完全没问题,但如果表名里带空格或特殊符号,后续写 SQL 就必须用方括号包起来,不熟练的人很容易在这里卡住。

第四步,建表间关系不要靠手写外码,走“数据库关系图”更直观:右键“数据库关系图”→“新建”,把三张表加进来,从学生表的学号拖到选修表的学号上,弹窗里确认主键表和外码表,点确定。课程表同理。关系图保存时如果提示“缺少表”,说明还有表没加进来,补上再存。

图形化建完之后,强烈建议右键数据库→“任务”→“生成脚本”,把整个库的结构导成 .sql 文件。这份脚本就是你后面写 T-SQL 版本的底稿,比对着界面手敲快得多。

2.3 用 T-SQL 复现 StuCou 库:建库、建表、插数据

实验一的后半段要求用 SQL 语句再建一个StuCou200905库,结构和前面一样。我习惯把整个脚本写成可重复执行的形式,开头先判断库是否存在,存在就删掉重建,避免反复调试时残留旧表。

-- 可重复执行:库已存在则先删除 IF DB_ID('StuCou200905') IS NOT NULL DROP DATABASE StuCou200905; GO CREATE DATABASE StuCou200905; GO USE StuCou200905; GO -- 先建被引用的学生表 CREATE TABLE dbo.学生200905 ( [学号] CHAR(7) NOT NULL, [姓名] VARCHAR(20) NOT NULL, [性别] CHAR(2) NOT NULL CONSTRAINT DF_学生_性别 DEFAULT '男', [出生日期] DATE NULL, [院系名称] VARCHAR(30) NULL, [备注] VARCHAR(50) NULL, CONSTRAINT PK_学生200905 PRIMARY KEY ([学号]) ); GO -- 课程表:先行课允许为空,表示没有先修要求 CREATE TABLE dbo.课程200905 ( [课程号] CHAR(3) NOT NULL, [课程名] VARCHAR(30) NOT NULL, [先行课] CHAR(3) NULL, [学分] SMALLINT NOT NULL CONSTRAINT DF_课程_学分 DEFAULT 4, CONSTRAINT PK_课程200905 PRIMARY KEY ([课程号]) ); GO -- 选修表:联合主码 + 两个外码 CREATE TABLE dbo.选修200905 ( [学号] CHAR(7) NOT NULL, [课程号] CHAR(3) NOT NULL, [分数] SMALLINT NULL, CONSTRAINT PK_选修200905 PRIMARY KEY ([学号], [课程号]), CONSTRAINT FK_选修_学生 FOREIGN KEY ([学号]) REFERENCES dbo.学生200905([学号]), CONSTRAINT FK_选修_课程 FOREIGN KEY ([课程号]) REFERENCES dbo.课程200905([课程号]) ); GO

这段脚本里有几个参数值得单独说。CHAR(7)对应报告里 7 位学号,如果换成VARCHAR(10)也不会报错,但联合主码的长度会影响索引大小,课程实验数据量小感知不到,真实项目里要算一下。CONSTRAINT DF_学生_性别 DEFAULT '男'给默认值起了名字,好处是后续想删默认值可以直接DROP CONSTRAINT DF_学生_性别,不用去查系统视图。外码没有写ON DELETE CASCADE,这是故意的——实验阶段先看清约束报错,比让级联删除悄悄带走数据更有教学价值。

插数据这一步,报告里给了三张表的示例数据,我建议按学生、课程、选修的顺序插,同样是因为外码。分数列允许 NULL,用来表示“选了但还没出成绩”,这个设计在后面统计平均分时会带出一个坑,第 5 章会讲。

-- 按外码依赖顺序插入 INSERT INTO dbo.学生200905 ([学号],[姓名],[性别],[出生日期],[院系名称],[备注]) VALUES ('0602001','李雷','男','1999-03-12','计算机系',NULL), ('0602002','韩梅梅','女','2000-07-08','计算机系',NULL), ('0602003','王强','男','1998-11-23','数学系',NULL); GO INSERT INTO dbo.课程200905 ([课程号],[课程名],[先行课],[学分]) VALUES ('C01','数据库原理',NULL,4), ('C02','数据结构','C01',4), ('C03','操作系统','C02',3); GO INSERT INTO dbo.选修200905 ([学号],[课程号],[分数]) VALUES ('0602001','C01',88),('0602001','C02',76), ('0602002','C01',92),('0602002','C03',NULL), ('0602003','C02',55); GO

插入完立刻跑一句SELECT * FROM dbo.选修200905;确认行数和分数列都对。如果这里就发现分数成了 NULL 或者行数少一条,八成是外码拦住了插入,去检查学号、课程号是不是在被引用表里存在。

2.4 数据更新:INSERT、UPDATE、DELETE 的边界

实验四的数据更新在报告里篇幅不长,但它是后面所有查询的“数据源维护”环节。INSERT 已经在 2.3 里用过,重点说 UPDATE 和 DELETE 的两个边界。

UPDATE 最容易翻车的是漏写 WHERE。UPDATE 选修200905 SET 分数 = 分数 + 5;会把全表所有分数都加 5,跑之前最好先用同样的 WHERE 跑一遍 SELECT 看命中几行。我一般的习惯是先写SELECT * FROM ... WHERE ...,确认结果集没问题,再把 SELECT 换成 UPDATE,WHERE 原样保留。

DELETE 的边界在外码上。如果直接DELETE FROM 学生200905 WHERE 学号='0602001';,而选修表里还有这个学生的选课记录,SQL Server 会报外码冲突,拒绝删除。这是参照完整性在起作用,不是 bug。想删就得先删选修表里的子记录,或者建外码时声明级联删除——课程实验阶段我建议保留报错,让你亲眼看到约束在拦什么。

-- 先看命中范围,再动手 SELECT * FROM dbo.选修200905 WHERE [学号]='0602001' AND [课程号]='C02'; UPDATE dbo.选修200905 SET [分数] = 82 WHERE [学号]='0602001' AND [课程号]='C02'; -- 删除子记录后再删主记录 DELETE FROM dbo.选修200905 WHERE [学号]='0602003'; DELETE FROM dbo.学生200905 WHERE [学号]='0602003';

这两条 DELETE 的顺序不能反,反了就是外码报错。实际项目里我更倾向于用逻辑删除(加一个is_deleted列)而不是物理 DELETE,但课程实验要求练物理删除,照做就行。

3. 单表查询十题:WHERE 骨架、LIKE 通配与聚合的边界

3.1 单表查询的固定骨架

单表查询的十道题看起来五花八门,拆开看都是同一个骨架:SELECT 列清单 FROM 表 WHERE 行过滤 GROUP BY 分组 HAVING 组过滤 ORDER BY 排序。区别只在于哪些子句出现、哪些省略。

我在复现时先把十道题按“考什么”分了四类:条件过滤(第 1、2、3、4、5、7 题)、表达式计算(第 6 题年龄)、排序取前 N(第 8 题)、分组聚合(第 9、10 题)。分类之后写起来就不容易乱,因为每一类对应的子句位置是固定的——WHERE 里不能出现聚合函数,聚合条件必须挪到 HAVING,这是新手最常犯的语法错误。

3.2 十道题逐条拆解与代码

第 1 题查计算机系全体学生信息,是最直白的等值过滤。注意报告里列了六个字段,实际写的时候用SELECT *也行,但实验报告里最好把列名写全,答辩时显得你知道自己在取什么。

-- 1. 计算机系全体学生 SELECT [学号],[姓名],[性别],[出生日期],[院系名称],[备注] FROM dbo.学生200905 WHERE [院系名称] = '计算机系';

第 2 题姓“李”,用LIKE '李%'。这里有个中文通配的细节:%匹配任意长度,_匹配单个字符。如果写成LIKE '李_',就只匹配“李”加一个字的姓名,两个字的名字会被漏掉。另外 SQL Server 里字符串比较默认不区分大小写,但中文没有大小写问题,真正要注意的是尾随空格——CHAR类型会自动补空格,比较时可能和预期不一致,所以姓名列我用了VARCHAR而不是CHAR。

-- 2. 姓李的学生 SELECT [学号],[姓名] FROM dbo.学生200905 WHERE [姓名] LIKE '李%'; -- 3. 没有先行课的课程 SELECT [课程名] FROM dbo.课程200905 WHERE [先行课] IS NULL;

第 3 题的关键是IS NULL不能写成= NULL。SQL 里 NULL 表示未知,任何和 NULL 的等值比较结果都是 UNKNOWN,不会返回行。这是血泪经验里排前三的坑,后面第 5 章还会再提一次。

第 4 题查有不及格的学生学号,重点是DISTINCT。一个学生可能有多门不及格,不去重的话学号会重复出现。

-- 4. 有不及格记录的学生(去重) SELECT DISTINCT [学号] FROM dbo.选修200905 WHERE [分数] < 60; -- 5. 选修 C01 或 C02 的学生 SELECT [学号],[分数] FROM dbo.选修200905 WHERE [课程号] IN ('C01','C02');

第 5 题的IN等价于两个OR,但可读性更好,课程号多了也不容易漏括号。注意IN列表里如果有 NULL,行为会变得微妙,实际项目里我会避免在 IN 列表里放 NULL。

第 6 题查计算机系学生姓名及年龄,年龄是算出来的,不是存出来的。SQL Server 里常用DATEDIFF(YEAR, 出生日期, GETDATE()),但它算的是“跨了多少个年份边界”,不是精确周岁。比如 1999-12-31 出生,在 2000-01-01 这天算出来是 1 岁,实际才 1 天。课程实验用这个够用,但要知道它的偏差方向。

-- 6. 姓名及年龄(按年份差,非精确周岁) SELECT [姓名], DATEDIFF(YEAR, [出生日期], GETDATE()) AS 年龄 FROM dbo.学生200905 WHERE [院系名称] = '计算机系'; -- 7. 1999—2000 年出生的计算机系学生 SELECT [姓名] FROM dbo.学生200905 WHERE [院系名称] = '计算机系' AND [出生日期] BETWEEN '19990101' AND '20001231';

第 7 题的日期字面量我用了'19990101'这种紧凑格式,而不是'1999-01-01'。原因是后者在某些语言设置下会被解析成“1999 年 1 月 1 日”还是“1 月 1 日 1999 年”取决于会话的DATEFORMAT,紧凑格式没有歧义。BETWEEN 的边界是闭区间,'20001231'当天的记录会被包含进来,如果出生日期列带时间部分,'20001231'会漏掉当天 00:00 之后的记录——课程实验里出生日期是纯 DATE,没这个问题,但换成 DATETIME 就要改成< '20010101'。

第 8 题姓李按字典序取前两个,SQL Server 里用TOP 2配合ORDER BY。这里有个玄学点:中文姓名的“字典顺序”取决于排序规则(collation),默认安装通常是Chinese_PRC_CI_AS,按拼音排;如果服务器排序规则是别的,结果可能不同。实验报告里不会写这一层,但换机器跑出不一样结果时,先查SELECT SERVERPROPERTY('Collation');。

-- 8. 姓李的学生按姓名排序取前两个 SELECT TOP 2 [学号],[姓名] FROM dbo.学生200905 WHERE [姓名] LIKE '李%' ORDER BY [姓名]; -- 9. 选修两门以上课程的学生 SELECT [学号], COUNT(*) AS 课程数 FROM dbo.选修200905 GROUP BY [学号] HAVING COUNT(*) > 2; -- 10. 选课数 >= 2 的学号、平均分、门数,按平均分降序 SELECT [学号], AVG([分数]) AS 平均成绩, COUNT(*) AS 选课门数 FROM dbo.选修200905 GROUP BY [学号] HAVING COUNT(*) >= 2 ORDER BY AVG([分数]) DESC;

第 9 题报告原文写的是“两门以上”,我按> 2处理;第 10 题写的是“大于等于 2”,用>= 2。这两个条件差一个等号,结果集会差一批人,抄作业时最容易在这里对不上答案。

3.3 年龄计算、日期区间和 TOP 的坑

年龄那题除了DATEDIFF的边界偏差,还有一个隐藏问题:如果出生日期是 NULL,DATEDIFF返回 NULL,这一行会出现在结果里但年龄列是空。实验数据里没有 NULL 出生日期,所以看不出来,但真实数据里要决定是过滤掉还是显示为空。

日期区间的坑集中在BETWEEN的闭区间语义和字面量格式上。我的习惯是:纯 DATE 列用BETWEEN '起始日' AND '结束日',带时间的列一律改成>= 起始日 AND < 次日,避免“结束日当天 00:00 之后的记录被漏掉”这种经典翻车。

TOP 的坑在于它必须和ORDER BY一起用才有确定结果。SELECT TOP 2 * FROM 学生;语法上合法,但返回哪两行取决于执行计划,今天跑和明天跑可能不一样。实验报告里第 8 题明确要求按字典序,所以ORDER BY不能省。另外TOP是 SQL Server 的方言,换成 MySQL 要写LIMIT,换成 Oracle 要写FETCH FIRST,跨库迁移时这是第一批要改的语法。

4. 多表连接与嵌套子查询:从 JOIN 到 EXISTS 的八加三题

4.1 连接的本质:笛卡尔积怎么被 ON 收窄

多表连接的八道题,很多人在第一题就卡住,原因是没想明白连接到底在做什么。两张表做连接,第一步是把左表的每一行和右表的每一行两两组合,得到笛卡尔积;第二步是用ON里的条件把不满足的行过滤掉。所以ON写漏了,结果行数就是两张表行数的乘积,数据量一大直接卡死查询窗口。

理解这个之后,内连接和外连接的区别就清楚了:内连接只保留ON匹配上的组合;左外连接保留左表全部行,右表没匹配上的列补 NULL。实验报告第 5、6 题要“所有学生的信息”和“所有课程的名字”,用的就是左外连接,因为要包含没选课的学生和没被选的课程。

三表连接可以看成两次两两连接:学生和选修先连,结果再和课程连。写的时候我习惯把连接条件紧跟在对应的JOIN后面,而不是全堆在 WHERE 里,这样内连接和外连接混用时不容易把外连接写成内连接。

4.2 八道多表题逐条拆解

第 1 题查选修数据库原理的计算机系学生,需要三张表:学生表过滤院系,课程表过滤课程名,选修表做桥接。

-- 1. 选修“数据库原理”的计算机系学生 SELECT s.[学号], s.[姓名] FROM dbo.学生200905 s JOIN dbo.选修200905 sc ON s.[学号] = sc.[学号] JOIN dbo.课程200905 c ON sc.[课程号] = c.[课程号] WHERE s.[院系名称] = '计算机系' AND c.[课程名] = '数据库原理';

第 2 题查间接先行课,是自连接的典型。课程表自己和自己连:一门课的先行课是另一门课,先行课的先行课再连一次。报告要求“有间接先行课才算”,所以两次连接都用内连接,天然过滤掉没有先行课的课程。

-- 2. 间接先行课:课程 -> 先行课 -> 先行课的先行课 SELECT a.[课程号] AS 课程, c.[课程号] AS 间接先行课 FROM dbo.课程200905 a JOIN dbo.课程200905 b ON a.[先行课] = b.[课程号] JOIN dbo.课程200905 c ON b.[先行课] = c.[课程号];

第 3 题查学号、姓名、课程名、成绩,是最标准的三表内连接。第 4 题查选修了课程的学生姓名,可以用DISTINCT去重,也可以用EXISTS,两种写法结果一样,执行计划在数据量小时差别不大。

-- 3. 学号、姓名、课程名、成绩 SELECT s.[学号], s.[姓名], c.[课程名], sc.[分数] FROM dbo.学生200905 s JOIN dbo.选修200905 sc ON s.[学号] = sc.[学号] JOIN dbo.课程200905 c ON sc.[课程号] = c.[课程号]; -- 4. 选修了课程的学生姓名(EXISTS 写法) SELECT s.[姓名] FROM dbo.学生200905 s WHERE EXISTS ( SELECT 1 FROM dbo.选修200905 sc WHERE sc.[学号] = s.[学号] );

第 5 题要所有学生信息加课程编号,包括没选课的学生,必须用左外连接。这里有个细节:LEFT JOIN之后没选课的学生课程号是 NULL,如果只要学生信息、课程号可有可无,这样写没问题;如果后续还要对课程号做过滤,过滤条件不能写在 WHERE 里,否则外连接会被退化成内连接——这是第 5 章要展开的坑之一。

-- 5. 所有学生 + 所选课程编号(保留没选课的学生) SELECT s.[学号], s.[姓名], s.[性别], s.[出生日期], s.[院系名称], s.[备注], sc.[课程号] FROM dbo.学生200905 s LEFT JOIN dbo.选修200905 sc ON s.[学号] = sc.[学号]; -- 6. 所有课程 + 被选情况(保留没被选的课程) SELECT c.[课程名], sc.[学号], sc.[课程号], sc.[分数] FROM dbo.课程200905 c LEFT JOIN dbo.选修200905 sc ON c.[课程号] = sc.[课程号];

第 7 题“所有可能的选修情况”是个容易看漏的题:它要的是学生和课程的笛卡尔积,不管实际选没选。用CROSS JOIN最直白,或者省略连接条件让两张表直接逗号分隔。数据量大时笛卡尔积很危险,但课程实验里学生和课程都只有几行,没问题。

-- 7. 所有学生 × 所有课程的组合 SELECT s.[学号], s.[姓名], c.[课程号], c.[课程名] FROM dbo.学生200905 s CROSS JOIN dbo.课程200905 c;

第 8 题查计算机系选课数大于 2 的学生,是“连接 + 分组 + HAVING”的组合。注意COUNT(*)数的是选课记录行数,如果一个学生同一门课选了两次(联合主码保证不会),这里会重复计数;用COUNT(DISTINCT sc.[课程号])更稳。

-- 8. 计算机系选课数 > 2 的学生 SELECT s.[姓名], AVG(sc.[分数]) AS 平均成绩, COUNT(*) AS 选课门数 FROM dbo.学生200905 s JOIN dbo.选修200905 sc ON s.[学号] = sc.[学号] WHERE s.[院系名称] = '计算机系' GROUP BY s.[学号], s.[姓名] HAVING COUNT(*) > 2 ORDER BY AVG(sc.[分数]) DESC;

4.3 自连接查间接先行课

第 2 题的自连接值得单独说两句,因为它是八道题里唯一一道“同一张表出现三次”的。三次引用必须用三个不同的别名,否则 SQL Server 会报“对象名重复”。别名我用了 a、b、c,对应“课程本身、直接先行课、间接先行课”三层。

写自连接时最容易犯的错是把连接方向搞反。a.[先行课] = b.[课程号]表示“a 的先行课是 b 这门课”,方向反了写成b.[先行课] = a.[课程号],语义就变成“a 是 b 的先行课”,结果集完全不同。验证方法是拿一门已知有间接先行课的课程手动推一遍:C03 的先行课是 C02,C02 的先行课是 C01,所以 C03 的间接先行课应该是 C01。跑完 SQL 对一下,对不上就是方向反了。

另外,如果一门课只有直接先行课、没有间接先行课,两次内连接之后这一行会被过滤掉,正好符合题目“有间接先行课才算”的要求。如果题目改成“所有课程及其间接先行课”,就要把第二次连接改成左外连接。

4.4 嵌套子查询:IN、EXISTS、比较运算符的选型

多表连接之外,实验二里还混了几道嵌套子查询的题,比如“查询其他系中比计算机系学生年龄都小的学生”。这道题的关键词是“都小”,对应ALL或MAX:比计算机系所有学生年龄都小,等价于出生日期比计算机系最晚出生的那个还晚。

-- 比计算机系所有学生都小(出生日期更晚) SELECT [学号],[姓名],[出生日期],[院系名称] FROM dbo.学生200905 WHERE [院系名称] <> '计算机系' AND [出生日期] > ( SELECT MAX([出生日期]) FROM dbo.学生200905 WHERE [院系名称] = '计算机系' );

IN、EXISTS、ANY/ALL这三类子查询的选型,我的经验是:子查询结果是小列表就用IN,关心“存在性”就用EXISTS,做全称比较就用ALL或聚合函数改写。EXISTS在子查询列上不关心返回什么,SELECT 1就够,写SELECT *也不会更慢,但可读性差一些。NOT IN要特别小心:如果子查询结果里含 NULL,整个NOT IN会返回空结果集,这是 SQL 三值逻辑的经典陷阱,改成NOT EXISTS更安全。

5. 避坑排查:五个现象、五个原因、五个解法

5.1 五个翻车现场

现象一:建表时报“对象名无效”或外码引用失败。原因几乎都是建表顺序错了,先建了选修表,而学生表还不存在。解法是把脚本按“学生→课程→选修”的顺序排,或者先建三张表不加外码,最后用ALTER TABLE ... ADD CONSTRAINT统一补外码。我一般用后者,因为外码单独一段,报错时一眼能看出是哪张表的问题。

现象二:WHERE 先行课 = NULL查出来零行。原因是 NULL 不能用等号比较,任何= NULL的结果都是 UNKNOWN。解法是改成IS NULL;反过来“不为空”用IS NOT NULL,不能写<> NULL。这个坑在实验二第 3 题就会出现,抄代码时如果照抄了= NULL,结果集是空的但不会报错,最容易被忽略。

现象三:左外连接写成内连接,没选课的学生消失了。现象是查询结果比预期少几行。原因是把右表的过滤条件写进了WHERE,比如WHERE sc.[分数] > 60,而右表没匹配上的行分数是 NULL,NULL 参与比较返回 UNKNOWN,整行被过滤掉,外连接退化成内连接。解法是把右表的过滤条件挪到ON里:LEFT JOIN 选修 sc ON s.学号 = sc.学号 AND sc.分数 > 60。这个区别在实验二第 5、6 题里体现得最明显。

现象四:分组统计平均分,结果偏高或偏低。原因是AVG([分数])会自动忽略 NULL 行,而COUNT(*)会把 NULL 行算进去。选修表里如果有一条“选了但没出成绩”的记录(分数为 NULL),AVG的分母是剔除了这条的,COUNT(*)却把它算进门数,两个数字对不上。解法是统一口径:要么用AVG(ISNULL([分数],0))把 NULL 当 0 处理(业务上不一定合理),要么用COUNT([分数])只数有成绩的记录。我一般会在报告里写清楚口径,避免答辩时被追问。

现象五:日期字面量'1999-1-1'在某些机器上解析失败或解析成错误日期。原因是DATEFORMAT会话设置不同,yyyy-mm-dd在大多数配置下没问题,但yyyy-m-d这种不补零的写法在部分语言设置下会歧义。解法是统一用'19990101'紧凑格式,或者用CONVERT(DATE, '1999-01-01', 120)显式指定格式。这条经验在跨机器交作业时特别有用,同一份脚本在自己电脑上跑得好好的,换到实验室机器就报错,八成是日期格式。

5.2 用校验 SQL 定位问题

出问题之后,别急着改代码,先用几条校验 SQL 把数据摸清楚。表行数、NULL 分布、外码孤儿记录,这三类检查能覆盖大部分“结果不对”的场景。

-- 三张表各有多少行 SELECT '学生' AS 表名, COUNT(*) AS 行数 FROM dbo.学生200905 UNION ALL SELECT '课程', COUNT(*) FROM dbo.课程200905 UNION ALL SELECT '选修', COUNT(*) FROM dbo.选修200905; -- 分数为 NULL 的记录(影响 AVG 和 COUNT 的口径) SELECT * FROM dbo.选修200905 WHERE [分数] IS NULL; -- 外码孤儿:选修表里有没有学生表不存在的学号 SELECT sc.[学号] FROM dbo.选修200905 sc LEFT JOIN dbo.学生200905 s ON sc.[学号] = s.[学号] WHERE s.[学号] IS NULL;

第三段“外码孤儿”查询在外码约束存在时应该永远返回空,返回非空说明约束被禁用了或者数据是绕过约束插进去的。课程实验里一般不会出现,但养成这个检查习惯,真实项目里能提前发现数据质量问题。

5.3 建库阶段的三个隐性约束

除了上面五个现象,建库阶段还有三个不报错但会影响后续的隐性约束,值得单独记一笔。

第一个是排序规则。库级排序规则决定了字符串比较和排序行为,建库时如果不指定,继承服务器默认。中文姓名排序、LIKE匹配在不同排序规则下可能有细微差别。课程实验不用改,但如果换机器后第 8 题结果不一样,先查排序规则。

第二个是NULL与空字符串的区别。VARCHAR列里''和NULL是两回事,''参与IS NULL判断为假,参与COUNT(列)会被计数。备注列如果允许为空,插入时到底写NULL还是'',要在报告里统一。

第三个是标识列。实验里的学号、课程号都是业务主键,不是自增列,所以建表时不要顺手加IDENTITY(1,1)。加了之后插入数据必须省略该列,和报告里的 INSERT 语句对不上,会报“列名或所提供值的数目与表定义不匹配”。

6. 组合查询的等价改写与结果验收:UNION、INTERSECT、EXCEPT 怎么对拍

实验三的后三道题,本质是同一个需求用两种写法实现,正好拿来练“等价改写”的手感。第 4 题查被 0602001 或 0602002 选修的课程号,UNION和IN两种写法:

-- UNION 写法:两个结果集合并去重 SELECT [课程号] FROM dbo.选修200905 WHERE [学号]='0602001' UNION SELECT [课程号] FROM dbo.选修200905 WHERE [学号]='0602002'; -- IN 写法:一次扫描 SELECT DISTINCT [课程号] FROM dbo.选修200905 WHERE [学号] IN ('0602001','0602002');

第 5 题查两人同时选修的课程号,INTERSECT和EXISTS两种写法。INTERSECT自带去重,EXISTS写法要确认外层去重,否则同一课程号可能出现多次。

-- INTERSECT:交集 SELECT [课程号] FROM dbo.选修200905 WHERE [学号]='0602001' INTERSECT SELECT [课程号] FROM dbo.选修200905 WHERE [学号]='0602002'; -- EXISTS:相关子查询 SELECT DISTINCT a.[课程号] FROM dbo.选修200905 a WHERE a.[学号]='0602001' AND EXISTS ( SELECT 1 FROM dbo.选修200905 b WHERE b.[学号]='0602002' AND b.[课程号]=a.[课程号] );

第 6 题查被 0602001 选但没被 0602002 选的课程号,EXCEPT和NOT EXISTS两种写法。EXCEPT是差集,注意顺序:A EXCEPT B是“在 A 不在 B”,反过来结果不同。NOT EXISTS写法里,子查询的关联条件不能漏,漏了就成了“0602002 什么都没选”时返回全部。

-- EXCEPT:差集,顺序敏感 SELECT [课程号] FROM dbo.选修200905 WHERE [学号]='0602001' EXCEPT SELECT [课程号] FROM dbo.选修200905 WHERE [学号]='0602002'; -- NOT EXISTS 等价写法 SELECT DISTINCT a.[课程号] FROM dbo.选修200905 a WHERE a.[学号]='0602001' AND NOT EXISTS ( SELECT 1 FROM dbo.选修200905 b WHERE b.[学号]='0602002' AND b.[课程号]=a.[课程号] );

组合查询的硬性约束是列数、列顺序、数据类型要一致。UNION两边一个查课程号、一个查学号,列数对上了但类型都是 CHAR,SQL Server 不会报错,结果却是错的。我的验收习惯是每组两种写法各跑一遍,把结果贴到两个临时表里做EXCEPT双向比对,两边都为空才算等价:

-- 等价性对拍:两个结果集互相 EXCEPT,都为空则一致 SELECT [课程号] FROM dbo.选修200905 WHERE [学号]='0602001' EXCEPT SELECT [课程号] FROM dbo.选修200905 WHERE [学号]='0602002' EXCEPT SELECT [课程号] FROM dbo.选修200905 WHERE [学号]='0602001' AND [课程号] NOT IN (SELECT [课程号] FROM dbo.选修200905 WHERE [学号]='0602002');

这段对拍 SQL 本身不追求业务正确,只用来验证“两种写法结果是否相同”。从那以后我每次交实验报告前,都会把关键题目的两种写法跑一遍对拍,结果不一致就回头查 NULL 和去重,而不是盯着屏幕肉眼比对几十行——肉眼比对漏一行,答辩时被指出来是真的尴尬。希望帮到你。

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

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

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

立即咨询