☰
数据库系统原理复习:SQL定义、查询与完整性约束易错点
2026/10/31 7:38:21 网站建设 项目流程

简介:这份太原理工大学软件工程专业《数据库概论》课程实验报告,面向正在学习数据库系统原理、需要完成交互式SQL实验或复习课程重点的本科生。文档以Microsoft SQL Server 2016为实验平台,围绕数据库对象管理、表操作与常用SQL语句展开,涵盖基本表的创建、修改和删除,索引与视图的建立,以及数据插入、更新等操作,并延伸到数据完整性、用户鉴别与数据控制等实验内容,可作为实验过程记录与结果整理的参考。资源包共1个docx文件,约1.69MB,内容为完整的实验报告文档,便于直接查阅实验目的、环境配置、操作步骤和结果示例。目前已有311人学习,适合需要对照实验要求梳理SQL语法、补齐报告结构或进行期末复习的读者使用。

1. 知识地图:数据库系统原理这门课的三层结构

数据库系统原理的复习,最容易踩的坑不是语法记不住,而是写完一条语句之后说不清它为什么能跑、为什么该报错。三份实验报告串起来其实是一张三层地图:底层是对象,表、索引、视图;中间是语句,定义、更新、查询;顶层是约束与权限,主键、外键、CHECK、授权。多数人卡在中间层狂背 SELECT,一考外键的级联行为就露馅。这张地图适合两类人:正要交实验、被报错信息折磨的,和期末想把整门课过一遍的。下面按层拆,每层给出必须记住的写法、容易翻车的位置,最后收在一张自查表上。

2. 数据定义层:表、索引、视图的建改删一次讲透

数据定义这部分看着简单,实际上一条CREATE TABLE里塞了列名、数据类型、主键、外键、CHECK、DEFAULT 六种信息,任何一处写错,后面所有增删改查都会跟着报错。实验里先建 Student、Course、SC 三张表,再建 Employee 做练习,顺序本身就有讲究:被引用的表必须先建,引用别人的表必须后建。把这一层吃透的标志是,给你一段业务描述,你能一次写出带完整约束的建表语句,而不是建完再补ALTER。

2.1 建表:数据类型和约束必须一条语句写全

数据类型的选择直接决定后面能不能存进去。学号用CHAR(8)而不是VARCHAR(8),因为学号长度固定,定长类型在比较和索引上都更省事;姓名用CHAR(10)或VARCHAR(8)都行,但要知道CHAR不足长度会用空格补齐,取出来做等值比较时可能莫名不相等。工资用NUMERIC(8,2),意思是总共 8 位有效数字、其中 2 位小数,最大值 999999.99,这个精度写小了后面插工资直接溢出。

-- 学生表:主键写在列级,性别用 CHECK 限死取值,班级非空 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 -- 省略该列插入时自动填 0 ); -- 课程表:主键用了命名约束,方便以后按名字删 CREATE TABLE Course ( Cno CHAR(4) CONSTRAINT PK_Course PRIMARY KEY, Cname CHAR(40), Cpno CHAR(4), -- 先修课号,指向本表的 Cno Ccredit SMALLINT ); -- 选课表:复合主键 + 两个外键,这是全篇最核心的一张表 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) );

这三段代码里有三个必须讲清楚的点。第一,SC 表的主键是(Sno, Cno)复合主键,不能只写Sno,否则一个学生只能选一门课,这是概念课最常考的判断。第二,约束尽量用CONSTRAINT 名字显式命名,比如PK_Course、SC_CHECK、FK_SC_Sno;不命名的话系统会自动生成类似CK__SC__Grade__1A14E395的随机名,后面想DROP CONSTRAINT就得先去系统视图里查名字,纯属给自己找麻烦。第三,字段级约束和表级约束的区别只在于写法位置,功能一样,但涉及多列的主键和外键只能写成表级。

2.2 改表与删表:ALTER 的四类动作

建完表发现少一列、类型不对、约束没加,都要靠ALTER TABLE。它一共就四类动作:加列、改列、加约束、删约束。真正容易翻车的是「改列」——把CHAR改成SMALLINT时,如果表里已有非数字内容,SQL Server 直接拒绝,不会帮你转换;把INT改小成SMALLINT时,如果已有值超出范围,同样报错。

ALTER TABLE Student ADD Sclass CHAR(4); -- 加列,已有行该列自动为 NULL ALTER TABLE Student ALTER COLUMN Sage SMALLINT; -- 改类型,前提是现有数据都能转换 ALTER TABLE Course ADD UNIQUE (Cname); -- 加唯一约束,课程名不许重名 ALTER TABLE Course ADD FOREIGN KEY (Cpno) REFERENCES Course(Cno); -- 自引用外键 ALTER TABLE SC DROP CONSTRAINT SC_CHECK; -- 删约束必须精确到约束名 ALTER TABLE SC ADD CONSTRAINT SC_CHECK CHECK (Grade >= 0 AND Grade <= 100);

最后两行值得单独说:先删后加是「改约束」的标准套路,因为约束不能直接修改,只能删掉重建。删的时候如果当初没命名,就得先查:

-- 查出某张表上所有 CHECK 约束的真实名字 SELECT name, definition FROM sys.check_constraints WHERE parent_object_id = OBJECT_ID('SC');

对比一下DROP TABLE、DELETE FROM、TRUNCATE TABLE三个操作,这是概念题的常客:DROP TABLE连表结构一起删,不可回滚;DELETE FROM只删数据保留结构,可以带 WHERE,可以回滚,会触发触发器;TRUNCATE清空全部行但保留结构,不写日志、不能带 WHERE、重置自增列。实验里删 Employee 表用的是DROP TABLE Employee,删完这张表就彻底不存在了,后悔药没有。

提示:删除顺序必须和外键依赖相反。要先DROP TABLE SC,再删 Student 和 Course,否则外键约束会拦住你。

2.3 索引:普通、唯一、聚簇的差别在哪

索引这部分的考点集中在三种类型的语义差异上。普通索引只是加速查找,允许重复值;唯一索引额外要求列值不重复,插入重复值会报错;聚簇索引决定数据行在磁盘上的物理排列顺序,一张表只能有一个。实验里给 SC 表建的CLUSTERED INDEX iSnoCno ON SC(Sno, Cno DESC)就是典型的复合聚簇索引。

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); -- 主键上再建唯一索引,纯属重复

几个参数层面的细节。复合索引(Sno, Cno)的列序不是随便写的:查询条件只带 Sno 能命中,只带 Cno 就用不上,这叫最左前缀。索引里的DESC影响的是索引内部排序方向,对点查几乎没影响,主要在ORDER BY Cno DESC这类排序场景里减少一次额外排序。iCno这一条是重复劳动——Cno已经是主键,主键自动带唯一索引,再建一个只是多占空间、多拖慢插入。

索引不是越多越好,这是最容易被忽略的成本意识:每建一个索引,所有 INSERT、UPDATE、DELETE 都要额外维护一份 B 树结构。实验阶段表里只有七行数据,建十个索引也感觉不到慢;真实场景下写多读少的表,索引泛滥是性能杀手。

2.4 视图:把复杂查询固化成一张虚拟表

视图本质是一条被存起来的 SELECT 语句,本身不存数据,每次查询视图时底层语句重新执行。它解决三类问题:简化多表连接、隐藏敏感列、给表达式列起个像样的名字。实验里建了三种典型视图,正好对应三种用法。

-- 行列子集视图:只暴露部分行和部分列 CREATE VIEW IS_Student AS SELECT Sno, Sname, Sage FROM Student WHERE Sdept = 'IS'; -- 带聚集函数的视图:必须给列起名,否则列名是 AVG(Grade) 这种表达式 CREATE VIEW S_G(Sno, Gavg) AS SELECT Sno, AVG(Grade) FROM SC GROUP BY Sno; -- 多表连接视图:把三表连接固化,查询时当单表用 CREATE VIEW XK_VIEW AS SELECT Student.Sno, Sname, Cno, Cname, Grade FROM Student, SC, Course WHERE Student.Sno = SC.Sno AND SC.Cno = Course.Cno;

S_G后面的(Sno, Gavg)是列名列表,不能省——AVG(Grade)作为列名既没法引用也不好读。视图定义里的列名列表还有一个用途:当 SELECT 中出现同名列(比如三表连接后有两个 Sno)时,用列名列表强行区分。

视图的可更新性是个高频考点:行列子集视图一般可以 INSERT/UPDATE/DELETE,改动会反映到基表;带聚集函数、GROUP BY、DISTINCT 的视图基本不可更新,因为一行视图数据对应不到唯一一行基表数据。想在视图上限制「插入的数据必须满足视图定义」,加WITH CHECK OPTION:

-- 通过该视图插入的系别必须是 IS,否则拒绝 ALTER VIEW IS_Student AS SELECT Sno, Sname, Sage FROM Student WHERE Sdept = 'IS' WITH CHECK OPTION;

查询视图和查表完全一样,SELECT * FROM IS_Student WHERE Sage > 20,优化器会把视图定义展开成对基表的查询,不会先把整个视图物化出来。这一点决定了视图在性能上基本是零成本的抽象。

3. 语句层:单表、分组、连接、嵌套、集合五级递进

查询是整门课分值最高、也最容易自我感觉良好的部分——单表查询写顺了会以为自己全懂了,一到多表连接和嵌套就开始出玄学结果。这一层的正确复习姿势是按复杂度阶梯往上走,每一级都问自己三个问题:这条语句先执行哪一步、结果集有多少列、哪些行会被过滤掉。五级分别是单表投影筛选、分组统计、多表连接、嵌套子查询、集合运算,下面逐级拆。

3.1 单表查询:八个动作其实是一条 SELECT 的可选配件

单表查询的所有变体,都可以看成SELECT ... FROM ... WHERE ... ORDER BY ...四个槽位里填不同东西。SELECT 槽位管投影(选哪些列、要不要算表达式、起什么别名、去不去重),WHERE 槽位管筛选(比较、范围、集合、模糊、空值、多重条件),ORDER BY 槽位管排序。

-- 表达式列 + 两种别名写法 + 函数处理 SELECT Sname, 2000 - Sage AS YearOfBirth, -- 别名写法一:AS 关键字 Department = LOWER(Sdept) -- 别名写法二:别名 = 表达式 FROM Student; -- DISTINCT 去重:一个学生选多门课就有多条记录,去重后才是人数 SELECT Sno FROM SC; -- 7 条,有重复 SELECT DISTINCT Sno FROM SC; -- 5 条,每个学号只出现一次 -- 四类典型条件 SELECT Sname, Sdept, Sage FROM Student WHERE Sage BETWEEN 20 AND 23; SELECT Sname, Sage FROM Student WHERE Sdept IN ('IS','MA','CS'); SELECT Sname, Sno, Ssex FROM Student WHERE Sname LIKE '刘%'; SELECT Sno, Cno FROM SC WHERE Grade IS NULL; -- 多列排序:先按系升序,同系内按年龄降序 SELECT * FROM Student ORDER BY Sdept ASC, Sage DESC;

几处必须记死的语义。BETWEEN 20 AND 23是闭区间,包含 20 和 23。LIKE '刘%'里%匹配任意长度字符(包括零个),_匹配恰好一个字符,SQL Server 还支持[abc]字符集,但考试一般只考前两个。Grade IS NULL不能写成Grade = NULL,因为 NULL 参与任何比较的结果都是 UNKNOWN,永远不成立,这是 SQL 三值逻辑的第一道坎。ORDER BY的DESC只作用于紧跟它的那一列,ORDER BY Sdept, Sage DESC的实际含义是「Sdept 升序、Sage 降序」,不是「两列都降序」,想两列都降必须写两次。

SELECT Sno FROM SC和SELECT DISTINCT Sno FROM SC的区别,本质上是关系代数里投影是否自动去重的问题。SQL 默认保留重复,因为去重要排序、有成本,这个设计选择在后面UNION与UNION ALL上还会再出现一次。

3.2 分组统计:WHERE 管行,HAVING 管组

聚集函数一共五个:COUNT、SUM、AVG、MAX、MIN。它们的共同点是「把多行压成一行」,所以只要 SELECT 列表里出现聚集函数而没写 GROUP BY,整张表就被压成一行。加了 GROUP BY,就变成「每个组压成一行」。

-- 全局聚集:整张 SC 表压成一行 SELECT COUNT(*) AS 总选课数, COUNT(DISTINCT Sno) AS 选课人数, MAX(Grade) AS 最高分 FROM SC; -- 分组聚集:每门课一行,HAVING 过滤组 SELECT Cno AS 课程号, COUNT(*) AS 人数, AVG(Grade) AS 均分, MAX(Grade) AS 最高分 FROM SC GROUP BY Cno HAVING AVG(Grade) > 85;

COUNT(*)数的是行数,包含 NULL;COUNT(Grade)数的是非 NULL 个数;COUNT(DISTINCT Sno)先按学号去重再数。三者结果可能完全不同:表里 7 条选课记录、5 个不同学生、6 条有成绩,三个数字分别是 7、5、6。这个细节几乎每张卷子都考。

WHERE和HAVING的分工不能混:WHERE 在分组之前过滤行,不能用聚集函数;HAVING 在分组之后过滤组,参数必须是分组列或聚集函数。完整执行顺序是:

  1. FROM定位表,做笛卡尔积
  2. WHERE过滤行
  3. GROUP BY分组
  4. HAVING过滤组
  5. SELECT投影并计算表达式和别名
  6. ORDER BY排序(只有这一步能用 SELECT 里定义的别名)

多表分组时要特别注意连接造成的行膨胀。按班级、按课程统计人数时,Student 和 SC 连接后一个学生会展开成多行,直接COUNT(*)数的是选课记录数,想数人必须COUNT(DISTINCT SC.Sno):

-- 每个班每门课的选课人数、均分、最高分 SELECT Student.Sclass AS 班级, SC.Cno AS 课程号, COUNT(DISTINCT SC.Sno) AS 选课人数, AVG(SC.Grade) AS 均分, MAX(SC.Grade) AS 最高分 FROM Student JOIN SC ON Student.Sno = SC.Sno GROUP BY Student.Sclass, SC.Cno;

3.3 连接查询:三种写法、自连接和外连接

连接查询在同一件事上有三种写法,考试让你「用两种方式写出」时不能慌:连接条件写在 WHERE 里(隐式连接)、写在 JOIN ON 里(显式连接)、用外连接。三种写法的结果在等值连接场景下相同,区别在于可读性和外连接能力。

-- 写法一:连接条件和过滤条件都塞在 WHERE SELECT Student.Sno, Sname, Cno, Grade FROM Student, SC WHERE Student.Sno = SC.Sno AND SC.Cno = '2' AND SC.Grade > 90; -- 写法二:连接条件放 ON,过滤条件放 WHERE,语义边界清晰 SELECT Student.Sno, Sname, Cno, Grade FROM Student JOIN SC ON Student.Sno = SC.Sno WHERE SC.Cno = '2' AND SC.Grade > 90; -- 写法三:左外连接,没选课的学生也保留,课程列为 NULL SELECT Student.Sno, Sname, Cno, Grade FROM Student LEFT OUTER JOIN SC ON Student.Sno = SC.Sno;

写法一和写法二在只涉及内连接时完全等价,但一旦升级到外连接,位置写错就会静默出错。LEFT JOIN SC ON Student.Sno = SC.Sno AND SC.Cno = '2'和LEFT JOIN SC ON Student.Sno = SC.Sno WHERE SC.Cno = '2'结果完全不同:前者保留所有学生、只有 2 号课有成绩,后者因为 WHERE 把 NULL 行全部滤掉,外连接退化成了内连接。这类错误不报错、只出错数据,是最难自查的一类。

自连接是指一张表和自己连接,必须起两个别名,否则系统分不清。找「间接先修课」就是典型:课程 1 的先修课是 5,课程 5 的先修课是 7,那么 1 的间接先修课就是 7。

-- first 是当前课,second 是它的直接先修课,再取 second 的先修课即间接先修课 SELECT first.Cno AS 课程号, second.Cpno AS 间接先修课 FROM Course first, Course second WHERE first.Cpno = second.Cno;

三表连接则是把链式关系一路接下去。查「选修了数据库系统原理的同学的学号和成绩」,路径是 Student → SC → Course,两个连接条件缺一不可:

SELECT Student.Sno, Sname, Grade FROM Student, SC, Course WHERE Student.Sno = SC.Sno AND SC.Cno = Course.Cno AND Course.Cname = '数据库系统原理';

注意:n 张表连接至少需要 n-1 个连接条件,少写一个就退化成笛卡尔积,行数瞬间爆炸,而且不报错。

3.4 嵌套查询:IN、比较、ANY/ALL、EXISTS 四种引子

嵌套查询是把一条 SELECT 的结果当作另一条 SELECT 的条件。按引出的运算符分四类,按执行方式分两种:不相关子查询(子查询不依赖外层,只算一次)和相关子查询(子查询引用外层列,外层每行都要代入算一次)。

-- IN 引出:和刘晨同系的学生,子查询只算一次 SELECT Sno, Sname, Sdept FROM Student WHERE Sdept IN (SELECT Sdept FROM Student WHERE Sname = '刘晨'); -- 比较运算符 + 相关子查询:找出超过本人平均分的课程 SELECT Sno, Cno, Grade FROM SC x WHERE Grade >= (SELECT AVG(Grade) FROM SC y WHERE y.Sno = x.Sno); -- ALL 引出:比计算机系所有学生年龄都小,且不在计算机系 SELECT Sname, Sage FROM Student WHERE Sage < ALL (SELECT Sage FROM Student WHERE Sdept = 'CS') AND Sdept <> 'CS'; -- EXISTS 引出:选修了 1 号课程的学生 SELECT Sname FROM Student WHERE EXISTS (SELECT 1 FROM SC WHERE SC.Sno = Student.Sno AND Cno = '1');

第二段的相关子查询是重点难点:x是外层别名,y是内层别名,y.Sno = x.Sno把内外层绑在一起,外层每取一行,内层就重算一次该学生的平均分。别名漏写或者绑错,语句照样能跑,结果全错。

EXISTS的语义只判断子查询有没有返回行,所以SELECT 1、SELECT *效果完全一样,写SELECT 1更清楚。EXISTS真正的威力在双重否定表达「全部」这个量词——查「选修了全部课程的学生」,SQL 没有全称量词,只能写成「不存在一门课,他没有选」:

-- 双重 NOT EXISTS:外层遍历学生,中层遍历课程,内层检查该生是否选了该课 SELECT Sname FROM Student s WHERE NOT EXISTS ( SELECT 1 FROM Course c WHERE NOT EXISTS ( SELECT 1 FROM SC WHERE SC.Sno = s.Sno AND SC.Cno = c.Cno ) );

这三层嵌套读起来绕,翻译成中文就一句:没有哪门课是这个学生没选的。这类题在卷面上出现频率不高,但一旦出现就是压轴,值得单独抄一遍背下来。

3.5 集合查询:并、交、差的方向性

集合运算把两个查询结果当成两个集合处理,要求列数相同、对应列的数据类型兼容。三个运算符的语义和数学上一一对应,但UNION有个默认行为必须记住:它会自动去重,等价于UNION ALL之后再DISTINCT。

-- 并:计算机系的学生,或者年龄不大于 19 的学生 SELECT * FROM Student WHERE Sdept = 'CS' UNION SELECT * FROM Student WHERE Sage <= 19; -- 交:既是计算机系,年龄又不大于 19 SELECT * FROM Student WHERE Sdept = 'CS' INTERSECT SELECT * FROM Student WHERE Sage <= 19; -- 差:是计算机系,但年龄大于 19(A 有 B 没有) SELECT * FROM Student WHERE Sdept = 'CS' EXCEPT SELECT * FROM Student WHERE Sage <= 19;

UNION去重需要排序,数据量大时明显比UNION ALL慢;如果业务上确定两个结果集不可能重复,写UNION ALL是更优选择。EXCEPT有方向性,A EXCEPT B和B EXCEPT A是两个完全不同的结果,这点和交、并不同。集合运算符的优先级低于AND/OR,混用时必须加括号,否则语义会和你想的不一样。

4. 完整性层:五类约束加触发器的控制链

前面两层解决「怎么建、怎么查」,这一层解决「数据库凭什么拦住你」。数据完整性分三类:实体完整性管主键不能空不能重,参照完整性管外键必须指向存在的行,用户定义完整性管业务规则比如成绩必须在 0 到 100 之间。三类各自对应不同的约束,出问题时对应的报错号也不一样,记住报错号能省一半排错时间。最后还有权限控制,管的是「谁能动这张表」。

4.1 实体完整性:主键与唯一约束不是一回事

主键约束和唯一约束都能保证列值不重复,区别有三条:一张表只能有一个主键,但可以有很多唯一约束;主键列不允许 NULL,唯一约束在 SQL Server 里只允许一个 NULL(标准 SQL 允许多个,这是实现差异);主键会自动创建一个聚簇索引(默认情况下),唯一约束创建的是非聚簇唯一索引。

-- 重复主键:报错 2627,违反主键约束 INSERT INTO Student VALUES('20100001','李斌','男',20,'CS','1001',0); -- 主键插 NULL:报错 515,不能将 NULL 插入列 Sno INSERT INTO Student VALUES(NULL,'李斌','男',20,'CS','1001',0); -- 唯一约束冲突:课程名重复,同样报错 2627 INSERT INTO Course VALUES('8','JAVA',7,3); INSERT INTO Course VALUES('9','数据结构',7,3); -- 数据结构已存在

还有一个更隐蔽的场景:修改主键值。UPDATE Student SET Sno='20100025' WHERE Sno='20100021'这条语句能不能执行成功,不取决于 Student 表本身,而取决于 SC 表的外键有没有配ON UPDATE CASCADE。如果没配,而且这个学生有选课记录,就会报 547 外键冲突。很多人在这一步以为是主键出了问题,其实是被引用方拦住了。

4.2 参照完整性:外键的四个动作和插入顺序

外键约束规定「子表的值必须在父表里存在」。它带来的直接后果是操作顺序被绑死:插入时先父后子,删除时先子后父。实验里插 SC 表的数据必须在 Student 和 Course 都插完之后,否则报 547。

外键还规定了当父表行被删除或更新时子表怎么办,一共四个动作:

动作删除父表行时的行为适用场景
NO ACTION(默认)有子行就拒绝删除保守策略,最安全
CASCADE级联删除所有子行订单-订单明细这类强依赖
SET NULL子表外键列置 NULL外键列允许为空时
SET DEFAULT子表外键列置默认值有默认值兜底时
-- 级联删除与级联更新:删学生时自动删掉他的选课记录 ALTER TABLE SC DROP CONSTRAINT FK_SC_Sno; ALTER TABLE SC ADD CONSTRAINT FK_SC_Sno FOREIGN KEY (Sno) REFERENCES Student(Sno) ON DELETE CASCADE ON UPDATE CASCADE;

ON DELETE CASCADE是双刃剑:方便,但也意味着一条 DELETE 可能删掉几百行你没想到的数据。生产环境里很多团队明令禁止级联删除,要求应用层显式处理,这个取舍值得知道。

自引用外键是这一节最绕的地方。Course 表的Cpno指向自己的Cno,于是出现一个死结:插入课程 1 时它的先修课是 5,但 5 还没插进去,外键检查通不过;想先插 5,5 的先修课是 7,7 又没插。这不是语句写错,是数据本身的循环依赖。标准解法有两个:建表时先不加这个外键,所有课程数据插完再ALTER TABLE ADD CONSTRAINT(此时会全表校验,数据合法就成功);或者先把Cpno全部插成 NULL,插完再逐条UPDATE回填。

4.3 用户定义完整性:CHECK 的边界和三值逻辑

CHECK约束管的是单行内的业务规则,比如性别只能是男女、成绩必须在 0 到 100 之间。它的语法简单,但有一个反直觉的行为:CHECK 表达式结果为 UNKNOWN 时视为通过。

-- 这条约束看起来能拦住非法成绩,实际上拦不住 NULL ALTER TABLE SC ADD CONSTRAINT SC_CHECK CHECK (Grade >= 0 AND Grade <= 100); INSERT INTO SC VALUES('20100003','1',NULL); -- 成功:NULL 比较结果是 UNKNOWN,不算违反

原因在于NULL >= 0的结果是 UNKNOWN 而不是 FALSE,CHECK 只在结果为 FALSE 时才拒绝。所以「成绩必须在 0 到 100」这个业务规则,如果还想禁止空成绩,必须同时加Grade SMALLINT NOT NULL。这是三值逻辑在约束层的第一次露面,和WHERE Grade = NULL永远不成立是同一个根源。

边界值取开区间还是闭区间也要想清楚。Grade > 0 AND Grade < 100意味着 0 分和 100 分都插不进去,而实际业务里这两个分数都合法,所以正确写法是>= 0 AND <= 100。反过来,如果规则是「成绩必须为正」,那就得用> 0,不能照抄闭区间。

DEFAULT约束的行为也容易误解:它只在 INSERT 语句省略该列时生效,显式写 NULL 不会触发默认值。

-- Stotal 有 DEFAULT 0,下面两条结果不同 INSERT INTO Student(Sno,Sname,Ssex,Sclass) VALUES('20100102','张盛','男','1008'); -- Stotal = 0 INSERT INTO Student VALUES('20100103','李四','男',19,'CS','1008',NULL); -- Stotal = NULL

4.4 触发器与权限:约束兜不住的地方谁来管

约束能表达的规则有限,跨表校验、复杂业务逻辑就要靠触发器。触发器是绑在表上的存储过程,在 INSERT/UPDATE/DELETE 前后自动执行,内部可以用两张逻辑表:inserted存新行,deleted存旧行。

-- 在 SC 表上做成绩校验:CHECK 已经能做,这里演示触发器的写法 CREATE TRIGGER trg_sc_check ON SC AFTER INSERT, UPDATE AS BEGIN IF EXISTS (SELECT 1 FROM inserted WHERE Grade < 0 OR Grade > 100) BEGIN RAISERROR('成绩必须在 0 到 100 之间', 16, 1); ROLLBACK TRANSACTION; -- 必须回滚,否则语句已经生效 END END;

三个关键点。第一,inserted和deleted是集合不是单行,批量插入 100 行时触发器只触发一次,所以判断必须写成IF EXISTS (SELECT 1 FROM inserted WHERE ...),不能假设只有一行。第二,触发器里必须显式ROLLBACK,因为 AFTER 触发器执行时语句已经改过数据了,只报错不回滚等于没拦住。第三,能用 CHECK 解决的规则优先用 CHECK,触发器开销更大、调试更难、还容易嵌套调用,属于「约束实在表达不了」才动用的工具。

权限控制这一块实验只点到了标题,按标准 SQL 补全最小模型:

GRANT SELECT ON Student TO U1; -- 给 U1 查学生表的权限 GRANT SELECT, UPDATE(Sage) ON Student TO U1; -- 列级授权:只能改年龄列 REVOKE UPDATE ON Student FROM U1; -- 收回 DENY SELECT ON SC TO U1; -- 显式拒绝,优先级最高

GRANT、REVOKE、DENY三者的优先级是DENY最高:即使通过角色继承了权限,只要在某个用户上显式 DENY,那项权限就不生效。这是 SQL Server 的扩展,标准 SQL 只有前两个。

5. 易错点清单:五个不报错却出错数据的地方

这一章专门收那些「语句能跑、结果不对」的坑。报错反而是好事,至少系统告诉你有问题;真正拖时间的错误是静默的语义偏差,跑完看起来正常,一对答案才发现全错。下面五条按「易漏点 → 为什么漏 → 怎么补」写,都是复习阶段反复栽跟头的位置。

5.1 自引用外键的插入死锁

易漏点:Course 表建了Cpno指向自己的Cno,然后按课程号 1 到 7 的顺序插入数据,第一条('1','数据库系统原理','5',4)就报外键冲突 547,因为 5 号课还不存在。

为什么漏:教科书上外键的例子都是两张不同的表,插入顺序天然是先父后子,没人提醒你「父子在同一张表」时这个顺序可能根本不存在。而且这条语句单独拿出来看完全合法,报错信息也只说「与外键约束冲突」,不会告诉你循环依赖。

怎么补:两条路任选。一是建表时不加这个外键,全部课程插完再ALTER TABLE Course ADD FOREIGN KEY (Cpno) REFERENCES Course(Cno),加约束时做一次全表校验,数据合法就通过。二是先插Cpno为 NULL 的行,再UPDATE回填。写实验报告时把这一段写进去,比罗列十条正常 INSERT 有价值得多。

5.2 CHECK 放过 NULL 和边界值写反

易漏点:CHECK (Grade > 0 AND Grade < 100)看起来对,实际把 0 分和 100 分这两个合法值挡在外面;CHECK (Grade >= 0 AND Grade <= 100)看起来更对,但允许Grade IS NULL悄悄插进去。

为什么漏:前者是把数学区间的书写习惯直接搬过来,没想清楚业务语义;后者是三值逻辑的隐蔽性,NULL 参与比较得到 UNKNOWN,而 CHECK 只在结果为 FALSE 时拒绝,UNKNOWN 被当成通过,这个规则几乎没人第一遍就记住。

怎么补:写 CHECK 之前先问两句话——「端点值合不合法」「这个字段允不允许空」。需要禁止空值就加NOT NULL,不能指望 CHECK 代劳。验证方法是主动插一条边界数据:插 0 分、插 100 分、插 NULL,看三条里面哪条被拒绝,被拒绝的就是写错了。

5.3 分组查询里混入非分组列

易漏点:SELECT Sno, Sname, AVG(Grade) FROM SC GROUP BY Sno,想「顺便」把姓名带出来,结果 SQL Server 直接报错,提示 Sname 既不在 GROUP BY 里也不在聚集函数里。

为什么漏:单表查询写顺手了,觉得 SELECT 想加什么列就加什么列。分组之后每一行代表一个组,一个学号对应一个组,但一个组里可能有多条 Sname 相同的记录——数据库没法替你决定显示哪一个,所以干脆禁止。MySQL 在宽松模式下会随便挑一个值返回,跨数据库迁移时这种代码就是定时炸弹。

怎么补:记住一条铁律,GROUP BY之后 SELECT 列表只允许出现两类东西——GROUP BY 里列出的列,和聚集函数。真需要带出姓名,要么把 Sname 也加进 GROUP BY(前提是函数依赖成立),要么先用子查询算出每组聚集值再 JOIN 回原表。

5.4 相关子查询的别名漏绑

易漏点:想查「超过本人平均分的课程」,写成SELECT Sno, Cno FROM SC x WHERE Grade >= (SELECT AVG(Grade) FROM SC y),忘了写WHERE y.Sno = x.Sno,结果内层算的是全表平均分,每个人都拿去和同一个数比,语句不报错,结果全错。

为什么漏:漏写关联条件后语句语法完全合法,数据库没有任何理由报错。而且结果集的行数可能看起来「差不多」,不仔细核对根本发现不了。相关子查询和不相关子查询在写法上只差一个 WHERE,语义上差了一个数量级。

怎么补:写完嵌套查询后做一次「别名检查」——内层 FROM 里的每个别名、外层 FROM 里的每个别名,两者之间的关联条件在哪一行、写没写。再补一个小数据集手算:找两三个学生,手动算他们的平均分,看查询结果对不对得上。这一步花三分钟,能省半小时找错。

5.5 NOT IN 遇上 NULL 和外连接条件放错位置

易漏点:WHERE Sno NOT IN (SELECT Sno FROM SC)想找没选课的学生,结果一行都查不出来;LEFT JOIN ... WHERE 右表列 = 某值想保留左表全部行,结果外连接退化成了内连接。

为什么漏:NOT IN的子查询结果里只要有一个 NULL,整个比较就变成 UNKNOWN,所有行都被滤掉——这是三值逻辑第三次出现,但它的表现形式和前两次完全不同,很难联想到一起。外连接那条更隐蔽:SQL 语法允许你把右表的过滤条件写在 WHERE 里,但语义上等于先做外连接、再筛掉 NULL 行,等于白做。

怎么补:NOT IN换成NOT EXISTS,语义更稳,不受 NULL 影响:

-- 找没选过任何课的学生,NOT EXISTS 不会因为 NULL 失效 SELECT Sno, Sname FROM Student s WHERE NOT EXISTS (SELECT 1 FROM SC WHERE SC.Sno = s.Sno);

外连接的过滤条件一律写在ON里而不是WHERE里:LEFT JOIN SC ON Student.Sno = SC.Sno AND SC.Cno = '2'。养成这个习惯,外连接就不会莫名其妙变成内连接。

6. 复习清单:把三份实验报告压成一张自查表

复习到这一步,剩下的不是再刷题,而是把已经写过的东西变成可验证的清单。我的做法是把每条知识点翻译成一个「能动手验证

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

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

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

立即咨询