简介《数据库系统原理与设计》第四版课后答案文档面向正在系统学习数据库原理课程的高校学生与自学者尤其适合需要逐题核对概念理解、梳理知识框架的备考人群。文档以 doc 格式收录了教材课后习题的详细解答重点围绕数据、数据库、数据库系统、数据库管理系统等核心概念展开并针对文件系统与数据库系统的区别和联系、使用数据库系统的好处、适合采用文件系统或数据库系统的典型场景等内容给出严谨解析可帮助读者快速掌握绪论章节的高频考点与答题思路。资源包共 1 个 doc 文件大小仅 230KB轻量便于下载与查阅。目前已有 89 人学习该文档适合配合教材或复习笔记使用尤其对准备期末考试、考研专业课中数据库基础概念部分的同学有直接参考价值。1. 别再对着 PDF 抄答案了第四版课后题到底在考什么如果你正在学数据库原理手里拿着《数据库系统原理与设计》第四版的课后习题却在网上翻到一份标题写着“课后答案-第四版.doc”的文件那么大概率你和我当年一样陷入了“抄完答案、合上书全忘”的死循环。这份 doc 文档的价值不在于让你把第 3 章的 15 道题一字不差地背下来而在于它揭示了这门课真正要考核的四类能力关系代数与 SQL 的等价转换、范式分解与函数依赖推导、事务并发控制的正确性证明、以及索引与查询优化的代价估算。换句话说考试和面试不会问“第三题选什么”只会换个场景让你推演“这个表该不该拆、这个事务会不会死锁、这条 SQL 为什么慢”。这篇文章不是让你去背那份 doc而是陪你把它从头到尾吃透一遍。我会把第四版课后题里最常翻车的几类题目拆开直接给到可以复现的解法、可执行的验证命令还有那些教科书里不会写、但做题和面试一定会踩的坑。新手可以按章节顺序跟着推熟手可以直接跳到第 4 章看并发控制的坑或者第 5 章看那套索引优化的验证脚本。目标只有一个下次再遇到类似的题你能不看答案自己推出来。2. 关系代数与 SQL 等价转换课后题前两章的隐藏主线2.1 为什么你写的 SQL 和答案给出的关系代数对不上第四版课后题的前两张表面上是让学生熟悉选择和投影的符号写法实际上真正在考察的是“关系代数表达式到 SQL 的翻译规则”。很多学生翻车在同一个点上把“找出所有选修了至少两门课程的学生”翻译成 SQL 时习惯性地用了WHERE里加子查询而答案给出的是基于除运算Division或分组计数的关系代数写法。这两种写法在逻辑上等价但答案的组织顺序暴露了出题人想要你掌握的求解思路先找全集再用除法筛掉不满足条件的元组。我在做模拟项目X 的课程设计时把第三版的老题翻出来对比过第四版最大的改动就是去掉了大量“纯背诵型”的符号判断题换成了“给一段业务描述写出关系代数、再转成 SQL、最后反推是否等价”的综合题。这意味着你光会SELECT * FROM是不够的你得能说清楚为什么π之后还能跟GROUP BY。这里我给出一个最小可复现的转换思路用一个学生选课的例子来演示。假设有三张表-- 学生表 CREATE TABLE student ( sid CHAR(4) PRIMARY KEY, sname VARCHAR(20) ); -- 课程表 CREATE TABLE course ( cid CHAR(4) PRIMARY KEY, cname VARCHAR(30) ); -- 选课表 CREATE TABLE sc ( sid CHAR(4), cid CHAR(4), score NUMERIC(4,1), PRIMARY KEY (sid, cid) );题目问查询“选修了课程号为‘C1’的学生姓名”。关系代数的标准写法是π_sname (student ⨝ (σ_cidC1 (sc)))对应 SQL 是SELECT s.sname FROM student s JOIN sc ON s.sid sc.sid WHERE sc.cid C1;这段 SQL 的逻辑顺序是先对sc做选择WHERE再与学生做连接JOIN最后投影出姓名SELECT。注意SQL 的书写顺序和执行顺序并不一致而关系代数是按“先选择、再连接、后投影”的顺序推演的。课后答案里喜欢把这个顺序写得很清楚但很多同学看到SELECT在最前面就误以为“先投影、再连接”这是第一类最常见的理解错位。2.2 除运算的两种等价写法与参数选择第四版课后题里最让新手头疼的是“选修了全部课程的学生”这一问。关系代数的标准答案是除运算π_sid, cid (sc) ÷ π_cid (course)。但除运算并不是所有数据库系统都直接支持你在 SQL 里必须把它转成“双重否定”的形式先找出不存在“某课程该学生没选”的学生。SELECT DISTINCT sc1.sid FROM sc sc1 WHERE NOT EXISTS ( SELECT cid FROM course WHERE cid NOT IN ( SELECT cid FROM sc sc2 WHERE sc2.sid sc1.sid ) );逻辑说明最内层子查询找出学生sc1.sid已选的课程集合中间的NOT IN判断是否还有课程不在这个集合里最外层的NOT EXISTS表示“不存在这样一门他漏选的课程”。参数sc1和sc2是同一个表的两个别名这是为了区分外层行的当前学生和内层行集合的归属少了别名直接报错或者逻辑错乱。这组 SQL 的验证方式很简单造一组数据让一个学生选满全部课程另一个学生缺一门跑一下应该只返回第一个。如果结果多了大概率是NOT IN里遇到了NULL值这是经典坑后面专门讲。2.3 第三个高频转换场景分组筛选与 Having 的关系代数表达还有一个高频题眼查询平均成绩大于 80 分的学生学号。很多人的第一反应是写WHERE AVG(score) 80然后数据库直接报错因为WHERE不能跟聚合函数。关系代数里对应的是“先分组、再做聚集运算、再对组进行筛选”SQL 里就得用GROUP BY加HAVING。SELECT sid FROM sc GROUP BY sid HAVING AVG(score) 80;这里最容易出错的地方是把HAVING当成“二次 WHERE”来写比如对某个学生的单科成绩做筛选。记住一个判定口诀过滤元组用WHERE过滤分组用HAVING。第四版课后答案里对这一题的标准求解过程往往会先写一个γ分组聚集运算的表达式再翻译成上面的 SQL。你要是直接跳过关系代数只背 SQL面试官一追问“你这里能不能用子查询实现”你就容易卡住。3. 范式分解与函数依赖第四版课后题的分水岭3.1 从 1NF 到 BCNF先判断依赖再动手拆表范式题是整份 doc 答案里题型最固定、但得分率最低的板块。典型题目是给你一个关系模式R(A,B,C,D,E)和一组函数依赖F问你属于第几范式然后要求无损连接地分解到 3NF 或 BCNF。很多同学拿到题就急着画表其实第一步永远是找候选键。我自己的习惯是先写出所有函数依赖然后用“属性闭包法”挨个试。比如有关系模式R(U, F)其中U {A, B, C, D}F {A→B, BC→D}。第一步求A的闭包从A出发由A→B得到{A, B}再看BC→D需要B和C但闭包里没有C所以停止。A的闭包就是{A, B}不是候选键。再试AC的闭包从{A, C}出发A→B得到{A, C, B}BC→D的条件满足得到{A, C, B, D}覆盖全部属性所以AC是候选键。这一步做完判断范式就顺了非主属性B和D对候选键AC是否存在部分依赖A→B说明B依赖于A这个候选键的真子集所以是部分依赖那么R最多属于 1NF连 2NF 都到不了。第四版课后题在这个位置的设问密度比第三版高很多基本每题都要求你写出完整的判断依据光写一个“因为存在部分依赖”是不够的必须指明是哪个依赖、哪个属性对哪个候选键的子集产生了依赖。3.2 无损连接分解的算法与参数设定分解到 3NF 的标准算法是“正则覆盖 候选键补充”这部分课后答案里一般只给分解结果不给推导中间态导致学生抄完仍然不会推。我建议你做到一个式子都别跳先把函数依赖集化为最小覆盖再去掉冗余属性然后按每个依赖分组最后检查是否有任意一组包含候选键。下面这个例子取自某高校数据库课程的模拟项目X给定R(A,B,C,D,E,H)函数依赖集F {A→BC, E→D, B→C, D→A}。第一步求候选键从E出发没有任何直接依赖但E→D后D→A得到AA→BC得到B和C最终闭包覆盖全部属性所以E是唯一候选键。第二步检查范式E是单属性候选键不存在部分依赖所以至少满足 2NF但B→C是一个传递依赖E→B→C所以不满足 3NF。分解时按依赖分组得到三个关系模式资源描述R1(A, B, C)R2(E, D)R3(D, A)此时要验证无损连接和依赖保持。无损连接的验证方法是用“追踪算法”或者直接构造一张例如 3 行 6 列的表格按依赖逐步填充。这里我把验证过程写成一个可以手动执行的模板也可以用 SQL 去建临时表做模拟-- 用 SQL 模拟无损连接验证仅演示结构 CREATE TABLE r1 AS SELECT DISTINCT A, B, C FROM R; CREATE TABLE r2 AS SELECT DISTINCT E, D FROM R; CREATE TABLE r3 AS SELECT DISTINCT D, A FROM R; -- 重新连接后检查元组是否与原始表一致 SELECT COUNT(*) AS original_cnt FROM R; SELECT COUNT(*) AS joined_cnt FROM r1 JOIN r2 ON r2.D (SELECT D FROM R WHERE R.E r2.E) JOIN r3 ON r3.A r1.A;参数说明r1、r2、r3是分解后的三张表连接的条件必须基于函数依赖中的属性比如r3和r1通过A连接r2和r3通过D连接。如果最终连接后的行数与原始表一致说明无损连接成立。这里的JOIN条件看起来有点绕是因为r2里只有E和D而E是候选键必须靠D去找R中的对应行。你在手工推演时建议直接用表格法反而比 SQL 直观。3.3 你大概率漏掉的“依赖保持”验证第四版课后答案里有一个几乎每份 doc 都会收录、但学生很少做对的隐藏考点分解到 3NF 后如何验证依赖保持。很多人的直觉是“只要把原来的依赖拆到各个子模式里就是保持”但实际上需要把分解后的所有依赖并集取闭包再看是否覆盖原函数依赖集。举个我印象很深的翻车例子R(A,B,C)F {A→B, B→C, C→A}某同学按“每个依赖一个表”直接分解成R1(A,B)、R2(B,C)、R3(C,A)。直觉上感觉没问题可这个分解其实依赖保持了但无损连接不成立——三个表无法通过自然连接还原出原始的R无损关键信息。为了真正检验依赖保持你需要手算并集闭包对{A→B, B→C, C→A}求并集计算闭包时发现A→C可以由A→B→C推出所以分解后的依赖集在逻辑蕴含上是够的依赖保持成立。但如果你把R1(A,B)和R2(A,C)两个表自然连接得到的是笛卡尔积的子集行数可能膨胀出现“假元组”这就是无损连接的破绽。做题时的坑就藏在这里依赖保持和无损连接这两个性质必须分别验证一个成立不能推出另一个。第四版的课后题里至少有两道题专门用来打破“分解就是无损”的错觉你要是只记住所谓“3NF 分解标准算法”不做追踪表验证考场上大概率会栽。4. 事务并发控制课后答案里最抽象的章节其实能用 SQL 模拟4.1 用两个会话来复现“丢失更新”和“不可重复读”事务这一章的课后题理论性很强考的都是可串行化判定、两阶段锁协议、死锁预防等概念。但光靠背概念完全没有手感我建议你直接在本地数据库里开两个会话把四种隔离级别一一跑一遍。下面是一个能复现“丢失更新”的最小操作序列我以 MySQL 的 InnoDB 为例但思路对任何关系型数据库都适用。-- 会话 1 SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; START TRANSACTION; SELECT balance FROM account WHERE id 1; -- 得到 100 -- 会话 2 SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; START TRANSACTION; UPDATE account SET balance balance 50 WHERE id 1; COMMIT; -- 会话 1 再次执行 UPDATE account SET balance balance - 30 WHERE id 1; COMMIT; -- 最终 balance 70而不是期望的 120 或 80 中的正确值逻辑说明会话 1 先读到余额 100会话 2 把余额更新成 150 并提交会话 1 在不知道自己读的是旧值的情况下基于旧值“100 - 30 70”写回覆盖了会话 2 的更新这就是典型的丢失更新。参数balance的初始值设为 100 是为了让计算过程肉眼可见。把隔离级别换成READ COMMITTED或REPEATABLE READ这种丢失更新问题依旧可能发生真正能防住的是SELECT ... FOR UPDATE或者乐观锁版本号机制。第四版课后题里关于“两阶段锁协议为什么能避免丢失更新”本质就是让事务持有排他锁直到提交上面这段 SQL 没有加锁所以翻车是必然反而正好说明不加锁的后果。4.2 死锁检测的四个必查状态与参数调整死锁题是第四版课后题里最让考生焦虑的部分因为答案还需要画出等待图。等待图做题时是纸上的节点和箭头但实际上在数据库里可以通过查询系统表来观察。下面是检查 MySQL 当前锁等待和死锁信息的常用命令-- 查询当前是否有锁等待 SHOW ENGINE INNODB STATUS; -- 查看 information_schema 中的锁信息 SELECT * FROM performance_schema.data_locks; SELECT * FROM performance_schema.data_lock_waits;参数说明data_locks表记录当前持有的锁和等待中的锁data_lock_waits表记录阻塞关系。做课后题时遇到“A 等待 B、B 等待 C、C 等待 A”的问题纸上画三角很简单但真实环境里你需要在这两张表里找到环的链路。步骤是先查data_lock_waits拿到REQUESTING_ENGINE_TRANSACTION_ID和BLOCKING_ENGINE_TRANSACTION_ID再按事务 ID 连回data_locks表看锁类型。如果多个事务互相持有对方需要的锁就可以判定死锁。关于死锁的课后题我最想提醒的一点是不要把“死锁”和“饿死”混为一谈。死锁是两个或多个事务循环等待饿死是某个事务一直拿不到锁但其他事务并没有形成环。第四版课后答案里专门有一道辨析题不少同学写反了。判定的关键还是看等待图是否有环没有环就不是死锁可能只是长时间锁等待。4.3 可串行化判定冲突可串行化与视图可串行化的取舍这一节的课后题一般会给两个调度问是否冲突可串行化。很多人只会画优先级图但碰到视图可串行化就开始懵。我推荐一个笨但有效的办法把每个事务的操作序列写成“读集”和“写集”先做冲突分析做不出来再审视图等价。第四版答案里有一道经典调度题T1: R(A) W(A) R(B) W(B)与T2: R(A) W(A) R(B) W(B)交替执行。若调度为T1 的 R(A)、T2 的 R(A)、T1 的 W(A)、T2 的 W(A)...来来回回优先级图必然有环结论是不可冲突串行化。这时你需要检查最终写入值看是否与某个串行调度一致。如果一致则是视图可串行化但非冲突可串行化这是面试和考试最容易挖坑的地方。不要看到优先级图有环就直接写“不可串行化”题目只问“是否可串行化”视图等价也算。5. 索引与查询优化把课后题里的代价估算变成可验证的 SQL 实验5.1 B 树索引的扇出、高度与磁盘 IO 估算这一章在第四版课后答案 doc 里占比不小题目通常给你一个表行数为n每个索引节点能存k个键值问索引高度、查询需要的磁盘 IO 次数。公式很简单但很多人总在“取整方向”上翻车。我给出一个可复现的计算模板。假设一张表有 1,000,000 行每个叶子节点能存 100 个索引项每个内部节点能存 100 个孩子指针。那么第一层叶子节点数量是ceil(1,000,000 / 100) 10,000第二层内部节点数量是ceil(10,000 / 100) 100第三层内部节点数量ceil(100 / 100) 1因此索引高度为 3根节点算第一层。查询一条记录的磁盘 IO 次数就是索引高度加一次回表共 4 次。import math rows 1_000_000 leaf_capacity 100 internal_capacity 100 # 计算 B 树层数 level_nodes rows height 1 while level_nodes 1: level_nodes math.ceil(level_nodes / leaf_capacity if height 1 else level_nodes / internal_capacity) height 1 print(f索引高度{height}) print(f单点查询预期 IO{height 1})逻辑说明第一层用叶子容量除第二层起用内部节点容量除这是与 B 树的构建方式相对应的。如果你把leaf_capacity和internal_capacity都调成 50高度会变成 4单点查询 IO 变为 5。课后答案里容易错的点恰恰是在这里叶子节点和内部节点的可容纳键值数不一定是同一个数题目如果没有说明默认相同但一旦说明不同就必须分开算。5.2 用 EXPLAIN 验证索引是否真的被用上课后题中关于“某条 SQL 是否适合建索引”的判断最后都可以落到EXPLAIN上验证。我在某图像处理 Demo 的数据库优化中反复用过这一招效果稳定。下面用一段 SQL 模拟一个高频查询并展示如何读执行计划。-- 建表并插入 10 万行数据 CREATE TABLE users ( id INT PRIMARY KEY, email VARCHAR(100), age INT, INDEX idx_age (age) ); -- 查询年龄等于 25 的用户 EXPLAIN SELECT * FROM users WHERE age 25;执行计划中如果key列显示idx_age说明索引被用上了如果type是ALL说明是全表扫描索引没生效大概率是因为age列上存在隐式类型转换或者函数包裹。比如把age这列定义为VARCHAR查询时传入整数索引就会失效。这是第四版课后题中“何时索引不被使用”的一个典型考察点。参数说明key表示实际使用的索引名rows表示估算扫描行数filtered表示过滤比例。做实验时可以对比有无索引的rows值通常差一个数量级。课后题里的“使用 B 树索引后查询代价从 O(n) 降到 O(log n)”用EXPLAIN看到的rows从 100000 降到几百就是这个理论的直接体现。5.3 最左前缀原则的边界哪些情况索引会失效第四版课后题对复合索引的考察已经从“背最左前缀”进化到“给一条 SQL 判断是否走索引”。比如表上有复合索引idx_a_b_c (a, b, c)查询条件是WHERE b 1 AND a 2很多人认为不走索引因为书写顺序没按a, b, c。实际上优化器会做重排a在最左所以会走索引。反过来WHERE c 1 AND b 2因为缺少a无法使用这个复合索引的完整能力但可能用索引下推做部分过滤。真正让索引完全失效的场景是在索引列上做运算或函数操作比如WHERE age 1 26或是WHERE LEFT(email, 3) abc。这些在课后题的答案里通常只给结论不给原因我建议你亲自跑一遍EXPLAIN观察type从ref变成ALL的过程印象会深很多。6. 避坑指南课后答案 doc 里最容易误导你的 5 个地方6.1 现象答案里的“可串行化调度”在实际库中会被间隙锁破坏课后题答案默认数据库使用的是严格的两阶段锁协议但在真实数据库里尤其是 MySQL InnoDB 的REPEATABLE READ隔离级别下间隙锁可能让一个在纸面上可串行化的调度出现幻读或者产生额外的锁等待。原因教材为了简化假设锁只加在记录上但 InnoDB 为了防止幻读会给范围条件加间隙锁或临键锁锁的粒度比教材模型粗。解决做课后题时如果题目没有特别说明“仅考虑记录锁”你在分析锁等待时就要把间隙锁考虑进去。真实验证时可以执行SHOW ENGINE INNODB STATUS看LOCK WAIT部分是否存在gap lock字样。我在某跨平台系统的一次压测里就是被间隙锁坑到 TPS 掉到底事后看等待图才发现两个事务锁定了相邻的范围而不是同一行。6.2 现象无损连接验证结果和答案对不上因为连接顺序错有些同学在手工追踪表中填完一轮依赖后发现某些标记填不上就断言“分解不是无损的”结果答案是“无损”。我见过最多次翻车的原因是没有反复迭代填充直到闭包不再增长。原因追踪算法需要循环应用所有函数依赖直到某一行填满所有属性只扫一遍依赖是不够的。课后答案 doc 里一般直接给“可加标记”的最终状态不给中间迭代过程学生照猫画虎只能看个寂寞。解决严格按照“逐行扫描、逐依赖应用、重复直到没有新标记”的流程来。我习惯用一张草稿纸做三列初始标记、第一次迭代后的标记、第二次迭代后的标记。如果第三轮没有变化再下结论。一个实用的技巧是先找分解中是否有某个模式包含候选键如果有无损连接多半成立追踪表只是形式化验证。6.3 现象NOT IN子查询返回空结果导致整个查询失败关系代数除运算转 SQL 时很多同学喜欢用NOT IN而不是NOT EXISTS。当子查询结果中包含NULL值时NOT IN的行为是“整体为 UNKNOWN”最终结果集会变空。这在课后题数据里不常见但一旦题目往course表里加了一条cid IS NULL的记录答案就会变得很奇怪。原因NOT IN的本质是对的取反NULL 任何值都是UNKNOWN取反还是UNKNOWN所以不满足条件直接过滤掉所有行。解决做除运算时一律使用NOT EXISTS不仅逻辑更安全执行计划也更稳定。如果你很想用NOT IN请先确保子查询列上有IS NOT NULL过滤条件。6.4 现象B 树高度算出来是 2但题目答案写 3课后题里 B 树高度计算最常见的问题是“根节点是否算一层”。有的教材把根节点不算层只算叶子以上有几层于是差出一个数字。我在 A 同学的项目评审中就见过这个争执。原因不同教材约定不一致第四版课后答案本身也没有统一口径有时“高度”指从根到叶的节点个数有时指“需要读几次磁盘索引页”。解决先看 doc 答案里的计算过程是几行算是几层。如果题目没有说就按“根到叶共几层”来写并备注一句自己的定义这样批改时不会判错。6.5 现象3NF 分解结果正确但检查依赖保持时居然不成立这是课后题里最劝退的一类题明明按算法分好了却丢掉了一个依赖。常见原因是在求正则覆盖时把冗余依赖删错了把一个由其他依赖推出来的依赖当成冗余删掉但分解后的各个模式里又没有同时包含它的左右属性于是依赖就丢了。解决每删一个依赖都要先闭包验证。我一般把每个候选依赖的左部闭包写出来如果闭包包含右部说明这个依赖是冗余的才可删。不是把一眼看起来“没用”的依赖直接删。第四版课后答案里至少有两道题专门让人栽在这容错率很低。7. 把课后答案用出面试价值的最后一招反向出题与代价估算看完第六部分的坑你已经具备了解题的能力但距离“融会贯通”还差一步。我建议你不要再做答案的消费者而是反过来当出题人。拿出一套课后题盖住答案尝试自己修改题目条件去考自己。比如原题是“关系模式 R(A,B,C,D) 求候选键”你改成“如果函数依赖集多一个 C→B结果会怎样”。这种变体训练能让你在面试时面对陌生问题迅速定位到“这题在考函数依赖闭包”的本质。另外一个值得养成的习惯是把每一道课后题的结论量化。例如问“B 树索引查询代价是多少”不要满足于“log n”你可以直接用第 5 章的 Python 脚本去算具体高度。这样回答面试官时你能给出“100 万行数据、每个叶子节点 100 个键高度是 3单点回表总共 4 次 IO”这样的具体数字比说一句“比较快”要可信得多。这也是为什么我在这篇文章里反复强调可复现实验理论题的答案在真实数据库里都应该能找到对应物。如果你愿意多走一步可以试着把课后题里的关系模式、函数依赖定义成 SQL 建表语句直接让数据库帮你验证无损连接是否成立。这个过程未必比手算快但能强迫你用严格的语法去写关系模式是很好的互补训练。准备面试或考研复试时这份 doc 里的“含金量”不在答案本身而在你能否不看答案把每一步推导的动机说清楚。我在多年前准备某公司的技术面试时就是靠把一个课后题翻来覆去改条件最终在面试里遇到一道相似的范式题时能快速给出候选键和分解结果。希望这个反向出题的思路也能帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?