简介:这份《数据库实验四》文档面向学习数据库原理与SQL Server/T-SQL的学生,聚焦完整性约束与表间引用关系,提供可对照复盘的实验记录。内容覆盖联合主键和唯一约束的创建与删除、主表和从表增删改时的参照完整性验证、级联引用测试,并记录约束冲突报错、失败原因分析、思考题及课外任务,便于读者理解外键失效、级联更新失败等常见问题,掌握约束设计与排错思路。资源包共1个docx文件,约711KB,以文字与T-SQL语句示例为主,结构紧凑,适合课程实验对照与整理笔记。已有348人学习,可作为数据库实验课配套材料,帮助快速梳理实验步骤、验证结果和完整性约束设计要点。
1. 数据库完整性约束的地图:五道防线各守哪一段
数据库这门课复习到完整性约束,很容易滑成一件事:背四条定义。但它实际更像一张网。实体完整性管「这一行靠什么被唯一认出来」,域完整性管「这一列允许填什么」,参照完整性管「两张表之间的引用关系不能凭空断掉」,用户定义完整性管「业务上说不说得通」。实验四那一串任务——建联合主键、删唯一约束、改主表键、改从表键、开级联——恰好把这张网的四个节点各拉了一遍,出错的地方也几乎全部集中在「谁引用谁」这个方向上。正在做数据库实验、准备期末、或者想补齐 T-SQL 语法的人,都可以拿它当一张地图用。先记住一句:约束报错不是数据库在找麻烦,它是整套系统里最便宜的一次拦截。
2. 实体完整性与域完整性:主键、联合主键与唯一约束的复习重点
2.1 主键的三重含义:唯一、非空、可被引用
多数人把主键记成「唯一」,这就少了两条。主键是三条性质叠在一起:列值唯一、列值非空、可以被其他表的外键引用。第三条最容易被忽略,而它恰恰是后面所有参照完整性测试的前提——从表的外键只能指向主表的主键或唯一约束列,指不到别的地方去。
-- 给 pay 表建联合主键:No + Year + Month 三列共同唯一 ALTER TABLE dbo.pay ADD CONSTRAINT PK_pay PRIMARY KEY (No, [Year], [Month]);这段语句执行成功后,pay 表上会多一个名为 PK_pay 的主键约束,同时 SQL Server 会自动为它建一个唯一索引。参数说明:括号里三列的顺序有意义,它决定了索引的前导列,也决定了「哪些列组合必须唯一」。如果表上已经存在聚集索引,再显式写 CLUSTERED 就会直接失败,所以初学阶段不加 CLUSTERED 更稳妥。
主键还有一条隐含规则:一张表只能有一个主键。想给多列加唯一性,要么合成一个联合主键,要么退一步用唯一约束。这个区别在实验里会反复出现,考试里也爱出成「判断下列说法是否正确」。
2.2 联合主键:No + Year + Month 捆在一起锁什么
pay 是工资表,No 是工号,Year 和 Month 是发放年月。单看 No 一定重复——同一个员工每个月都有一条记录;单看 Year 也重复——同一年有几百号人。只有三列合起来才唯一:某人在某年某月的工资记录只能有一条。
这就是联合主键的语义:它锁的不是「每一列都唯一」,而是「每一种组合唯一」。梳理下来会发现,判断一张表该不该用联合主键,问自己一句就够——业务上到底是哪一个最小组合在标识这条记录。答不上来,说明还缺一个代理键(比如自增 ID)。
最容易漏的是组合内部的 NULL 问题。主键列一律隐式 NOT NULL,所以联合主键的三列谁都不能为空。如果业务上「月份可以为空」表示「年终奖」,那就不能把它塞进主键,得换成别的建模方式。这一点在写实验报告时常常被一句「三列联合定义为主键」带过去,实际上它是后面插入数据失败的伏笔。
删除主键同样一行语句:
-- 删除 pay 表上的主键约束 ALTER TABLE dbo.pay DROP CONSTRAINT PK_pay;执行这条语句时如果报错「对象正被 FOREIGN KEY 约束引用」,说明已经有别的表拿它当外键目标了,必须先删引用方。这个顺序问题在 2.4 节展开。
2.3 唯一约束:比主键松一档的唯一性
dept 表上的部门名称需要一个唯一性保证,但它显然不该当主键——主键已经是部门代号。这时就该唯一约束出场。
-- 在 dept 表的部门名称列上建唯一约束 ALTER TABLE dbo.dept ADD CONSTRAINT UK_dept_Dname UNIQUE (Dname); -- 删除这个唯一约束 ALTER TABLE dbo.dept DROP CONSTRAINT UK_dept_Dname;逻辑很直白:Dname 不允许出现两个相同的部门名,但可以为 NULL(如果列本身允许)。删除时按约束名精确删除,不能用列名。参数上要注意,UNIQUE 后面可以跟多列,写成 UNIQUE (Dname, DeptNo) 就变成了组合唯一,和联合主键的区别只在于「允不允许 NULL」以及「一张表能有几个」。
| 对比项 | PRIMARY KEY | UNIQUE |
|---|---|---|
| 一张表能有几个 | 只能一个 | 可以有多个 |
| 是否允许 NULL | 不允许 | 允许(SQL Server 单列下只允许一个 NULL) |
| 是否自动建索引 | 是,默认唯一索引 | 是,唯一索引 |
| 能否被外键引用 | 能 | 能 |
| 典型场景 | 部门代号、工号 | 部门名称、身份证号、邮箱 |
表格里「单列只允许一个 NULL」这一格是 SQL Server 的特殊行为,也是最容易出问题的地方:SQL 标准允许唯一列出现多个 NULL,但 SQL Server 把 NULL 当作可比较值来处理唯一性,所以第二个 NULL 会被拒绝。做实验时如果拿唯一列插两条空值数据报错,不要怀疑自己写错了语法,那是产品行为。
2.4 删除与重建:先解引用再动主键
约束之间是有依赖的:外键依赖主键(或唯一约束)。所以删除顺序永远是从表先、主表后。反过来操作,SQL Server 会明确拒绝,报错大意是「该对象正被一个 FOREIGN KEY 约束引用,无法删除」。
正确的顺序写成流程就是三步:先找出所有指向这张表的外键,把它们删掉;再删主键或唯一约束;改完结构后重新把外键建回去。中间那一步不能省,也不能靠「先禁用约束」蒙混——禁用只是暂时不检查,结构依赖还在。
提示:重建外键时默认会校验现有数据,如果历史数据里有孤儿行,建约束会直接失败。这时要么先修数据,要么用 WITH NOCHECK 建约束,但后者会让约束处于「不受信任」状态,查询优化器不会拿它做简化,后面排查问题时很容易被这一点误导。
还有一个实操细节:写实验报告时常把「删除主键」写成一行就完事,实际执行时如果 pay 表的 No 同时被别的表当外键引用,这一行必然失败。养成习惯,先查一遍依赖再动手,比事后猜报错快得多。
3. 参照完整性:主表和从表的增删改,到底谁被拦下
3.1 外键的方向:从表指向主表
参照完整性只有一句话:从表里的外键值,要么在主表被引用列里真实存在,要么是 NULL。方向感是这一章的全部难点——外键写在从表上,指向主表。
在实验的这组表里,dept 是主表,DeptNo 是它的主键;person 是从表,它的 DeptNo 列是指向 dept 的外键;pay 也是从表,它的 No 列通过外键 fk_no 指向 person 的 No。关系是链式的:dept ← person ← pay。
-- person 的 DeptNo 指向 dept 的 DeptNo ALTER TABLE dbo.person WITH CHECK ADD CONSTRAINT FK_person_dept FOREIGN KEY (DeptNo) REFERENCES dbo.dept (DeptNo);WITH CHECK 表示建约束时校验已有数据,这是默认行为,写出来只是让意图明确。括号里的对应关系不能颠倒:前一个括号是从表列,REFERENCES 后面是主表和被引用列。
判断谁是主表谁是从来不看表名,看方向:被 REFERENCES 指向的那张是主表。同一个人可能在 A 关系里是主表,在 B 关系里是从表,这在链式结构里非常常见。
3.2 动主表:更新 DeptNo 为什么被 REFERENCE 约束拦住
实验里最典型的一步:把 dept 里部门代号 '00101' 改成 '00108'。
-- 主表 dept 更新被引用键 UPDATE dbo.dept SET DeptNo = '00108' WHERE DeptNo = '00101'; -- 报错:UPDATE 语句与 REFERENCE 约束 "FK_person_dept" 冲突失败的原因不难推:person 表里有若干行的 DeptNo 还是 '00101',一旦主表把这个值改掉,这些行就指向了一个不存在的部门,参照完整性当场断裂。默认的引用动作是 NO ACTION,也就是「不许动,报错」。
同样的逻辑适用于删除:删掉 dept 里的 '00101',person 里引用它的行会集体变成孤儿,所以默认也会被拦下。而往主表插入一条新部门是永远没问题的——新增一个被引用目标不会破坏任何现有引用。
把这条整理成一句口诀:主表的插入永远安全,主表的更新和删除要看有没有人引用。
3.3 动从表:pay.No 改成主表里没有的值必然失败
反过来,从表上的操作限制在另一个方向。实验里把 pay 的工号 '000002' 改成 '000020',报错「UPDATE 语句与 FOREIGN KEY 约束 fk_no 冲突」。因为 person 表里根本不存在 '000020' 这个工号,改完之后 pay 这一行就悬空了。
-- 从表 pay 更新外键列,目标值在主表 person 中不存在 UPDATE dbo.pay SET No = '000020' WHERE No = '000002'; -- 报错:与 FOREIGN KEY 约束 "fk_no" 冲突从表的四类操作可以这样归类:插入一行,外键值必须在主表存在;更新外键列,新值必须在主表存在;删除一行,永远安全,删从表不会破坏任何引用;更新非外键列,也永远安全。这一格是很多人绕不过来的地方——总觉得「从表受限制更多」,其实从表在删除上是完全自由的,受限制的是主表。
顺带记一个边界情况:如果外键列允许 NULL,插入 NULL 是合法的,语义是「暂未归属」。这也是为什么设计外键列时要想清楚它该不该 NOT NULL。
3.4 读报错:从约束名反推表和列
报错信息里那串东西不是随机字符串。像 FK__person__DeptNo__2B0A656D,中间夹着从表名 person 和被引用列 DeptNo,末尾那串是 SQL Server 自动生成的后缀;而 fk_no 这种则是建约束时手工起的名字。手工命名的好处在这里体现得很直接:一看到 fk_no 就知道是 pay 的 No 列那条外键,排查速度差好几倍。
把常见操作和后果整理成一张表,复习时对着看比背定义快:
| 操作位置 | 具体动作 | 是否破坏完整性 | 约束类型 |
|---|---|---|---|
| 从表 | 插入主表中不存在的课程号 | 破坏 | 参照完整性 |
| 主表 | 插入与现有主键重复的行 | 破坏 | 实体完整性 |
| 主表 | 删除仍被从表引用的课程号 | 破坏 | 参照完整性 |
| 任意表 | 把性别改成「男」「女」以外的值 | 破坏 | 用户定义完整性 |
| 从表 | 删除一行选课记录 | 不破坏 | 参照完整性 |
| 主表 | 插入一条全新的部门记录 | 不破坏 | 参照完整性 |
有了这张表,思考题里「举例说明哪些增删改会破坏完整性」就不用现编了。真正要理解的是背后那条线:破坏永远发生在「被引用的那一端」或「唯一性被挑战的那一端」。
4. 级联引用:ON UPDATE CASCADE 能救什么、救不了什么
4.1 五种引用动作:默认的 NO ACTION 挡掉了什么
外键可以声明被引用键变化时从表怎么办,选项就那么几个:
| 引用动作 | 主表更新/删除时从表的行为 | 适合的场景 |
|---|---|---|
| NO ACTION | 有引用就报错阻止 | 默认,最保守 |
| CASCADE | 从表对应行跟着更新/删除 | 明细表、选课记录 |
| SET NULL | 从表外键列置空 | 可选归属,如「负责人」 |
| SET DEFAULT | 从表外键列回到默认值 | 有兜底分类的字典表 |
| RESTRICT | 与 NO ACTION 接近,检查时机略有差别 | 强一致场景 |
把默认行为理解成「数据库替你选择了最不容易出错的那一种」更贴切。级联不是更高级的写法,它只是把「手动维护一致性」这件事交给数据库自动完成,代价是影响范围会顺着关系链扩散,动一行可能改掉一片。
4.2 建级联的正确姿势:先删后建,不能原地改
SQL Server 不支持直接 ALTER 一个已有外键的引用动作,必须删掉重建。
-- 第一步:删掉旧外键 ALTER TABLE dbo.sc DROP CONSTRAINT FK_sc_course; GO -- 第二步:带级联动作重新建 ALTER TABLE dbo.sc WITH CHECK ADD CONSTRAINT FK_sc_course FOREIGN KEY (cno) REFERENCES dbo.course (cno) ON UPDATE CASCADE ON DELETE NO ACTION; GO第一句按名字删掉原约束,第二句重建并声明「主表 cno 更新时从表跟着改」。ON UPDATE 与 ON DELETE 可以分开指定,这里只开了更新级联,删除仍然保持阻止,因为删课程属于业务上需要人工确认的动作。GO 是批处理分隔符,删和建必须分属两个批次,否则解析阶段就会报约束不存在。
建完立刻验证比凭记忆相信它生效更靠谱:
-- 查外键的实际动作与信任状态 SELECT fk.name AS 外键名, OBJECT_NAME(fk.parent_object_id) AS 从表, OBJECT_NAME(fk.referenced_object_id) AS 主表, fk.is_not_trusted AS 未受信任, fk.update_referential_action_desc AS 更新动作, fk.delete_referential_action_desc AS 删除动作 FROM sys.foreign_keys AS fk WHERE OBJECT_NAME(fk.parent_object_id) IN ('sc', 'course', 'person', 'pay');输出里 update_referential_action_desc 显示 CASCADE 才算真的开了级联;如果显示 NO_ACTION,说明语句里漏了或者被别的原因挡掉。is_not_trusted 为 1 则意味着约束没校验历史数据,这类约束在优化器眼里等于不存在,别拿它当保障。
4.3 sc 的 cno 级联失败:两种最常见的原因
课外任务里把课程号 '0809023501' 改成 '0809023601',期望 sc 里的选课记录跟着变,结果级联没生效。梳理下来,原因基本落在两个地方。
第一种是语句本身就建失败了。course 表的 cno 除了被 sc 引用,还被 course 自己的 cpno 引用。如果 cpno 那条外键也声明了级联,那么更新 course.cno 时就存在两条传播路径,SQL Server 会直接拒绝建约束,报错号 1785,提示「可能会导致循环或多重级联路径」。这时候失败发生在建约束那一刻,而不是更新数据那一刻。
第二种是建成功了但没生效——常见于历史数据里有孤儿行,于是有人用了 WITH NOCHECK,约束虽然存在但不受信任,级联行为在实际更新时并不会按预期触发。区别方法就是上一节那条查询:看 is_not_trusted 和 update_referential_action_desc 两列。
注意:遇到「级联没反应」,先确认约束建没建成、建成的是什么动作,再去怀疑数据。顺序反了会白查半小时。
4.4 自引用外键:course.cpno 引用 course.cno 为什么更难级联
course 表里 cpno 是先修课编号,它指向本表的 cno。这是一条自引用外键:一行课程的先修课,是另一行课程。
-- 自引用外键:cpno 指向本表的 cno ALTER TABLE dbo.course WITH CHECK ADD CONSTRAINT FK_course_cpno FOREIGN KEY (cpno) REFERENCES dbo.course (cno);这条语句本身没问题,问题出在给它加 ON UPDATE CASCADE 的时候。自引用结构下,更新一个 cno 可能触发对同一张表的再次更新,形成潜在的循环,SQL Server 会直接拒绝,报错同样是 1785 那类「循环或多重级联路径」。
所以实验里那句结论「外键不能实现级联更新」要理解得窄一点:不是外键做不到级联,而是自引用和多重路径这两种结构下,数据库不允许自动级联。想保证正确性,就得靠别的手段——先更新所有引用方、再更新主键,整个过程包在事务里,要么全成要么全回滚。
-- 自引用场景下手动维护一致性:先改引用方,再改被引用方 BEGIN TRANSACTION; UPDATE dbo.course SET cpno = '0809023601' WHERE cpno = '0809023501'; UPDATE dbo.course SET cno = '0809023601' WHERE cno = '0809023501'; COMMIT TRANSACTION;顺序不能反:先改引用方,让它暂时指向一个还没出现的新值会被外键拦住;只有先改引用方到新值、再改被引用方,中间态才始终满足约束。当然更稳的做法是先插入新行、迁移引用、再删旧行,代价是主键值会多占一个。这段代码的关键不是语法,而是「谁先谁后」这个顺序,写反了必然在第一条语句上翻车。
4.5 级联之外的兜底:触发器与应用层事务
级联能覆盖的场景其实不宽——单路径、非自引用、结构稳定。真实项目里常见的替代方案有两种。
一是触发器,在主表更新后自动修正从表。优点是能处理多路径和复杂条件,缺点是隐式执行、调试困难,出问题时像个黑匣子,得靠日志一点点还原。
二是把一致性收进应用层事务,用显式的多条语句完成迁移,数据库只负责校验。这种做法把复杂度搬到了代码里,好处是逻辑可见、可测试,坏处是任何绕过这段代码的写入路径都会破坏一致性。
选哪种,看的是「这套数据有几条写入路径」。只有一条,级联最省事;有好几条,触发器或应用层事务更可控。这一点在实验报告里通常不写,但期末大题很喜欢问「级联失败后如何保证数据正确性」,答「约束 + 事务 + 应用层校验」比单答「加级联」得分高。
5. 易错点复盘:五个最容易出问题的约束细节
5.1 语法层的三个易漏点
易漏点一:以为 ALTER 能直接改外键动作。为什么漏:多数 DDL 语句支持原地修改,习惯性以为外键也一样。怎么补:记住外键的唯一修改方式是 DROP 再 ADD,而且两句之间要加 GO 分批。
易漏点二:建约束时漏写列级对应关系,把 REFERENCES 后面写成从表。为什么漏:语句能跑通,直到真正插入数据才发现方向反了。怎么补:写完念一遍「从表的这一列,指向主表的哪一列」,方向不对立刻能听出来。
易漏点三:忘了 NULL 在唯一约束下的产品差异。为什么漏:标准 SQL 允许多个 NULL,SQL Server 单列只允许一个。怎么补:把这一条单独抄在笔记的「方言差异」区,和「TOP 代替 LIMIT」「GETDATE() 代替 NOW()」放一起。
5.2 语义层的三个易错点
易错点一:分不清主表和从表谁被限制。为什么漏:两边的报错长得几乎一样,都是「与某约束冲突」。怎么补:看报错里出现的是 REFERENCE 约束还是 FOREIGN KEY 约束——前者是动主表被拦,后者是动从表被拦;再对一眼表名,方向立刻清楚。
易错点二:把「级联失败」直接归因于数据问题。为什么漏:级联失败的表现和普通外键冲突一样,都是更新没成功。怎么补:按「约束是否存在 → 动作是否为 CASCADE → 是否受信任 → 数据是否满足」四步依次排查,别跳步。
易错点三:实验记录里同一步骤既写了失败又写了成功,事后自己都说不清。为什么漏:加了级联前后各测一次,两次结果混在一段文字里。怎么补:每次改动结构前后各留一条独立记录,标明「当前约束状态 + 操作 + 结果」,这三样凑齐,报告才站得住。
提示:约束被拒绝执行时,第一反应不该是删约束。删掉之后不合逻辑的数据依然会写进去,只是没人再提醒你。正确做法是回到数据本身,看是哪一行违反了规则。
6. 复习清单:从语法到排错的查漏补缺路线
复习这一块,我习惯按「语法 → 语义 → 排错」三层过一遍,每层留一个可验证的动作,而不是只看笔记。
第一层语法,把五条语句默写出来并跑通:建主键、删主键、建唯一约束、建外键、建带级联的外键。默写不出来的,说明只是眼熟。表格对照着记更省力:
| 目标 | 关键语句片段 | 常错处 |
|---|---|---|
| 建联合主键 | ADD CONSTRAINT PK_x PRIMARY KEY (a,b,c) | 列顺序、NULL 列不能入主键 |
| 删主键 | DROP CONSTRAINT PK_x | 有外键引用时先删外键 |
| 建唯一约束 | ADD CONSTRAINT UK_x UNIQUE (col) | 与主键混淆、NULL 数量 |
| 建外键 | FOREIGN KEY (col) REFERENCES 主表(列) | 方向写反 |
| 开级联 | ON UPDATE CASCADE | 需先 DROP 再 ADD |
第二层语义,用手推的方式过一遍四种操作:主表插入、主表更新、从表插入、从表删除,各问一句「会不会被拦、被哪类约束拦」。推不出来的,回到第 3 章那张表对一遍,直到能不看表直接答。
第三层排错,把这条查询当成固定动作:
-- 排查外键问题时的第一条语句 SELECT fk.name, fk.is_not_trusted, fk.update_referential_action_desc, fk.delete_referential_action_desc FROM sys.foreign_keys AS fk WHERE OBJECT_NAME(fk.parent_object_id) = 'sc';看到 NO_ACTION 就知道级联没开成,看到 is_not_trusted 为 1 就知道约束没校验历史数据,两种情况对应的修法完全不同。这一步花三十秒,能省掉半小时的瞎猜。
最后留一个自查问题:如果现在让你把 dept 的部门代号改掉,同时 person 和 pay 都跟着变,你会怎么写?答案不是一句 CASCADE 就完事,而是要判断这条链上有几条路径、有没有自引用、要不要包事务。答得出来,这一章就算过关了。
血泪经验是,我以前做这类实验总想着先跑通再补记录,结果每次回头写报告都要重新猜当时改了什么。从那以后我每次动约束结构,都强制走一遍「查依赖 → 改结构 → 查元数据验证 → 记一条结果」这四步,哪怕是再简单的一行 DROP。慢是慢了点,但没有一次再为「到底改没改成功」返工。希望帮到你。
本文还有配套的精品资源,点击获取