简介:《数据库系统概论(第5版)王珊版》课后习题答案文档,专为正在学习数据库原理的高校学生、考研复习者以及需要备课的教师准备。内容围绕关系数据库核心理论展开,覆盖关系模型三部分、关系数据语言分类、实体完整性及参照完整性规则、关系代数五种基本运算及派生运算、SQL语言五大特点等高频考点,并配有第2章和第3章的详细解题过程与SQL查询语句示例,如SPJ数据库的各类查询、建表语句及连接运算辨析。整份资源为单个doc文档,约588KB,内容编排紧凑,便于按章节查阅或打印使用。目前已有1723人学习下载,读者可通过对照练习检验自身掌握程度,加深对数据库概念与SQL实践的理解,适合复习备考和日常自学巩固。
1. 数据库系统概论第5版:这份课后答案到底能帮你解决什么
做过数据库课程设计的人大概都有这种经历:明明照着教材敲的 SQL,一跑全是报错;明明背得出范式的定义,一拿到表结构就不知道从哪下手。这份《数据库系统概论》(第5版)课后答案,正好把这类问题挡在门外。它覆盖关系模型、关系代数、SQL、安全性、完整性、范式判断和数据库设计七章内容,每个关键习题都给了可执行的答案和推导过程,适合正在学数据库基础课、准备期末或者做课程设计前想系统过一遍的人。我把它拆开讲一遍,重点放在"答案背后的逻辑"和"照抄也会踩的坑"上。
2. 关系模型与关系代数:SPJ 查询从自然语言到表达式的拆解
2.1 关系模型三组件:数据结构、操作集合、完整性约束怎么配合
关系模型的核心不是一张张表,而是三个组件协同工作:关系数据结构、关系操作集合、关系完整性约束。数据结构解决"数据长什么样"——关系、元组、属性、码这些概念都归它管;操作集合解决"能对数据做什么"——选择、投影、连接、除这些运算都归它管;完整性约束解决"什么数据是合法的"——实体完整性、参照完整性、用户定义完整性都归它管。三者缺一个,关系模型都不成立。
实体完整性这条规则很直接:主属性不能取空值。也就是说,一张表的主码一旦定了,任何一行数据的主码列都不能是 NULL。这条规则看起来简单,但在实际建表时经常被忽略,尤其复合主码的情况——比如 SPJ 表用 (SNO, PNO, JNO) 三列做复合主码,有人只给其中一列加了 NOT NULL,另外两列允许空,这种表能建出来,但已经违反了实体完整性。
参照完整性稍微绕一点:外码属性的值要么为空,要么等于被参照表某个元组的主码值。这里有个关键前提——外码属性本身不是主属性时才能为空。原文里专门强调了这个条件:"属性 F 本身不是主属性,则可以取空值,否则不能取空值。"举个例子,SPJ 表里的 SNO 是外码,但如果它同时也是复合主码的一部分,那就不能为空,必须真实引用到 S 表中存在的供应商。这个细节在考试里是高频考点,在实际建表时也是外键约束配置的依据。
2.2 五种基本运算与 SPJ 查询推导:为什么先选择再投影
关系代数的基本运算只有五种:并、差、笛卡尔积、投影、选择。交、连接、除这三种运算都可以用基本运算表达出来。理解这个层级关系比背公式更重要——碰到复杂查询时,先想能不能拆成这五种基本运算,再想有没有更简洁的复合运算可用。
原题给出的 SPJ 数据库包含 S、P、J、SPJ 四个关系模式,五道查询题非常有代表性。第一题最简单:求供应工程 J1 零件的供应商号码 SNO。表达式是 πSno(σJno='J1'(SPJ))。逻辑是先对 SPJ 表做选择,筛选出 JNO 等于 J1 的行,再做投影,只保留 SNO 列。选择在前、投影在后,这个顺序不是随便定的——先选择能把行数降下来,减少后续运算的开销。
第三题开始有难度:求供应工程 J1 零件为红色的供应商号码 SNO。答案分两步:
πSno(πSno,Pno(σJno='J1'(SPJ)) ⋈ πPno(σColor='红'(P)))这里的核心手法是"先缩小再连接"。左侧先对 SPJ 做选择(限 J1 工程)和投影(只留 SNO、PNO),右侧先对 P 做选择(限红色零件)和投影(只留 PNO),两侧做完投影后再做自然连接。为什么要先投影?因为 P 表有 PNAME、COLOR、WEIGHT 多个属性,如果不先把无关列去掉,自然连接的结果里会带出一堆用不上的列。两侧连接后,得到的就是"供应了 J1 工程且零件是红色"的 (SNO, PNO) 配对,最后再投影出 SNO。整个运算过程每一层都在缩小中间结果。
第四题考察的是差运算:求没有使用甲城供应商生产的红色零件的工程号 JNO。表达式是 πJno(SPJ) - πJno(σCity='甲城'∧Color='红'(S⋈SPJ⋈P))。思路是先求出"所有供应过零件的工程号",再减去"使用过甲城红色零件的工程号",剩下的就是没使用过的。这种"全体减反例"的思考方式在关系代数里很常见,翻译成 SQL 就是 NOT IN 子查询。
第五题是除法:求至少用了供应商 S1 所供应的全部零件的工程号 JNO。表达式是 πJno,Pno(SPJ) ÷ πPno(σSno='S1'(SPJ))。除法的含义是"包含全部"——被除关系里某个 JNO 对应的 PNO 集合,必须完全包含除数里 S1 供应的全部 PNO。我在实际教学里看到不少同学卡在这题上,建议先分别算出两侧的中间结果,再手动配对验证,理解除法比背公式靠谱得多。
2.3 等值连接与自然连接:区别、联系与转换到 SQL 的映射
等值连接和自然连接的区别,原文给的答案很精炼:等值连接是从两个关系的广义笛卡尔积中选取 A、B 属性值相等的元组,连接运算符是"=";自然连接是特殊的等值连接,要求比较的分量是相同的属性组,而且结果中去掉重复属性列。
| 对比项 | 等值连接 | 自然连接 |
|---|---|---|
| 连接条件 | 任意属性值相等 | 相同属性组的值相等 |
| 重复列 | 保留(可能有两列同名) | 自动去掉重复列 |
| 表达自由度 | 可指定不同属性名 | 属性名必须相同 |
实际写 SQL 时,等值连接对应 INNER JOIN ... ON,自然连接在很多数据库里有 NATURAL JOIN 语法,但线上环境我一般不建议用 NATURAL JOIN——它自动匹配同名列,表结构一变更就容易查出意想不到的结果,这个坑在排障时很隐蔽。
关系代数与 SQL 的映射关系也值得单独梳理一遍:选择对应 WHERE 子句,投影对应 SELECT 子句,并对应 UNION,差可以用 EXCEPT 或 NOT IN 表达,连接对应 JOIN 或逗号加 WHERE。原文第 2 章习题 6 的五道查询,到了第 3 章就是同一组语义换成 SQL 重写,这种"同一批问题、两种语言解法"的对照非常有价值——关系代数是思考工具,SQL 是落地工具,两者要能在脑子里自由切换。
3. SQL 落地:四个表的建表、查询更新与视图的边界
3.1 建表:C(2) 与 CHAR(2) 的差异,以及主码、外码、UNIQUE 的排布
原题要求用 SQL 语句建立 S、P、J、SPJ 四个表。原文给出的建表语句用的是 VFP 风格的写法,比如 Sno C(2)、Sname C(6) 这种类型声明,这在标准 SQL 里跑不通。标准 SQL 应该用 CHAR(n) 声明定长字符列。下面是改写后的标准写法:
CREATE TABLE S ( Sno CHAR(2) PRIMARY KEY, Sname CHAR(6) NOT NULL, Status CHAR(2), City CHAR(4) ); CREATE TABLE P ( Pno CHAR(2) PRIMARY KEY, Pname CHAR(6), Color CHAR(2), Weight INT ); CREATE TABLE J ( Jno CHAR(2) PRIMARY KEY, Jname CHAR(8), City CHAR(4) ); CREATE TABLE SPJ ( Sno CHAR(2) NOT NULL, Pno CHAR(2) NOT NULL, Jno CHAR(2) NOT NULL, Qty INT, PRIMARY KEY (Sno, Pno, Jno), FOREIGN KEY (Sno) REFERENCES S(Sno), FOREIGN KEY (Pno) REFERENCES P(Pno), FOREIGN KEY (Jno) REFERENCES J(Jno) );这段建表语句里有两个细节值得展开。第一,S 表的 Sno 用 PRIMARY KEY 声明,等价于非空加唯一,比原文的 UNIQUE 更严谨——UNIQUE 本身允许一个 NULL 值,而主码不允许空值,语义上有差别。第二,SPJ 表的三列都是 NOT NULL,同时组合成复合主码,意味着这是"一个供应商给一个工程供应一种零件"的事实表,每一行都必须是完整的三元组。外键约束在最后的 FOREIGN KEY 子句里集中声明,这是标准 SQL 的推荐做法。
原文把第 5 章习题 6 的建表题也放在这里对比:DEPT 和 EMP 两个表,EMP 里有 CHECK (Age<=60) 的约束,外键 FK_DEPTNO 引用 DEPT(Deptno)。建表时把完整性约束就地声明,是最不容易遗漏的方式——如果等到应用层才做校验,数据库里已经可能混进脏数据了。
3.2 查询与更新:DISTINCT、NOT IN、UPDATE 和 DELETE 的先后顺序
原题针对四个表完成第 2 章习题 6 中的查询,SQL 版本的核心写法如下:
-- (1) 求供应工程 J1 零件的供应商号码 SELECT DISTINCT SNO FROM SPJ WHERE JNO='J1'; -- (2) 求供应工程 J1 零件 P1 的供应商号码 SELECT DISTINCT SNO FROM SPJ WHERE JNO='J1' AND PNO='P1'; -- (3) 求供应工程 J1 零件为红色的供应商号码 SELECT DISTINCT SNO FROM SPJ, P WHERE JNO='J1' AND SPJ.PNO=P.PNO AND COLOR='红'; -- (4) 求没有使用甲城供应商生产的红色零件的工程号 SELECT DISTINCT JNO FROM SPJ WHERE JNO NOT IN ( SELECT JNO FROM SPJ, P, S WHERE S.City='甲城' AND COLOR='红' AND S.SNO=SPJ.SNO AND P.PNO=SPJ.PNO );第一、二题加了 DISTINCT,因为同一个供应商可能给 J1 工程供多种零件,不加会查出重复行。第三题是两表连接,连接条件写在 WHERE 里,这是传统的隐式连接写法,教学场景清晰易懂。第四题的 NOT IN 子查询对应关系代数里的差运算——先查出"用了甲城红色零件的 JNO"集合,再用 NOT IN 排除。
更新操作这部分,原题最值得反复看的是第 7 问"找出没有使用甲城产的零件的工程号码",它明确写了两种写法及各自适用条件:
-- 写法一:NOT IN 子查询,适用于 JNO 唯一或不唯一 SELECT DISTINCT JNO FROM SPJ WHERE JNO NOT IN ( SELECT DISTINCT JNO FROM SPJ, S WHERE S.SNO=SPJ.SNO AND S.City='甲城' ); -- 写法二:<> 比较,只适用于 JNO 唯一的情况 SELECT DISTINCT JNO FROM SPJ, S WHERE S.SNO=SPJ.SNO AND S.City<>'甲城';这两种写法结果不一致的根源在于 NULL 值的比较语义:<> 比较遇到 NULL 会得到 UNKNOWN,行会被过滤掉。只要 SPJ 表里有 JNO 为 NULL 的行(理论上有外码约束不应该有,但脏数据场景常见),写法二就会漏数据。原题的"注意"批注就是在提醒这个坑,我在实际排障时遇到过不止一次——用 <> 排除了某条件,结果 NULL 行跟着一起消失,查半天查不出来。
删除操作的顺序问题也藏在原题第 10 问里:删除供应商 S2 的记录,要从 S 表和 SPJ 表分别删除,必须先删 SPJ 再删 S,否则外键约束会直接拒绝删除。INSERT 则注意列顺序和字符串引号,原题的 VALUES ('S2', 'J6', 'P4', 200) 四个值对应四列,类型要匹配。
3.3 视图 VSP:虚表概念、创建语句与"可更新视图"的限制
原题要求为某建设工程项目建立一个供应情况视图,包括 SNO、PNO、QTY:
CREATE VIEW VSP AS SELECT SNO, SPJ.PNO, QTY FROM SPJ, J WHERE SPJ.JNO=J.JNO AND J.JNAME='某建设工程项目'; -- (1) 找出该工程项目使用的各种零件代码及其数量 SELECT DISTINCT PNO, QTY FROM VSP; -- (2) 找出供应商 S1 的供应情况 SELECT * FROM VSP WHERE SNO='S1';视图是"虚表"这个说法很多人理解不到位。视图不存储数据,数据库里只存放视图的定义,查询视图时实时从基本表取数。这意味着基本表数据变了,视图查出来的结果也变——这是视图和临时表的根本区别。
关于可更新视图,原题的结论是:行列子集视图一般是可更新的;属性来自集合函数或表达式的视图肯定不可更新。判断标准在于"更新能否唯一有意义地转换为对基本表的更新"。比如 VSP 视图,每行都能追溯到 SPJ 表的一行,UPDATE 可以转换;但如果是 SELECT MAX(工资) 这种聚合视图,一行对应多行基本表数据,UPDATE 不知道该改哪一行,只能拒绝。视图的三个核心用途——简化操作、提供多角度视角、对机密数据做安全保护——在后续章节的授权组合里还会用到,比如先建"部门工资"视图再授权,就能让用户看聚合结果而看不到个人工资明细。
4. 安全性与完整性:GRANT、审计、六类约束和范式判定
4.1 安全性控制:五类手段与 GRANT/REVOKE 授权模型
数据库安全性保护的是"防止不合法使用造成的数据泄露、更改或破坏",原文列了五类常用手段,我整理成一张对照表:
| 手段 | 作用层次 | 典型应用 |
|---|---|---|
| 用户标识和鉴别 | 进入系统前 | 口令、生物识别 |
| 存取控制 | 访问数据时 | DAC(自主)+ MAC(强制) |
| 视图机制 | 数据呈现时 | 隐藏敏感列 |
| 审计 | 操作发生后 | 审计日志追踪 |
| 数据加密 | 存储/传输时 | 密文存储、加密传输 |
GRANT 和 REVOKE 是自主存取控制的落地语句,原题有一组很完整的授权例子:
-- (a) 用户 A 对两个表有 SELECT 权力 GRANT SELECT ON 职工, 部门 TO A; -- (b) 用户 B 对两个表有 INSERT 和 DELETE 权力 GRANT INSERT, DELETE ON 职工, 部门 TO B; -- (d) 用户 C 对职工表有 SELECT 权力,对工资字段有 UPDATE 权力 GRANT SELECT, UPDATE(工资) ON 职工 TO C; -- (f) 用户 D 对两个表有所有权力,并能继续授权 GRANT ALL PRIVILEGES ON 职工, 部门 TO D WITH GRANT OPTION; -- (g) 用户 E 只能看各部门最高/最低/平均工资,不能看个人工资 CREATE VIEW 部门工资 AS SELECT 部门.名称, MAX(工资), MIN(工资), AVG(工资) FROM 职工, 部门 WHERE 职工.部门号=部门.部门号 GROUP BY 职工.部门号; GRANT SELECT ON 部门工资 TO E;这里有个容易忽略的细节:UPDATE(工资) 这种列级授权,只给用户更新某一列的权限,其他列看得到但改不了。视图授权组合更是经典——用户 E 连职工表的 SELECT 权限都没有,只能查聚合视图,个人工资明细完全隔离。这是最实用的最小权限落地方式。
对应的权限回收用 REVOKE:REVOKE SELECT ON 职工, 部门 FROM A; 如果当初授权时带了 WITH GRANT OPTION,回收时也别忘了加 CASCADE,否则用户转授出去的权限会残留。原题第 8 问最后还专门有一句 DROP VIEW 部门工资,授权回收和视图删除是两件事,漏了后者视图定义还在数据库里。
4.2 完整性约束:六类约束、违约处理与参照完整性的三种策略
数据完整性关注的是"数据的正确性和相容性",和安全性的区别在于:完整性防的是不合语义的数据,安全性防的是非法用户和非法操作。原文把完整性约束分成六类——静态列级、静态元组、静态关系、动态列级、动态元组、动态关系。这个分类的底层逻辑是两个维度:静态还是动态(数据是否在变化)、约束作用在哪个层次(列、元组、关系)。
DBMS 的完整性控制机制要具备三个功能:定义(声明约束)、检查(验证操作是否违反约束)、违约反应(拒绝或执行附加操作)。原文第 5 章习题 7 的结论非常实用:违反实体完整性和用户定义完整性的操作,一般都拒绝执行;但违反参照完整性的操作,不一定是简单拒绝,需要根据应用语义选择策略。
参照完整性的三种策略是级联删除(CASCADES)、受限删除(RESTRICTED)、置空值删除(NULLIFIES)。典型场景:删除部门表中某部门时,如果职工表里还有该部门的员工,级联删除会连员工一起删,受限删除会报错拒绝,置空值删除会把员工的部门号置为 NULL。选哪种取决于业务语义——员工离职档案要保留就用受限删除,临时分组解散用置空值删除更合理。建表时对应 FOREIGN KEY ... ON DELETE CASCADE 这类子句,别建完表再改,生产环境改外键策略代价很大。
4.3 范式判定实操:学生班级系学会案例的函数依赖分析
范式这一章是整份答案里最需要"动手推"的部分。原题给了"系、学生、班级、学会"四个关系模式,要求写出每个关系模式的最小函数依赖集、候选码、外部码,并判断是否存在传递函数依赖。先看学生 S 的模式:
S(Sno, Sname, Sbirth, Dept, Class, Rno)函数依赖集:Sno→Sname,Sno→Sbirth,Sno→Class,Class→Dept,Dept→Rno。候选码是 Sno;外部码是 Dept 和 Class。传递依赖有两处:Sno→Dept 且 Dept→Rno,所以 Sno 与 Rno 之间存在传递函数依赖;Class→Dept 且 Dept→Rno,Class 与 Rno 之间也传递。
班级 C 的模式:C(Class, Pname, Dept, Cnum, Cyear)。函数依赖:Class→Pname,Class→Cnum,Class→Cyear,Pname→Dept。候选码是 Class。因为 Class→Pname 且 Pname→Dept,Class 与 Dept 之间存在传递依赖。系 D 的模式:D(Dept, Dno, Office, Dnum)。Dept 和 Dno 互相决定,候选码可以是 Dept 或 Dno 中任意一个,且不存在传递依赖。学会 M 的模式最简单:M(Mname, Myear, Maddr, Mnum),Mname 是唯一决定因素,无传递依赖。
原题还有一个经典结论:任何二目关系都属于 3NF 和 BCNF。证明思路是把二目关系 R(A1, A2) 的依赖关系分成三种情况——A1→A2 但 A2↛A1、A1→A2 且 A2→A1、码为 (A1, A2)——逐一验证每个决定因素都包含码。这个结论好用,但只适用于二目关系,三目以上的关系要先找全体候选码再逐个判断,直接套结论会翻车,这个在避坑章节里我会单独展开。
5. 避坑手册:这份答案里最容易被误读的五个细节
5.1 用 "<>" 排除某个城市,结果把 NULL 行也弄丢了
现象:第 3 章习题里求"没使用甲城零件的工程号",用S.City<>'甲城'查出来的结果比预期少,丢了几行。
原因:SQL 的三值逻辑。当 City 为 NULL 时,City<>'甲城'的结果是 UNKNOWN,不是 TRUE,WHERE 子句只保留 TRUE 的行,NULL 行就被默默过滤了。原答案特意批注"适用于 JNO 是唯一的情况",就是因为唯一 JNO 不会引入 NULL 干扰。
解决:统一用 NOT IN 子查询写法,或者显式加OR City IS NULL。我现在的习惯是排查类查询一律先跑一遍SELECT COUNT(*) FROM 表 WHERE 目标列 IS NULL,确认没有 NULL 干扰再写排除条件,这个动作 30 秒能省下半天排查时间。
5.2 删供应商记录时先删父表,外键直接报错
现象:执行DELETE FROM S WHERE SNO='S2',数据库报外键冲突,删除被拒绝。
原因:SPJ 表里还有 SNO='S2' 的供应记录,外键约束阻止父表被引用行删除。原题答案特意把删除分成两步——先删 SPJ 的对应记录,再删 S 表记录,顺序反了就翻车。
解决:先查后删,SELECT * FROM SPJ WHERE SNO='S2'确认子表数据已清理,再删父表。生产环境建议用事务包裹,两条 DELETE 放同一事务里,中途出错可以回滚。
5.3 REVOKE 之后忘了 DROP VIEW,敏感数据还在漏
现象:给用户 E 的"部门工资"视图授权已回收,但用户仍能查到聚合数据。
原因:视图本身还在。REVOKE 收回的是权限,视图定义存放在数据字典里,除非显式执行 DROP VIEW,否则视图依然存在。原题第 8 问的答案里REVOKE SELECT ON 部门工资 FROM E和DROP VIEW 部门工资两条语句是配套出现的,缺一不可。
解决:删除授权时同步检查是否有配套视图,先 DROP VIEW 再 REVOKE,或者反过来都行,关键是别漏。我在清理历史权限时会把查询到的视图清单逐一比对,确认没有"死授权活视图"的组合。
5.4 VFP 风格的 C(2) 直接搬到标准 SQL,类型对不上
现象:照抄原答案里的Sno C(2)建表,在 MySQL 或标准 SQL 环境里直接报语法错误。
原因:C(2) 是 VFP 的字符类型写法,等价于标准 SQL 的 CHAR(2),但语法不通用。原书的建表习题在某些版本里保留了这个方言写法。
解决:统一改写为 CHAR(2),并顺手把 UNIQUE 改成 PRIMARY KEY——主码本身就要求非空唯一,语义更严谨。这个坑提示了一个通用习惯:拿到任何资料里的 SQL,先确认目标数据库方言,再决定是否原样执行。
5.5 "二目关系都是 BCNF"当万能结论用,三目以上直接套
现象:做范式判断题时,对三目关系直接套"任何二目关系都是 BCNF"的结论,判断出错。
原因:这个结论有严格的前提——关系只有两个属性。三目关系里属性间的依赖组合复杂得多,必须逐一找候选码、检查每个非平凡函数依赖的决定因素是否包含码。
解决:每次判断前先数属性个数,二目关系可以秒判;三目及以上就老老实实走流程:列函数依赖集、找候选码、检查部分依赖和传递依赖。血泪经验是——尤其考试时,别图快,一步步写推导过程,反而容易拿分。
6. 数据库设计六阶段与一个验证表结构的土办法
数据库设计过程原文概括为六个阶段:需求分析、概念结构设计、逻辑结构设计、数据库物理设计、数据库实施、数据库运行和维护。这六个阶段不是一次走完就结束,实际项目里往往是反复迭代的。概念结构设计阶段形成的 E-R 图独立于具体 DBMS,逻辑结构设计阶段才把 E-R 图转换成关系模型,物理设计阶段再考虑存储结构和索引。
这里分享一个我一直在用的验证表结构土办法:写完逻辑设计后,别急着建表,先把每个关系模式的函数依赖集写出来,然后做一次"候选码检查"。具体做法分三步:第一,列出所有属性和候选码;第二,逐个检查非主属性是否完全函数依赖于候选码(有没有部分依赖);第三,检查是否存在非主属性之间的传递依赖。任何一步发现问题,就回到逻辑设计阶段拆表重来。
比如设计一个订单系统,先写出 O(OrderId, CustomerId, CustomerName, ProductId, Qty),一眼就能看出问题:CustomerName 只依赖 CustomerId 而不依赖 OrderId,主码是复合的 (OrderId, ProductId),这里就有部分函数依赖,这个表连 2NF 都够呛。正确做法是拆分成订单表、客户表、订单明细表,各表单独满足至少 3NF 再合并。这个检查过程不需要任何工具,一张纸一支笔就能做,但能挡住绝大多数更新异常——数据冗余、删除丢失、修改不一致这些都是在范式不达标时埋下的雷。
从那以后我每次设计表结构,都强制走一遍"属性清单加函数依赖集"的检查流程,哪怕只有四五张表也不跳过。这个习惯帮我挡掉了至少三次上线后才发现的数据问题,成本几乎为零,收益却很大。如果你正在做数据库课设或者准备面试中的设计题,不妨也把这个流程固定下来。希望帮到你。
本文还有配套的精品资源,点击获取