简介:太原理工大学软件工程专业《数据库概论》课程实验报告,适合正在学习数据库原理与SQL实践的高校学生参考。报告基于Microsoft SQL Server 2016,完整覆盖交互式SQL语句使用、数据完整性、用户鉴别与数据控制等实验环节,包含创建/修改/删除数据表、设置主外键与唯一约束、建立普通/唯一/聚簇索引、创建视图以及数据插入和更新等完整操作过程,并配有具体SQL语句和运行结果,可直接对照复现数据库管理关键流程。报告按照实验目的、实验平台、主要实验内容及结果进行组织,便于按步骤对照练习。压缩包内共1个docx文档(1.69MB),为完整实验报告文件,下载后即可阅读或打印。该资源已有311人学习,适合期末复习、课程实验备查以及SQL实操练习,尤其对太原理工大学相关课程学生具有直接参考价值。
1. 数据库概论实验报告:一份能从上机到避坑的 SQL Server 实操地图
这份实验报告是我见过的少数「知识点覆盖密度」很高的数据库课程作业。它不跟你讲抽象理论,直接把 SQL Server 2016 里建表、加约束、建索引、建视图、增删改查、分组统计、连接查询、嵌套查询、集合运算、完整性验证全部拆成一条条可执行的 SQL,连出错时的报错点都保留在文本里。对软件工程专业刚开数据库课的人来说,它是很好的语法速查表;对准备实验考试的人来说,它是一份能照着跑的真题。更难得的是,这份报告里留了不少新手必踩的坑——表名拼错、列名混用、外键顺序不对、约束放行 NULL,每条我都帮你标出来。
2. 建库建表与数据定义:主键、外键、CHECK、DEFAULT 的排布与索引视图
数据库实验的第一步永远是 DDL,而这份报告第一部分的主题就是「创建、修改、删除数据库对象」。很多人觉得建表简单,真正动手才发现约束怎么写、外键引用哪个表、索引要不要建 UNIQUE,全是细节。这一章把报告中数据定义部分拆开讲。
2.1 从 CREATE TABLE 开始:主键、外键与 NOT NULL 怎么排布
报告中一共出现了两套建表语句,第一套在实验二里,只带主键;第二套在实验三里,约束更全。我建议直接看第二套,因为它把一张业务表该有的约束都列出来了:
CREATE TABLE Student( Sno CHAR(8) PRIMARY KEY, Sname CHAR(8) NOT NULL, Ssex CHAR(2) CHECK(Ssex IN ('男','女')), Sage SMALLINT, Sdept CHAR(20), Sclass CHAR(4) NOT NULL, Stotal SMALLINT DEFAULT 0 );这段建表语句一次用到了五种约束:主键约束(PRIMARY KEY)、非空约束(NOT NULL)、检查约束(CHECK)、默认值约束(DEFAULT)。先说类型选择,Sno CHAR(8)表示学号固定 8 位,用定长字符串比用VARCHAR(8)更省存储且语义明确;Sage SMALLINT用短整型存年龄,范围 -32768 到 32767,完全够用;Stotal SMALLINT DEFAULT 0的意思是学生总学分默认从 0 开始,之后通过 UPDATE 累加。
这里有个容易被忽视的点:Sname CHAR(8) NOT NULL保证姓名不能为空,Sclass CHAR(4) NOT NULL保证班级不能为空,但Sage、Sdept没加 NOT NULL,说明这两个字段允许 NULL。业务上这是合理的——年龄可能采集不到,系别可能后续再补。换句话说,建表时每一列要不要允许 NULL,本身就是一次数据模型设计决策,不是顺手写的。
再看选课表 SC,它的结构涉及复合主键和两个外键:
CREATE TABLE SC( Sno CHAR(8), Cno CHAR(4), Grade SMALLINT CONSTRAINT SC_CHECK CHECK(Grade > 0 AND Grade < 100), PRIMARY KEY (Sno, Cno), CONSTRAINT FK_SC_Sno FOREIGN KEY (Sno) REFERENCES Student(Sno), CONSTRAINT FK_SC_Cno FOREIGN KEY (Cno) REFERENCES Course(Cno) );复合主键(Sno, Cno)表达的是「一个学生选一门课只有一条成绩记录」,这比单列主键更贴近业务。两个外键分别指向 Student 和 Course,保证选课记录里不能出现不存在的学号或课程号。注意外键创建时我加上了名字FK_SC_Sno和FK_SC_Cno,这是强烈推荐的习惯——约束没名字,后续 DROP 的时候只能猜系统生成的随机名。
建表顺序同样有讲究。必须先建 Student 和 Course,再建 SC,因为 SC 的外键依赖前两张表;如果顺序反了,SQL Server 会直接报 "引用的表不存在"。同理,删除表的时候要反过来,先删 SC,再删 Student 和 Course。
2.2 自引用外键:Course.Cpno 引用 Course.Cno 的先修课设计
课程表里有个容易看懵的设计:课程先修课字段Cpno引用的是本表的Cno。这是很典型的自引用外键,表达「一门课的先修课是另一门课」。
CREATE TABLE Course( Cno CHAR(4) PRIMARY KEY, Cname CHAR(40), Cpno CHAR(4), Ccredit SMALLINT ); ALTER TABLE Course ADD FOREIGN KEY (Cpno) REFERENCES Course(Cno);这里的业务语义是:Cpno存放的是本课程的前置课程号。比如课程 1「数据库系统原理」的先修课是课程 5「数据结构」,那么课程 1 这一行的 Cpno 就是 5。自引用外键的插入顺序是个隐藏炸弹:先修课必须先于引用它的课程插入。以报告中的数据顺序为例,课程 1 引用课程 5、课程 4 引用课程 6,如果数据库严格校验外键,插入顺序就要先插 5、6、7 再插 1、4,否则会报外键冲突。课堂环境里 SQL Server 对外键的默认校验行为可能不会立刻暴露问题,但生产环境一定会拦你。常见做法是先分析依赖图再决定插入顺序,或者先把 Cpno 都置 NULL,数据齐了再 UPDATE。
2.3 索引的三种形态:普通索引、唯一索引与聚簇索引
报告里创建索引的语句集中在 3.1 节,虽然只有四条,但覆盖了索引的三种主要形态:
CREATE INDEX iCname ON Course(Cname); CREATE UNIQUE INDEX iSname ON Student(Sname); CREATE CLUSTERED INDEX iSnoCno ON SC(Sno, Cno DESC); CREATE UNIQUE INDEX iCno ON Course(Cno);普通索引CREATE INDEX iCname ON Course(Cname)用于加速按课程名查询,比如频繁执行WHERE Cname = '数据库系统原理'。唯一索引CREATE UNIQUE INDEX iSname ON Student(Sname)除了加速查找,还强制姓名不能重复——注意如果 Student 表里已经存在两个同名同学,这条语句会直接报重复键错误,所以唯一索引最好在数据导入前建立。聚簇索引CREATE CLUSTERED INDEX iSnoCno ON SC(Sno, Cno DESC)影响的是数据页的物理存储顺序:SC 表的记录会先按 Sno 排序,同 Sno 内再按 Cno 降序排列。聚簇索引不是越多越好,一张表只能有一个,因为物理顺序只有一种。
这里有一个容易翻车的现象:如果 Student 表的主键Sno默认就是聚簇索引,再执行CREATE CLUSTERED INDEX建第二个聚簇索引,SQL Server 会拒绝。理论上主键不一定要求聚簇,但 SQL Server 默认PRIMARY KEY就是聚簇索引,所以实操中创建聚簇索引前先确认表上是否已有。报告里给 SC 建聚簇索引没问题,因为 SC 的主键约束会自动建一个唯一索引,但不会和聚簇索引冲突。
2.4 视图:一段被命名的 SELECT,不是独立数据副本
报告中视图部分最典型的是这一段:
CREATE VIEW IS_Student AS SELECT Sno, Sname, Sage FROM Student WHERE Sdept = 'IS';执行完这条语句之后,IS_Student看起来像一张表,但你SELECT * FROM IS_Student查到的是实时从 Student 表过滤出来的数据。视图不占用额外存储,Student 表数据一变,视图查出来的结果跟着变。很多人第一次学视图会误以为它创建了数据副本,其实它只是保存了一条 SQL 定义。
视图的价值在于封装复杂查询。比如报告里的另一个视图:
CREATE VIEW S_G(Sno, Gavg) AS SELECT Sno, AVG(Grade) FROM SC GROUP BY Sno;这个视图把「按学号统计平均成绩」的聚合逻辑固化下来,上层应用只要查 S_G,不需要每次写 GROUP BY。需要注意聚合视图的列必须显式命名,视图列名写在视图名后面的括号里,也就是S_G(Sno, Gavg)这一段的含义。生产环境里视图常用于权限控制:只暴露需要的列,隐藏敏感字段,这是数据库安全设计里的常见做法。
2.5 ALTER TABLE 修改表结构:加列、改类型、加约束
实验报告里 3.1 节的「修改表结构及约束」部分,正好展示了 ALTER TABLE 的几种用法:
ALTER TABLE Student ADD Sclass CHAR(4); ALTER TABLE Student ALTER COLUMN Sage SMALLINT; ALTER TABLE Course ADD UNIQUE(Cname); ALTER TABLE SC DROP CONSTRAINT SC_CHECK; ALTER TABLE SC ADD CONSTRAINT SC_CHECK CHECK(Grade >= 0 AND Grade <= 100);第一条给 Student 表追加班级列,新列默认允许 NULL,所以已有数据不受影响。第二条把 Sage 从整型改成 SMALLINT,这是缩窄类型的操作,如果已有数据超出 SMALLINT 范围会失败。第三条给 Cname 加唯一约束,同理,如果课程表里已经有重名课程,这条语句会直接报错。第四、五条的组合是「先删再建」,因为 SQL Server 不直接支持修改 CHECK 约束内容,只能 DROP 掉再 ADD。这也是为什么建约束时命名如此重要——SC_CHECK这个名字在这里就是给 DROP 语句用的地址。
还有一个细节:报告第二套建表语句里 CHECK 写的是Grade > 0 AND Grade < 100,而第五行的 ALTER 改成Grade >= 0 AND Grade <= 100,这个变化暴露了约束边界问题。成绩 0 分和 100 分到底允不允许?课堂实验里可以随便改,生产环境的规则要先跟业务确认,不要凭感觉定边界。
3. 数据操作:INSERT、UPDATE、DELETE 的四种形态与临时表后悔药
实验二中 3.2 节覆盖了数据操作的三类语句。这段内容初看像流水账,细看会发现它几乎把 INSERT、UPDATE、DELETE 的所有常用形态都走了一遍,而且埋着两个不细心就会踩的坑。
3.1 INSERT 的几种形态:VALUES、按列插入与 INSERT...SELECT
报告里的插入语句分成两组。第一组是标准的 VALUES 插入:
INSERT INTO Student VALUES('20100001','李勇','男',20,'CS','1001'); INSERT INTO Student(Sno, Sname, Ssex, Sclass) VALUES('20100102','张盛','男','1008');第二条只给四个列赋值,没出现的 Sdept、Sage、Stotal 会被填默认值或 NULL。这正好演示了列级插入的用法:不是每次都要把所有字段写全,没写的列走 DEFAULT 或 NULL 逻辑。SQL Server 2008 之后还支持多行 VALUES:
INSERT INTO Student VALUES ('20100002','刘晨','女',19,'CS','1001'), ('20100021','王敏','女',18,'MA','1002');一条语句插多行,比逐行执行效率高很多,这在导入测试数据时很实用。紧接着报告里有一段「把查询结果插入另一张表」的写法:
CREATE TABLE cs_Student( 学号 CHAR(8), 姓名 CHAR(8), 年龄 SMALLINT ); INSERT INTO cs_Student SELECT Sno, Sname, Sage FROM Student WHERE Sdept = 'CS';这是 INSERT...SELECT 形态,目标表 cs_Student 的列顺序要和 SELECT 出来的列一一对应,而且目标表必须提前建好。如果目标表不存在,还有个更快的写法SELECT ... INTO 新表 FROM ...,后面 DELETE 部分会用到。性能上 INSERT...SELECT 是一次性批量写入,比逐行 INSERT 快一个数量级,大批量导数据时优先考虑。
3.2 UPDATE:单行更新、全体更新与关联子查询更新
报告里的 UPDATE 语句按影响行数可以分成三个层级:
UPDATE Student SET Sage = 22 WHERE Sno = '20100001'; UPDATE Student SET Sage = Sage + 1; UPDATE Sc SET Score = Score + 5 WHERE 'CS' = (SELECT Sdept FROM Student WHERE Student.Sno = Sc.Sno);第一条是单行更新,靠主键定位,影响一行。第二条没有 WHERE,更新全表,把所有学生年龄加一岁。这类语句在生产环境极其危险,忘记 WHERE 就是事故。第三条是关联子查询更新:右边子查询根据Student.Sno = Sc.Sno关联到 SC 表的每一行,判断该学生是否属于 CS 系,如果是就把成绩加 5 分。注意这里把子查询写在等号左边常量'CS'右边,语法上完全合法,但可读性一般,我更习惯写成WHERE Student.Sdept = 'CS'这种显式关联。
这里要专门提醒一个报告里隐藏的坑:SC 表在实验二建表时列名叫Score,但在查询部分又大量出现Grade;实验三里改成了Grade。如果按原报告顺序执行,前一半用 Score、后一半用 Grade,SQL Server 会在第一次执行 Grade 时报 "列名无效"。拿到这份报告后第一件事应该是全局搜索替换,把列名统一成你实际建表用的那个名字。
3.3 DELETE 与 SELECT INTO 临时表:误删之前的最后一道后悔药
删除部分的安排很有意思。报告先直接删 SC 表数据,然后复制出一个临时表 tmpSC,在临时表上做各种删除实验:
SELECT * INTO tmpSC FROM Sc; DELETE FROM tmpSC WHERE Sno = '20100001' AND Cno = '1'; DELETE FROM tmpSC WHERE Sno = '20100002'; DELETE FROM tmpSC WHERE 'CS' = (SELECT Sdept FROM Student WHERE Student.Sno = tmpSC.Sno); DELETE FROM tmpSC;SELECT * INTO tmpSC FROM Sc是 SQL Server 特有的建表+复制语法,一条语句同时完成「建新表」和「把旧表数据复制进去」两个动作。在临时表上做破坏性实验,不影响原始数据,这个思路本身就是 DELETE 操作的推荐做法。
删除顺序也是个常见翻车点。如果直接执行DELETE FROM Student WHERE Sno='20100001',而 SC 表里还有这个学生的选课记录,外键约束会拦下这条语句并报错。正确顺序永远是先删子表引用行,再删主表行。报告里的 tmpSC 实验其实就是在规避这个问题。
另外报告里有一条看起来执行成功的语句:
DELETE FROM Student WHERE Sno = '20100022';但插入时用的学号是 8 位20100022,这里写了 9 位201000022。结果就是 DELETE 影响 0 行,数据还在。这是典型的「写错匹配值导致静默失败」,没有任何报错,但逻辑没生效。以后排查 DELETE 或 UPDATE 不见效时,先看影响行数是不是 0,如果是,多半是匹配条件根本没对上。
4. 查询操作:单表、聚合、连接、嵌套与集合运算的完整走查
实验二的 3.3 节是整份报告里篇幅最长的部分,几乎把 SELECT 查询的所有基础语法过了一遍。这一章的练习质量很高,值得一段一段跟着查。
4.1 单表查询:DISTINCT、BETWEEN、LIKE、IS NULL 与排序
报告里单表查询的语句很多,挑几个容易用错的看。第一个是去重:
SELECT Sno FROM Sc; SELECT DISTINCT Sno FROM Sc;第一句返回所有选课记录对应的学号,7 行里学号有重复;第二句加 DISTINCT,只返回不重复的学号。注意 DISTINCT 作用的是整行,如果 SELECT 后面跟了多个列,只要整行不完全相同就不会去重。
SELECT Sname, Sdept, Sage FROM Student WHERE Sage BETWEEN 20 AND 23;BETWEEN 20 AND 23在 SQL Server 里是闭区间,包含 20 和 23 本身。这个边界容易被忽略,写条件时如果业务要求的是开区间,就得改成Sage > 20 AND Sage < 23。
SELECT Sname, Sno, Sage FROM Student WHERE Sname LIKE '刘%';LIKE 的%匹配任意长度字符串,_匹配单个字符,如果想要精确匹配「刘洋」两个字但不确定第二个字,可以写LIKE '刘_'。注意 LIKE 后的模式串要加引号,忘了加引号会直接语法报错。
SELECT Sno, Cno FROM Sc WHERE Score IS NULL;空值判断必须用IS NULL,不能用= NULL。SQL 里 NULL 和任何值做比较的结果都是 UNKNOWN,用等号判断永远查不出数据。这一点新手踩坑率极高。
排序语句SELECT * FROM Student ORDER BY Sdept, Sage DESC的含义是:先按 Sdept 升序,同一系内按年龄降序。DESC只作用于紧挨着它的 Sage,Sdept 默认升序。如果想两个字段都降序,不要写成ORDER BY Sdept, Sage DESC,而是要写成ORDER BY Sdept DESC, Sage DESC。
4.2 聚合与 GROUP BY:聚合函数忽略 NULL 的边界条件
报告里的分组统计部分有一组经典练习:
SELECT COUNT(*) FROM Student; SELECT COUNT(DISTINCT Sno) FROM Sc; SELECT MAX(Score) FROM Sc; SELECT Cno, COUNT(*), AVG(Score), MAX(Score) FROM Sc GROUP BY Cno;COUNT(*)统计全部行数,不管列是否 NULL;COUNT(Column)只统计该列非 NULL 的行数;AVG(Score)计算平均分时会自动跳过 NULL 成绩。所以一个班如果有三个缺考的学生,AVG(Score)算出来的平均分是三门有效成绩的平均,不是按全员分母算的。要不要把缺考当 0 分计,取决于业务口径,但 SQL 默认行为是忽略 NULL。
GROUP BY 之后的 HAVING 是分组后的过滤:
SELECT Cno, COUNT(*), AVG(Score), MAX(Score) FROM Sc GROUP BY Cno HAVING AVG(Score) > 90;WHERE 在分组之前过滤行,HAVING 在分组之后过滤组。选课人数不足、均分不达标的课程组,用 HAVING 处理。还有一个规则:SELECT 后面出现的非聚合列,必须出现在 GROUP BY 里,否则 SQL Server 会报错或返回不确定结果。
报告里有一条统计混用了列名AVG(grade)和MAX(grade),如果表里实际列叫 Score,这条语句会报列名无效。我在前面已经提醒过列名统一的重要性,这里不再重复,但建议跑这段脚本前先花一分钟确认每一处列名。
4.3 连接查询:内连接、左外连接与三表连接的区别
报告里同一道题用了两种写法,正好可以对比。第一种是传统 WHERE 连接:
SELECT Student.Sno, Sname, Cno, Grade FROM Student, SC WHERE Student.Sno = SC.Sno;第二种是 ANSI JOIN 写法:
SELECT Student.Sno, Sname, Cno, Grade FROM Student JOIN SC ON (Student.Sno = SC.Sno);两种写法结果一样,都是内连接:只返回两边都匹配的行。没有选课的学生不会出现在结果里。在 FROM 里声明连接条件,是现在更推荐的方式,因为连接条件和过滤条件分得更清楚。
左外连接是重点:
SELECT Student.Sno, Sname, Cno, Grade FROM Student LEFT OUTER JOIN SC ON (Student.Sno = SC.Sno);这条语句返回所有学生,没有选课的学生也会出现,对应 SC 的字段显示为 NULL。LEFT OUTER JOIN 的含义是「左侧表的所有行都保留,右侧表只取匹配行」。这在统计「哪些学生没选课」时特别好用,配合WHERE SC.Sno IS NULL就能筛出来。
三表连接需要两个连接条件:
SELECT Student.Sno, Sname, Cname, Grade FROM Student, SC, Course WHERE Student.Sno = SC.Sno AND SC.Cno = Course.Cno;表越多越容易忘条件。三个表连接至少需要两个连接条件,四个表至少三个。少写一个条件,结果集就会变成笛卡尔积,行数暴涨。遇到多表查询结果莫名其妙变多时,先检查连接条件有没有漏。
4.4 嵌套查询与集合运算:IN、ALL、EXISTS 的适用场景
嵌套查询部分是 SQL 里思维跳跃最明显的地方。报告里从简单到复杂给了四类。
IN 引出的子查询最简单:
SELECT Sno, Sname, Sdept FROM Student WHERE Sdept IN (SELECT Sdept FROM Student WHERE Sname = '刘晨');子查询先找出刘晨所在系,外层再找同一系的学生。注意这里子查询结果集是「多个可能值」,所以外层用 IN 而不是=。
相关子查询用到了表别名:
SELECT Sno, Cno FROM SC x WHERE Grade >= (SELECT AVG(Grade) FROM SC y WHERE y.Sno = x.Sno);这条查询找出每个学生超过自己平均成绩的选课记录。x 和 y 是同一张表的两个实例,内层子查询通过y.Sno = x.Sno关联外层当前行。这就是相关子查询的核心——子查询每执行一次,用的都是外层当前行的值。这类查询的执行方式和 IN 子查询不同,逻辑上更像个循环,数据量大时要留意性能。
ALL 的语义是「比子查询所有值都……」,比如:
SELECT Sname, Sage FROM Student WHERE Sage < ALL (SELECT Sage FROM Student WHERE Sdept = 'CS') AND Sdept <> 'CS';含义是非 CS 系中年龄比 CS 系所有学生都小的学生。< ALL等价于「小于子查询结果最小值」,< ANY等价于「小于最大值」。这两个修饰符在特定需求下好用,但可读性一般,实际开发中我更常写成聚合子查询Sage < (SELECT MIN(Sage) FROM Student WHERE Sdept = 'CS'),语义更直白。
EXISTS 处理的是「存在性判断」:
SELECT Sname FROM Student WHERE EXISTS (SELECT * FROM SC WHERE Sno = Student.Sno AND Cno = '1');EXISTS 只关心子查询有没有返回行,不关心返回什么内容,所以子查询写SELECT *或SELECT 1都行。它也是表达「所有」「不存在」语义的基础工具,比如「找出没选修任何课程的学生」,EXISTS 配合 NOT 就能做。
集合运算部分 SQL Server 的关键字和别的数据库略有差异:
SELECT * FROM Student WHERE Sdept = 'CS' UNION SELECT * FROM Student WHERE Sage <= 19; SELECT * FROM Student WHERE Sdept = 'CS' EXCEPT SELECT * FROM Student WHERE Sage <= 19;UNION 去重合并,UNION ALL 保留重复;INTERSECT 取交集;EXCEPT 取差集,意思是「属于第一个集合但不属于第二个集合」。注意 SQL Server 用 EXCEPT,而 Oracle 里同样的语义是 MINUS。跨数据库迁移时这个关键字要记得改。
4.5 执行顺序与常见的低级错误
SQL 语句的逻辑执行顺序是 FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY,很多人会误以为 SELECT 先执行,其实 SELECT 的列别名在 WHERE 阶段根本不可见。这也解释了为什么WHERE Sage > 20可以,但WHERE 出生年份 < 2000不行——后者如果「出生年份」是 SELECT 里定义的别名,WHERE 阶段还没有这个列。
报告里还有一条隐性问题:有一句查询把表名写成了Sudent。SQL Server 会立刻报 "对象名无效",但实际上只需要把表名改回Student就能通过。这类错误不是逻辑问题,纯靠细心。拿到一个陌生环境的数据库,先SELECT TOP 10 * FROM 表名确认表和列真实存在,再写完整查询,能省不少排错时间。
5. 避坑手册:五个真实数据库实验翻车现场
这一章的五个问题,全部来自实际跑这份报告时的常见失败场景。每条都按「现象 → 原因 → 解决」整理,可以当速查表用。
5.1 建表时末尾多一个逗号:SQL Server 直接语法报错
现象:执行实验二里 Student 表的建表语句,SQL Server 报Incorrect syntax near ')'。
原因:原始语句末尾是Sdept char(20), ),最后一个列定义后面多了一个英文逗号。SQL Server 不允许列清单以逗号结尾。
解决:删掉最后一个列名后面的逗号,改成Sdept char(20))。以后写 CREATE TABLE 时,所有逗号都只放在列与列之间,最后一列后不写逗号。
5.2 学号多打一位:DELETE 静默返回 0 行
现象:执行DELETE FROM Student WHERE Sno='201000022',结果显示(0 rows affected),但这条学生记录明明存在。
原因:表里插入的学号是 8 位20100022,DELETE 条件里写成了 9 位的201000022,匹配不到任何记录。SQL Server 不会报错,只是不干活。
解决:执行 DELETE 或 UPDATE 之后养成看影响行数的习惯。影响行数是 0 不代表执行失败,而是代表条件没有命中。先SELECT Sno FROM Student WHERE Sno = '201000022'确认一下匹配值是否正确,再决定要不要执行。
5.3 外键拦截删除:先删子表再删主表
现象:直接执行DELETE FROM Course WHERE Cno='1',SQL Server 报DELETE statement conflicted with the REFERENCE constraint "FK_SC_Cno"。
原因:SC 表通过外键引用了 Course(Cno),课程 1 还有学生在选,外键约束不允许直接删除被引用的父表记录。
解决:先删除 SC 表中该课程对应的选课记录,再删除 Course 中的课程记录。如果有临时备份表,也可以先在备份表上练习删除逻辑,确认无误后再对正式表操作。
5.4 主键重复与 UNIQUE 冲突:分清 2627 和 2601
现象:重复执行INSERT INTO Student VALUES('20100001', ...),SQL Server 报Violation of PRIMARY KEY constraint;给 Course 表加 UNIQUE(Cname) 时如果已有重复课程名,报Violation of UNIQUE KEY constraint。
原因:重复插入主键值会触发错误码 2627,重复插入唯一索引值触发错误码 2601。两类错误本质相同:插入的数据和已有约束冲突。
解决:插入前先查重,或者用IF NOT EXISTS包一层。如果错误码是 2601,还要检查是不是唯一索引建立在已有重复数据的列上,这种情况下要先清理重复数据,再建唯一约束。
5.5 CHECK 约束放行 NULL:三值逻辑下的漏网之鱼
现象:INSERT INTO SC VALUES('20100003','1',NULL)执行成功,没有触发Grade > 0 AND Grade < 100的 CHECK 约束。
原因:SQL 里 NULL 参与的判断结果是 UNKNOWN,不是 FALSE。CHECK 约束只拦截结果为 FALSE 的情况,UNKNOWN 会被放行。
解决:如果业务要求缺考成绩也不能为空,要把列改成Grade SMALLINT NOT NULL。CHECK 约束管范围,NOT NULL 管是否允许空,两个约束缺一不可。
6. 进阶验证:把实验脚本变成一份可复用的 DDL/DML 自检清单
这份报告最有价值的地方,是它完整覆盖了从建库到约束验证的全流程,但直接按原顺序跑,会因为列名混用、表名拼写等问题中断。我一般会把实验脚本按模块重排成五段,每段一个 GO,放在同一个 .sql 文件里,作为新环境自检清单:
| 模块顺序 | 脚本内容 | 验证点 |
|---|---|---|
| 第一段 | CREATE TABLE 三张表和索引视图 | 表结构、约束、对象是否存在 |
| 第二段 | INSERT 全部样例数据 | 主键唯一、外键引用、CHECK 生效 |
| 第三段 | SELECT 查询(单表、聚合、连接、嵌套、集合) | 结果行数与逻辑预期一致 |
| 第四段 | UPDATE / DELETE 实验 | 影响行数、外键拦截行为 |
| 第五段 | 违规插入与约束阻断实验 | 错误码 2627、2601、外键冲突 |
整理时把每段开头用注释写清楚预期结果,比如「预期 4 行,DISTINCT 后 3 行」。这样每次拿到新库,把脚本从头跑一遍,数据库基础功能有没有问题一目了然。
另外,报告里提到数据完整性但没有展开触发器。如果想验证 CHECK 约束之外的强制逻辑,可以补一段最简单的 AFTER INSERT 触发器:
CREATE TRIGGER trg_sc_grade ON SC AFTER INSERT, UPDATE AS BEGIN IF EXISTS(SELECT 1 FROM inserted WHERE Grade > 100 OR Grade < 0) BEGIN RAISERROR('成绩超出合法范围', 16, 1); ROLLBACK TRANSACTION; END END这段触发器在每次插入或更新 SC 表之后检查inserted逻辑表里的新数据,发现成绩越界就报错并回滚事务。inserted表是 DML 触发器中自动生成的临时表,保存受影响行的新版本数据。如果有更新操作,另一个逻辑表deleted保存旧版本行。触发器和 CHECK 约束的区别在于,CHECK 只能做简单的行内范围判断,触发器可以写更复杂的业务规则,比如根据其他表的状态决定是否允许操作。
从那以后,我每次接触一个新数据库,都会先花十分钟把约束关系和外键依赖理清楚,再把这份自检脚本从头跑一遍,而不是上来就写业务查询。这个习惯救过我好几次——有一次差点在带外键的表上直接 DROP,就是靠脚本里的删除顺序提醒拦下来的。这份实验报告的完整脚本按上述模块重排后,在 SQL Server 2016 的 SSMS 里建一个空库逐段执行即可复现所有结果,适合拿去做实验对照和考前复习,希望帮到你。
本文还有配套的精品资源,点击获取