简介:这份数据库实验四文档面向高校数据库课程学习者,聚焦T-SQL完整性约束的动手实践,帮助读者掌握主键、唯一约束、引用完整性与级联引用的创建、删除及测试方法。资源包内含1个docx文件,约711KB,以实验报告形式记录课内任务与思考题,涵盖将pay表No、Year、Month联合设为主键、删除dept表部门名称唯一约束、测试主从表增删改对参照完整性的影响,以及级联更新失败的原因分析。文档还整理了外键约束冲突的报错信息与验证结论,并附课外任务中sc、course表级联更新的测试记录,便于对照复现实验步骤、理解约束被拒绝时的合理处理思路。目前已有343人学习下载,适合正在完成数据库实验或需要梳理完整性约束知识点的学生参考,可快速定位报错原因并完成实验报告撰写。
1. 数据库实验四.docx:一份文档背后藏着的约束与完整性实战
很多人看到「数据库实验四.docx」这个文件名,第一反应是学生作业。但我带过几届新人后发现,这份文档里真正值钱的东西,是它逼着你去动手处理主键、唯一约束、外键和参照完整性这一整套约束体系。你如果只是把 SQL 抄进去跑一遍交差,那确实学不到什么;但如果你把它当成一次完整的约束设计演练,从建表、加约束、测违规插入到排查报错,这套流程走下来,你对关系型数据库的理解会上一个台阶。这篇笔记就是按这个思路展开的:先讲清楚约束到底在管什么,再给出可以直接复现的 T-SQL 操作步骤,最后把我在实际项目里踩过的坑一条条列出来。适合正在做数据库实验的学生,也适合工作中需要维护数据一致性的后端和运维同学。
2. 主键、唯一约束、外键:三种约束到底在管什么
2.1 主键和唯一约束的区别不是「能不能为空」这么简单
很多人背过一句话:主键不能为空,唯一约束可以为空。这话没错,但只停留在表面。真正做设计的时候,你需要理解的是它们在索引层面的差异。主键在 SQL Server 里默认创建一个聚集索引,也就是说表里的数据行物理上就是按主键顺序排列的。唯一约束默认创建的是非聚集索引,数据行的物理顺序不受影响。
这个区别在数据量小的时候感受不到,一旦表上了百万行,聚集索引的选择就直接影响范围查询的性能。我见过一个订单表,主键用了随机生成的 GUID,结果每次插入都导致页分裂,写入性能惨不忍睹。后来把主键改成自增整数,插入性能直接翻了几倍。所以主键选什么列,不只是逻辑设计问题,也是物理设计问题。
唯一约束的典型场景是业务上的唯一标识。比如用户表里手机号必须唯一,但手机号不适合做主键,因为可能为空(未绑定),也可能变更。这时候就用唯一约束来管。一个表可以有多个唯一约束,但只能有一个主键。
还有一点容易忽略:唯一约束对 NULL 的处理。在 SQL Server 中,唯一约束允许存在多个 NULL 值,因为 NULL 不等于 NULL。但如果你用了筛选唯一索引(Filtered Unique Index),就可以做到「只对非空值做唯一性检查」,这在处理软删除场景时特别有用。
2.2 外键和参照完整性:约束不是越多越好
外键的本质是在两张表之间建立一条引用规则:子表里的外键值必须能在父表的主键或唯一键里找到。参照完整性就是靠这个机制来保证的。听起来很美好,但实际项目里外键的使用一直有争议。
支持用外键的理由很直接:数据库层面帮你兜底,应用代码有 bug 的时候不至于产生孤儿数据。反对的理由也很实际:高并发写入场景下,外键检查会增加锁竞争;分库分表之后外键根本没法跨库生效;数据迁移和批量导入的时候外键约束会拖慢速度。
我的经验是:核心业务表之间的强关联关系,该加外键就加,尤其是金融、订单这类不能出错的场景。但日志表、统计表、临时表这类,就别加了,应用层保证就行。另外,外键的级联操作(ON DELETE CASCADE / ON UPDATE CASCADE)要慎用,级联删除在生产环境里一旦触发,可能删掉你意想不到的数据。
2.3 在 SQL Server 里建一张带完整约束的表
下面这段 T-SQL 可以直接在 SSMS 或 Azure Data Studio 里跑。我建了一个简单的选课场景:学生表、课程表、选课表,三张表之间通过主键和外键关联。
-- 先删掉已存在的表,注意删除顺序:先删子表再删父表 IF OBJECT_ID('dbo.Enrollment', 'U') IS NOT NULL DROP TABLE dbo.Enrollment; IF OBJECT_ID('dbo.Course', 'U') IS NOT NULL DROP TABLE dbo.Course; IF OBJECT_ID('dbo.Student', 'U') IS NOT NULL DROP TABLE dbo.Student; -- 学生表:学号做主键,邮箱加唯一约束 CREATE TABLE dbo.Student ( StudentID INT IDENTITY(1,1) NOT NULL, -- 自增主键,避免手动赋值冲突 StudentNo VARCHAR(20) NOT NULL, -- 学号,业务唯一标识 Name NVARCHAR(50) NOT NULL, Email VARCHAR(100) NULL, CreatedAt DATETIME2 DEFAULT SYSDATETIME(), CONSTRAINT PK_Student PRIMARY KEY (StudentID), CONSTRAINT UQ_Student_No UNIQUE (StudentNo), CONSTRAINT UQ_Student_Email UNIQUE (Email) ); -- 课程表:课程编号做主键 CREATE TABLE dbo.Course ( CourseID INT IDENTITY(1,1) NOT NULL, CourseCode VARCHAR(10) NOT NULL, Title NVARCHAR(100) NOT NULL, Credits TINYINT NOT NULL, CONSTRAINT PK_Course PRIMARY KEY (CourseID), CONSTRAINT UQ_Course_Code UNIQUE (CourseCode), CONSTRAINT CK_Course_Credits CHECK (Credits BETWEEN 1 AND 10) ); -- 选课表:复合主键 + 两个外键 CREATE TABLE dbo.Enrollment ( StudentID INT NOT NULL, CourseID INT NOT NULL, EnrollDate DATE NOT NULL DEFAULT GETDATE(), Score DECIMAL(5,2) NULL, CONSTRAINT PK_Enrollment PRIMARY KEY (StudentID, CourseID), CONSTRAINT FK_Enroll_Student FOREIGN KEY (StudentID) REFERENCES dbo.Student(StudentID) ON DELETE CASCADE, CONSTRAINT FK_Enroll_Course FOREIGN KEY (CourseID) REFERENCES dbo.Course(CourseID) ON DELETE NO ACTION, CONSTRAINT CK_Enroll_Score CHECK (Score IS NULL OR (Score >= 0 AND Score <= 100)) );这段代码里有几个设计决策值得说明。StudentID 用了 IDENTITY 自增,这是 SQL Server 里最常见的主键生成方式,好处是插入有序、索引碎片少。StudentNo 单独加了唯一约束,因为学号是业务层面的唯一标识,但可能变更(比如转专业后重新编号),所以不适合做主键。
Enrollment 表用了复合主键 (StudentID, CourseID),这天然保证了同一个学生不能重复选同一门课。两个外键的删除行为不同:学生删除时级联删除选课记录(ON DELETE CASCADE),课程删除时禁止操作(ON DELETE NO ACTION),因为课程被删了但选课记录还在的话,数据就没意义了,应该先处理选课记录再删课程。
CHECK 约束用来做值域限制,Credits 限制在 1 到 10 之间,Score 限制在 0 到 100 之间。这些约束看起来简单,但能挡住不少脏数据。
3. 约束的验证与排错:怎么确认约束真的生效了
3.1 用违规插入来测试约束是否生效
建完表之后,别急着往里灌数据。先做一轮「故意违规」测试,确认每个约束都真的在干活。下面这几条 INSERT 语句,每一条都应该报错。
-- 测试1:主键重复(先插一条正常的) INSERT INTO dbo.Student (StudentNo, Name, Email) VALUES ('S001', N'张三', 'zhangsan@test.com'); -- 再插一条相同 StudentNo 的,应该报唯一约束冲突 INSERT INTO dbo.Student (StudentNo, Name, Email) VALUES ('S001', N'李四', 'lisi@test.com'); -- 预期错误:Violation of UNIQUE KEY constraint 'UQ_Student_No' -- 测试2:外键引用不存在的父记录 INSERT INTO dbo.Enrollment (StudentID, CourseID) VALUES (999, 1); -- 预期错误:The INSERT statement conflicted with the FOREIGN KEY constraint -- 测试3:CHECK 约束违规 INSERT INTO dbo.Course (CourseCode, Title, Credits) VALUES ('C999', N'测试课', 0); -- 预期错误:The INSERT statement conflicted with the CHECK constraint 'CK_Course_Credits' -- 测试4:级联删除验证 INSERT INTO dbo.Course (CourseCode, Title, Credits) VALUES ('C001', N'数据库原理', 3); INSERT INTO dbo.Enrollment (StudentID, CourseID) VALUES (1, 1); DELETE FROM dbo.Student WHERE StudentID = 1; -- 删除学生后,Enrollment 里对应的记录应该也被自动删掉了 SELECT * FROM dbo.Enrollment; -- 应该返回空结果集这几条测试跑完,你对每个约束的行为就有了直观感受。特别是级联删除那条,很多人只在文档里看过,真跑一遍才会意识到它的威力——删一条学生记录,选课表里相关的行全没了。生产环境里如果没想清楚就加 CASCADE,后果可能很严重。
3.2 用系统视图查约束定义和依赖关系
约束建多了之后,光看建表语句容易乱。SQL Server 提供了一组系统视图可以查约束的元数据。
-- 查某张表上所有的约束 SELECT t.name AS TableName, c.name AS ConstraintName, c.type_desc AS ConstraintType, c.definition AS CheckDefinition FROM sys.objects c JOIN sys.tables t ON c.parent_object_id = t.object_id WHERE c.type IN ('PK', 'UQ', 'F', 'C') -- PK=主键, UQ=唯一, F=外键, C=CHECK AND t.name = 'Enrollment' ORDER BY c.type_desc; -- 查外键的引用关系 SELECT fk.name AS FKName, OBJECT_NAME(fk.parent_object_id) AS ChildTable, COL_NAME(fkc.parent_object_id, fkc.parent_column_id) AS ChildColumn, OBJECT_NAME(fk.referenced_object_id) AS ParentTable, COL_NAME(fkc.referenced_object_id, fkc.referenced_column_id) AS ParentColumn, fk.delete_referential_action_desc AS OnDelete, fk.update_referential_action_desc AS OnUpdate FROM sys.foreign_keys fk JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id;第一个查询列出指定表上的所有约束及其类型,CheckDefinition 列会显示 CHECK 约束的具体表达式。第二个查询把外键的父子表、列名和级联行为都列出来了,排查「这个外键到底怎么关联的」时候特别管用。
3.3 修改和删除约束的正确姿势
约束不是建完就不能动的。业务变化了,约束也得跟着调。但修改约束不像改列名那么简单,很多情况下需要先删后建。
-- 删除唯一约束 ALTER TABLE dbo.Student DROP CONSTRAINT UQ_Student_Email; -- 新增一个带条件的唯一索引(只对非空邮箱做唯一检查) CREATE UNIQUE NONCLUSTERED INDEX UQ_Student_Email_Filtered ON dbo.Student(Email) WHERE Email IS NOT NULL; -- 临时禁用外键约束(批量导入数据时常用) ALTER TABLE dbo.Enrollment NOCHECK CONSTRAINT FK_Enroll_Student; -- 导入完成后重新启用 ALTER TABLE dbo.Enrollment CHECK CONSTRAINT FK_Enroll_Student; -- 删除外键 ALTER TABLE dbo.Enrollment DROP CONSTRAINT FK_Enroll_Course;禁用外键约束这个操作要特别小心。NOCHECK 之后,数据库不再验证新插入的数据是否满足外键条件,但已有的数据不受影响。重新启用时用 CHECK CONSTRAINT,SQL Server 会验证现有数据,如果有违规数据,启用会失败。所以批量导入场景下,正确的顺序是:禁用约束 → 导入数据 → 清洗数据 → 启用约束。
4. 避坑指南:主键、外键和唯一约束的五个血泪教训
4.1 坑一:自增主键用完导致插入失败
现象:某张日志表突然开始报错「Arithmetic overflow error converting IDENTITY to data type int」,所有插入操作全部失败。
原因:int 类型的 IDENTITY 最大值是 21 亿多。一张高频写入的日志表,每天插入几百万条,几年下来就把 int 用完了。这个问题在测试环境根本发现不了,因为测试环境的数据量远达不到上限。
解决:建表时预估数据量,高频写入的表直接用 BIGINT 做主键。已经用了 INT 的表,可以通过 ALTER TABLE 修改列类型为 BIGINT,但这个操作会锁表,大表上要安排在维护窗口做。更稳妥的做法是提前监控 IDENTITY 的使用率:
SELECT OBJECT_NAME(object_id) AS TableName, name AS ColumnName, last_value AS CurrentValue, max_value AS MaxValue, CAST(last_value AS FLOAT) / max_value * 100 AS UsagePercent FROM sys.identity_columns WHERE max_value IS NOT NULL ORDER BY UsagePercent DESC;4.2 坑二:外键导致批量删除超时
现象:删除一张父表的旧数据时,DELETE 语句跑了十几分钟还没结束,最后超时回滚。
原因:父表上挂了多个外键,每个外键都有 ON DELETE CASCADE。删除一条父记录时,数据库需要级联删除所有子表里的关联记录。如果子表数据量大且没有对应的索引,每次级联删除都要全表扫描,几条记录还能忍,几万条就是灾难。
解决:两个方向。一是给子表的外键列建索引,这样级联删除能走索引查找而不是全表扫描。二是如果级联删除的数据量确实很大,改成分批删除:先删子表数据,再删父表数据,每批控制在几千条以内。
-- 给外键列建索引 CREATE NONCLUSTERED INDEX IX_Enrollment_StudentID ON dbo.Enrollment(StudentID); CREATE NONCLUSTERED INDEX IX_Enrollment_CourseID ON dbo.Enrollment(CourseID); -- 分批删除示例 WHILE 1 = 1 BEGIN DELETE TOP (5000) FROM dbo.Enrollment WHERE StudentID IN (SELECT StudentID FROM dbo.Student WHERE CreatedAt < '2023-01-01'); IF @@ROWCOUNT < 5000 BREAK; WAITFOR DELAY '00:00:01'; -- 给其他事务留点喘息时间 END;4.3 坑三:唯一约束和 NULL 值的玄学行为
现象:给 Email 列加了唯一约束,但插入多条 Email 为 NULL 的记录时,居然都成功了。团队里有人觉得这是 bug,有人觉得是特性,争论了半天。
原因:SQL 标准里 NULL 不等于 NULL,所以唯一约束对多个 NULL 值不做限制。这不是 SQL Server 的 bug,是标准行为。但很多人第一次遇到时会懵。
解决:如果业务上要求「邮箱要么不填,要么唯一」,那当前行为就是对的。如果要求「邮箱必须填且唯一」,那就把列设为 NOT NULL 再加唯一约束。如果需要「非空值唯一,空值可以重复」,用筛选唯一索引:
CREATE UNIQUE NONCLUSTERED INDEX UQ_Student_Email_Filtered ON dbo.Student(Email) WHERE Email IS NOT NULL;4.4 坑四:Navicat 里设置唯一约束不生效
现象:在 Navicat 的可视化界面里给某列勾了「唯一」选项,保存后插入重复值居然不报错。
原因:Navicat 保存表结构变更时,如果表里已经有重复数据,唯一索引创建会失败,但 Navicat 的报错提示有时候不够明显,容易被忽略。另一种情况是勾选的位置不对——在「索引」标签页里加唯一索引和在「设计表」里勾唯一选项,效果虽然一样,但操作路径不同,容易搞混。
解决:保存后一定要用系统视图确认约束是否真的建上了:
SELECT name, type_desc, is_unique FROM sys.indexes WHERE object_id = OBJECT_ID('dbo.Student') AND is_unique = 1;如果查询结果为空,说明唯一索引没建成功。这时候检查表里是否有重复数据,先清洗再重建。
4.5 坑五:删除主键时遇到 ORA-03113 连接中断
现象:在 Oracle 里执行 ALTER TABLE DROP PRIMARY KEY 时,会话突然断开,报 ORA-03113。
原因:这个错误码表示「通信通道的文件结束」,通常不是主键本身的问题,而是数据库进程崩溃或连接异常中断。常见诱因包括:UNDO 表空间不足、删除主键时级联操作触发了大量回滚、或者数据库实例本身有故障。
解决:先检查数据库的 alert log 和 trace 文件,确认是否有 ORA-00600 之类的内部错误。如果只是 UNDO 不足,扩大 UNDO 表空间后重试。如果是级联删除导致的问题,先禁用相关外键再删主键。另外,删除主键前确认是否有外键引用了它,有的话需要先处理外键。
-- Oracle 中查看引用了某主键的外键 SELECT fk.owner, fk.table_name AS child_table, fk.constraint_name AS fk_name, pk.table_name AS parent_table FROM all_constraints fk JOIN all_constraints pk ON fk.r_constraint_name = pk.constraint_name WHERE pk.table_name = 'YOUR_TABLE' AND fk.constraint_type = 'R';5. 从实验到生产:约束设计的进阶技巧
5.1 用延迟约束解决循环引用问题
两张表互相引用的情况在实际项目里并不少见。比如部门表有一个「负责人」字段指向员工表,员工表又有一个「所属部门」字段指向部门表。建表的时候就会遇到先建哪张表的问题。
SQL Server 不支持延迟约束(DEFERRABLE),但可以通过先建表不加外键、插入数据后再补加外键的方式绕过。PostgreSQL 则支持 DEFERRABLE INITIALLY DEFERRED,可以在事务提交时才检查约束。
-- SQL Server 的变通做法 -- 第一步:建表时不加外键 CREATE TABLE dbo.Department ( DeptID INT PRIMARY KEY, DeptName NVARCHAR(50), ManagerID INT NULL -- 先不加外键 ); CREATE TABLE dbo.Employee ( EmpID INT PRIMARY KEY, EmpName NVARCHAR(50), DeptID INT, CONSTRAINT FK_Emp_Dept FOREIGN KEY (DeptID) REFERENCES dbo.Department(DeptID) ); -- 第二步:插入初始数据 INSERT INTO dbo.Department (DeptID, DeptName, ManagerID) VALUES (1, N'技术部', NULL); INSERT INTO dbo.Employee (EmpID, EmpName, DeptID) VALUES (1, N'张三', 1); -- 第三步:更新循环引用字段,再补加外键 UPDATE dbo.Department SET ManagerID = 1 WHERE DeptID = 1; ALTER TABLE dbo.Department ADD CONSTRAINT FK_Dept_Manager FOREIGN KEY (ManagerID) REFERENCES dbo.Employee(EmpID);这个做法在数据迁移场景里也常用:先灌数据,再补约束,比边灌边检查快得多。
5.2 约束命名规范:别让 sys.objects 里全是乱码
系统自动生成的约束名(比如 PK__Student__1234ABCD)在排查问题时毫无帮助。我习惯在建表时就显式命名所有约束,规则是:前缀表示类型(PK_ / UQ_ / FK_ / CK_ / DF_),后面跟表名和列名。
| 前缀 | 约束类型 | 示例 |
|---|---|---|
| PK_ | 主键 | PK_Student |
| UQ_ | 唯一约束 | UQ_Student_Email |
| FK_ | 外键 | FK_Enrollment_Student |
| CK_ | CHECK 约束 | CK_Course_Credits |
| DF_ | 默认值约束 | DF_Student_CreatedAt |
命名规范的好处是,当你看到错误信息「Violation of UNIQUE KEY constraint 'UQ_Student_Email'」时,不用去查系统视图就知道是哪张表的哪个约束出了问题。
5.3 用事务包裹约束变更
修改约束的操作应该放在事务里,尤其是涉及数据清洗的场景。比如要给某列加唯一约束,但表里已经有重复数据,你需要先删重复再建约束,这两步必须在一个事务里完成,否则中间状态可能被其他会话看到。
BEGIN TRANSACTION; BEGIN TRY -- 删除重复数据,保留每组的最小 ID DELETE FROM dbo.Student WHERE StudentID NOT IN ( SELECT MIN(StudentID) FROM dbo.Student GROUP BY Email ) AND Email IS NOT NULL; -- 加唯一约束 ALTER TABLE dbo.Student ADD CONSTRAINT UQ_Student_Email UNIQUE (Email); COMMIT TRANSACTION; END TRY BEGIN CATCH ROLLBACK TRANSACTION; THROW; -- 把错误抛出去,方便排查 END CATCH;这个模板我用了很多次,核心就是 TRY...CATCH 加显式事务。注意 ALTER TABLE 在 SQL Server 里是 DDL 操作,虽然可以放在事务里,但某些情况下会隐式提交,所以测试环境先验证一遍再上生产。
5.4 验证约束是否覆盖了所有业务规则
最后分享一个我自己的习惯:建完约束后,写一组「应该失败」的测试用例,覆盖所有业务规则。比如「同一个学生不能选同一门课两次」「学分必须在 1 到 10 之间」「删除学生时选课记录要级联删除」。这些用例跑一遍,比看建表语句靠谱得多。
-- 约束验证清单(每条都应该报错或产生预期行为) -- 1. 重复学号 INSERT INTO dbo.Student (StudentNo, Name) VALUES ('S001', N'测试'); -- 2. 重复选课 INSERT INTO dbo.Enrollment (StudentID, CourseID) VALUES (1, 1); INSERT INTO dbo.Enrollment (StudentID, CourseID) VALUES (1, 1); -- 3. 引用不存在的课程 INSERT INTO dbo.Enrollment (StudentID, CourseID) VALUES (1, 9999); -- 4. 学分超范围 INSERT INTO dbo.Course (CourseCode, Title, Credits) VALUES ('C999', N'测试', 11); -- 5. 成绩超范围 UPDATE dbo.Enrollment SET Score = 150 WHERE StudentID = 1 AND CourseID = 1;这套清单我一般会写成 SQL 脚本存进版本控制,每次改表结构后跑一遍。看起来麻烦,但比生产环境出问题再回头查要省事得多。数据库约束这东西,建的时候多花十分钟,后面能省十个小时的排查时间。希望帮到你。
本文还有配套的精品资源,点击获取