news 2026/10/14 6:55:09

数据库原理习题到工程能力:范式分解、SQL优化与事务并发实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
数据库原理习题到工程能力:范式分解、SQL优化与事务并发实战

简介:这份资源是钱学忠《数据库原理及技术》教材的配套习题答案,面向高校数据库课程学生、备考人员及自学者,用于课后练习核对与知识点巩固。内容覆盖数据库设计、SQL语言、关系数据库理论、DBMS与数据库管理等核心章节,可帮助读者理清E-R模型转换、关系代数演算、范式规范化及并发控制、备份恢复等难点。资源以rar压缩包形式提供,整体约16.69MB,上游未提供具体文件数量与类型明细,下载后可直接对照教材章节查阅。目前已有693人学习下载,适合需要系统梳理数据库概念、验证习题思路并提升设计与运维能力的读者参考使用。

1. 数据库原理及技术(钱学忠)答案:从习题到工程能力的转化路径

很多人搜「数据库原理及技术(钱学忠)答案」,真正想要的并不是一份可以照抄的题解,而是想搞明白:范式分解到底怎么一步步推、SQL 写出来为什么跑不对、事务并发那几种异常怎么用手推出来。我带过几届做数据库课设的同学,发现一个反直觉的现象——把答案背得最熟的人,往往在真正建表、写查询、调索引时翻车最惨。原因很简单,习题考的是「给定条件下的推导」,工程考的是「条件本身要你自己定」。这篇笔记就按这个思路走:先把教材里那些核心考点还原成可操作的推导步骤,再落到能跑的 SQL 和能验证的脚本上,最后讲清楚哪些习题套路到了真实项目里会失效。适合正在啃这本教材、同时想把这门课变成实际动手能力的人。

2. 把关系代数与范式分解写成可验证的推导流程

教材里关系代数和范式这两块,是搜答案频率最高的部分,也是最能拉开理解差距的地方。很多人卡住不是因为不会算,而是没有一套固定的推导顺序,每次靠感觉,稍微复杂一点就乱。我一般会把它拆成「先定候选键、再判范式、最后做分解」三步,每一步都能用代码或表格验证,而不是纯靠脑补。

2.1 候选键求解:从函数依赖集出发的固定套路

候选键是后面所有判断的地基。给定一个函数依赖集 F 和属性集 U,求候选键的常见做法是画属性分类图:把每个属性按在依赖中出现在左边、右边、两边、都不出现分成四类。只在左边出现的属性一定属于某个候选键,只在右边出现的属性一定不属于任何候选键,两边都出现的需要逐个尝试。

我一般会用一个小的 Python 脚本来枚举验证,避免手推出错:

from itertools import combinations # 属性全集 attrs = ['A', 'B', 'C', 'D', 'E'] # 函数依赖,用元组表示 (左边集合, 右边集合) fds = [ ({'A'}, {'B'}), ({'B'}, {'C'}), ({'C', 'D'}, {'E'}), ({'D'}, {'A'}), ] def closure(init, fds): """求属性闭包:从 init 出发,能推出的所有属性""" result = set(init) changed = True while changed: changed = False for left, right in fds: if left.issubset(result) and not right.issubset(result): result |= right changed = True return result def find_candidate_keys(attrs, fds): keys = [] n = len(attrs) for size in range(1, n + 1): for combo in combinations(attrs, size): combo_set = set(combo) # 跳过已被已知候选键包含的组合 if any(set(k).issubset(combo_set) for k in keys): continue if closure(combo_set, fds) == set(attrs): keys.append(combo) return keys print(find_candidate_keys(attrs, fds))

这段代码的逻辑很直白:closure函数反复扫描所有函数依赖,只要左边被当前属性集包含,就把右边并进来,直到不再变化。find_candidate_keys从属性个数为 1 开始枚举所有组合,一旦某个组合的闭包等于全集,就记为候选键,同时跳过它的超集。参数上唯一需要改的就是attrs和fds,把题目里的依赖抄进去即可。跑出来的结果和手推对照,能立刻发现自己是漏了某个依赖还是闭包算错了。

提示:枚举法在属性数超过 8 个时会明显变慢,但教材习题一般不超过 6 个属性,完全够用。真实项目里求候选键更多靠业务理解,而不是穷举。

2.2 范式判定:1NF 到 BCNF 的逐级检查表

判范式最怕跳步。我的习惯是做成一张检查表,从 1NF 开始逐级往下,任何一级不满足就停在那里,不要直接跳到 BCNF 去分析。下面这张表是我自己用的版本,把每一级的判定条件和常见误判都列清楚了。

范式级别判定条件常见误判点
1NF每个属性都是原子值,不可再分把「联系方式」当成一个字段存多个电话
2NF在 1NF 基础上,非主属性完全依赖于候选键候选键是复合键时,漏判部分依赖
3NF在 2NF 基础上,非主属性不传递依赖于候选键把候选键之间的依赖误当成传递依赖
BCNF每个函数依赖的左边都包含候选键只检查了非主属性,忘了主属性之间的依赖

判定 2NF 时,关键是先确认候选键是不是复合的。如果是单属性候选键,2NF 自动满足,不用再查部分依赖。判定 3NF 时,传递依赖的形式是 X→Y→Z,其中 X 是候选键,Y 不是候选键,Z 是非主属性。很多人会把 Y 是候选键的情况也算进去,那就错了。

2.3 无损分解与保持依赖:两个独立验证的脚本

分解是否无损、是否保持依赖,是习题里最容易混的两个概念。它们要分开验证,不能用一个条件代替另一个。无损分解用「交集能推出其中一个差集」来判断,保持依赖则要把每个函数依赖投影到分解后的各个子模式上,看它们的并集能否推出原依赖集。

def is_lossless(decomp, fds): """判断分解是否无损:对任意两个子模式,交集闭包能推出其中一个""" for i in range(len(decomp)): for j in range(i + 1, len(decomp)): r1, r2 = set(decomp[i]), set(decomp[j]) inter = r1 & r2 if closure(inter, fds) >= r1 or closure(inter, fds) >= r2: return True return False def project_fds(sub, fds): """把函数依赖集投影到子模式上""" result = set() for left, right in fds: if left.issubset(sub) and right.issubset(sub): result.add((frozenset(left), frozenset(right))) return result def is_dependency_preserving(decomp, fds): """判断是否保持依赖:投影后的并集闭包要能推出原依赖""" projected = set() for sub in decomp: projected |= project_fds(set(sub), fds) for left, right in fds: if not right.issubset(closure(left, projected)): return False return True

is_lossless遍历所有子模式对,只要有一对的交集闭包能覆盖其中一个子模式,就判定无损。is_dependency_preserving先把每个依赖投影到包含它全部属性的子模式上,再检查原依赖的右边是否都能从投影集的闭包里推出。这两个函数可以独立调用,分别回答两个问题。参数上注意decomp是子模式列表,每个子模式用集合或列表表示属性。

注意:无损分解和保持依赖没有必然联系。存在无损但不保持依赖的分解,也存在保持依赖但有损的分解。习题里经常把这两个条件放在一起考,答题时要分别写清楚。

3. SQL 习题到可执行查询:把答案变成能跑的表和数据

教材上的 SQL 题,很多只给一个查询要求,不提供建表语句和测试数据。照着答案抄一遍,运行报错都不知道错在哪。我的做法是:每道 SQL 题都自己补一套最小的建表和插数脚本,让查询真正跑起来,再对照结果验证逻辑。

3.1 建表与插数:最小可复现数据集

以教材里常见的「学生-课程-选课」模型为例,我一般会建三张表,字段尽量精简,只保留题目用得到的列。

-- 学生表 CREATE TABLE student ( sno VARCHAR(10) PRIMARY KEY, sname VARCHAR(20) NOT NULL, sage INT, sdept VARCHAR(20) ); -- 课程表 CREATE TABLE course ( cno VARCHAR(10) PRIMARY KEY, cname VARCHAR(30) NOT NULL, credit INT ); -- 选课表,复合主键 CREATE TABLE sc ( sno VARCHAR(10), cno VARCHAR(10), grade INT, PRIMARY KEY (sno, cno), FOREIGN KEY (sno) REFERENCES student(sno), FOREIGN KEY (cno) REFERENCES course(cno) ); -- 插入测试数据 INSERT INTO student VALUES ('S01', '张三', 20, '计算机'), ('S02', '李四', 21, '计算机'), ('S03', '王五', 22, '数学'); INSERT INTO course VALUES ('C01', '数据库', 4), ('C02', '操作系统', 3), ('C03', '离散数学', 3); INSERT INTO sc VALUES ('S01', 'C01', 88), ('S01', 'C02', 76), ('S02', 'C01', 92), ('S03', 'C03', 85);

建表时把外键约束加上,是为了在插数阶段就能发现引用错误,而不是等到查询结果不对再回头查。sc表用(sno, cno)做复合主键,对应教材里最常见的选课关系。测试数据只插了 4 条选课记录,足够验证大多数单表和多表查询。

3.2 嵌套查询与连接查询的等价改写

教材里有一类题要求「用嵌套查询实现」,另一类要求「用连接查询实现」。很多人只会写一种,遇到指定写法就卡住。其实两者在逻辑上可以互相改写,关键是搞清楚相关子查询和连接条件的对应关系。

-- 嵌套查询:查选了 C01 的学生姓名 SELECT sname FROM student WHERE sno IN ( SELECT sno FROM sc WHERE cno = 'C01' ); -- 连接查询等价写法 SELECT DISTINCT s.sname FROM student s JOIN sc ON s.sno = sc.sno WHERE sc.cno = 'C01'; -- 相关子查询:查每个学生选课数 SELECT sname, (SELECT COUNT(*) FROM sc WHERE sc.sno = student.sno) AS cnt FROM student; -- 连接加分组等价写法 SELECT s.sname, COUNT(sc.cno) AS cnt FROM student s LEFT JOIN sc ON s.sno = sc.sno GROUP BY s.sno, s.sname;

第一组改写里,IN子查询和JOIN加WHERE的结果集是一样的,但连接写法要加DISTINCT,因为一个学生可能选多门课导致重复。第二组改写里,相关子查询用LEFT JOIN加GROUP BY替代,注意LEFT JOIN保证没选课的学生也出现,计数为 0。参数上唯一要调的是连接条件和分组字段,分组时要把sname也放进GROUP BY,否则在严格模式下会报错。

提示:真实项目里优先用连接查询,优化器对它的处理更成熟。嵌套查询在子查询结果集很大时性能会明显下降,教材里要求写嵌套只是为了考语法。

3.3 聚合与分组:HAVING 和 WHERE 的边界

WHERE和HAVING的区别是高频考点,也是实际写查询时最容易放错位置的地方。WHERE在分组前过滤行,HAVING在分组后过滤组。判断标准很简单:条件里有没有聚合函数,有就用HAVING,没有就尽量用WHERE。

-- 查平均分大于 80 的课程 SELECT cno, AVG(grade) AS avg_grade FROM sc GROUP BY cno HAVING AVG(grade) > 80; -- 查选了 2 门以上课程的学生 SELECT sno, COUNT(*) AS cnt FROM sc GROUP BY sno HAVING COUNT(*) >= 2; -- WHERE 先过滤再分组:只统计 C01 和 C02 的成绩 SELECT cno, AVG(grade) AS avg_grade FROM sc WHERE cno IN ('C01', 'C02') GROUP BY cno;

第一个查询里AVG(grade) > 80必须放在HAVING,因为聚合结果在分组后才产生。第三个查询里cno IN (...)放在WHERE,因为它在分组前就能确定哪些行参与统计。把过滤条件尽量前移到WHERE,能减少参与分组的行数,这是实际调优时的一个基本习惯。

4. 事务与并发控制:把习题里的异常现象手推一遍

事务隔离级别和并发异常是教材里比较抽象的一章,光看定义很容易混。我的经验是:把每种异常用一个具体的时间线手推一遍,比背定义管用得多。下面用两个事务 T1 和 T2 的操作序列来说明。

4.1 脏读、不可重复读、幻读的时间线推演

时间T1 操作T2 操作现象
t1读 A=100
t2A=A-50,写 A=50
t3读 A=50脏读:读到未提交数据
t4回滚,A 恢复 100
t5再读 A=100前后不一致

不可重复读的时间线是:T1 读 A=100,T2 修改 A 并提交,T1 再读 A 发现变成 50。幻读则是 T1 按条件查出一组行,T2 插入新行并提交,T1 再查发现多了一行。三者的区别在于:脏读读到未提交数据,不可重复读读到已提交的修改,幻读读到已提交的新增或删除。

4.2 隔离级别选择:从习题条件到实际配置

教材里会给出「某隔离级别下是否允许某异常」的表格,实际配置时对应的是数据库的隔离级别参数。以常见的关系型数据库为例,隔离级别从低到高是读未提交、读已提交、可重复读、串行化。读未提交允许脏读,读已提交禁止脏读但允许不可重复读,可重复读禁止不可重复读但可能允许幻读,串行化全部禁止。

-- 查看当前隔离级别(以 MySQL 语法为例) SELECT @@transaction_isolation; -- 设置会话隔离级别为读已提交 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 开启事务并验证 START TRANSACTION; SELECT * FROM sc WHERE sno = 'S01'; -- 在另一个会话中修改并提交,再执行同样的查询 SELECT * FROM sc WHERE sno = 'S01'; COMMIT;

设置隔离级别时要注意作用范围:SESSION只影响当前连接,GLOBAL影响后续新连接。实际项目里很少用串行化,因为并发性能下降明显,多数场景用读已提交或可重复读就够。参数选择的核心是权衡一致性和并发度,习题里只问「允许不允许」,实际要问「代价能不能接受」。

注意:不同数据库对隔离级别的默认值和实现细节有差异,教材上的理论表格是标准定义,实际行为要以所用数据库的文档为准。

5. 避坑与排查:习题答案照搬到工程里的五个翻车点

这一章是我自己踩过和见别人踩过的坑,每条都按「现象、原因、解决」写清楚。习题和工程的差距,基本都藏在这些地方。

5.1 范式分解过度导致查询变慢

现象:按 BCNF 把一张表拆成五六个小表,查询时要连五六次,性能明显下降。原因:习题只考分解的正确性,不考连接代价,实际查询的连接开销随表数量上升。解决:实际设计时通常做到 3NF 就够,只有在更新异常确实严重时才继续拆。拆之前先想清楚主要查询路径,避免为了范式而范式。

5.2 把教材的 SQL 方言当成通用语法

现象:习题里写的 SQL 在目标数据库上直接报错。原因:教材常用标准 SQL 或某种方言,不同数据库对函数名、分页语法、类型转换的支持不一样。解决:写查询前先确认目标数据库,把LIMIT、TOP、ROWNUM这类方言差异点单独记下来,测试时用最小查询先验证语法。

5.3 忽略 NULL 导致聚合结果偏差

现象:AVG(grade)算出来的平均值和手算对不上。原因:聚合函数默认忽略 NULL,如果grade列有 NULL,参与计算的行数比总行数少。解决:明确业务上 NULL 代表什么,需要计入时用COALESCE(grade, 0)转换,不需要计入时在文档里写清楚。COUNT(*)和COUNT(列名)的区别也要注意,前者数行,后者只数非 NULL。

5.4 事务边界过大引发锁等待

现象:一个事务里做了大量查询和更新,其他会话一直等待。原因:事务开启后持有的锁在提交前不释放,事务越长,锁持有时间越久。解决:把事务范围缩到最小,只包住必须原子执行的操作。查询和计算尽量放在事务外,更新语句集中执行后尽快提交。

5.5 索引建了但查询没走

现象:明明在cno上建了索引,查询还是全表扫描。原因:查询条件对列做了函数运算或隐式类型转换,导致索引失效。解决:用EXPLAIN查看执行计划,确认是否走索引。把WHERE cno + 1 = 'C02'改成WHERE cno = 'C01',把字符串列和数字比较时统一类型。索引不是建了就生效,要看查询写法是否匹配。

6. 用执行计划验证你的答案:一个可复用的排查习惯

前面几章把习题里的推导、SQL、事务都过了一遍,最后落到一个我一直在用的习惯上:任何查询写完之后,不要只看结果对不对,还要看执行计划。结果对只是逻辑正确,执行计划才能告诉你这条查询在数据量变大之后会不会崩。教材习题的数据量很小,怎么写都快,但真实表动辄百万行,逻辑等价的两种写法性能可能差几十倍。

以EXPLAIN为例,我一般关注四个字段:访问类型、可能用到的索引、实际用到的索引、扫描行数。访问类型从差到好大致是全表扫描、索引扫描、索引查找、唯一查找。如果一条查询在有小索引的列上还是全表扫描,基本可以确定是写法问题。扫描行数则直接反映查询要处理的数据规模,行数远超结果集行数时,说明过滤条件没有有效下推。

-- 查看查询的执行计划 EXPLAIN SELECT s.sname, COUNT(sc.cno) AS cnt FROM student s LEFT JOIN sc ON s.sno = sc.sno GROUP BY s.sno, s.sname; -- 对比:把过滤条件前移,观察扫描行数变化 EXPLAIN SELECT s.sname, COUNT(sc.cno) AS cnt FROM student s LEFT JOIN sc ON s.sno = sc.sno AND sc.grade >= 60 GROUP BY s.sno, s.sname;

第一条查询会对sc全表扫描后再分组,第二条把grade >= 60放到连接条件里,扫描行数会减少。参数上要注意,连接条件里的过滤和WHERE里的过滤在LEFT JOIN下语义不同:放在ON里不影响左表保留,放在WHERE里会把左表中不满足条件的行也过滤掉。这个区别在习题里很少考,但实际写报表查询时经常翻车。

我自己的习惯是:每写完一条稍微复杂的查询,先跑EXPLAIN,确认访问类型和扫描行数在可接受范围,再去看结果。这个习惯帮我省过很多次后悔药——有几次上线前发现某条查询在测试数据上毫秒级,在真实数据量下要几十秒,都是靠执行计划提前发现的。教材答案给你的是逻辑正确的下限,执行计划帮你守住性能的上限。把这两件事都做了,数据库这门课才算真正从习题变成了能力。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 6:55:09

数据库原理及技术(钱学忠)答案:从习题到可验证SQL实战

简介:这份资源是钱学忠《数据库原理及技术》教材的配套习题答案,面向高校计算机相关专业学生、备考数据库课程考试的学习者,以及希望系统巩固数据库基础的在职开发者。内容围绕数据库设计、SQL语言、关系数据库理论、DBMS与数据库管理等核心章…

作者头像 李华
网站建设 2026/10/14 6:54:18

开源小模型+端侧部署:老旧笔记本跑本地知识库问答全指南

国庆假期这七天,身边不少朋友都在景点人挤人,我窝在家里,盯着手机上的AI资讯刷了一整天又一天。倒也不是我有加班癖,实在是这周的AI圈动静太大:昨天还在盘点各家新模型,今天开源社区就蹦出个跑分惊人的小参…

作者头像 李华
网站建设 2026/10/14 6:54:11

垃圾回收任务分配难题:MATLAB最近邻启发式算法实现与路径优化

1. 从实际问题说起:垃圾回收任务分配难在哪先说一个我常遇到的问题。做城市环卫信息化项目的朋友应该都有体会:一个中等的居民区片区,分散着几十个垃圾投放点,车队就那么几辆车,每天都要跑一趟回收垃圾。路线排得好不好…

作者头像 李华
网站建设 2026/10/14 6:54:00

AI编码代理上下文压缩自救:外部记忆+init.d式恢复机制实战

我最近完成了一个累计消耗 20 万 token 的重构任务,主角不是人类团队的代码评审,而是一名 AI 编码代理。任务中途,它经历了一次上下文压缩——平台为了节省窗口,把早期会话中大量的代码发现过程、关键决策和变量命名约定全部浓缩成…

作者头像 李华
网站建设 2026/10/14 6:53:46

API网关和反向代理区别在哪?MAI Gateway统一治理方案深度解析

“API 网关和反向代理区别在哪”——表面看是产品对比,深层看是两个时代的入口范式差异。本文走"统一治理方案深度解析"视角,从命名、演进、架构三层切入,再讲 MAI Gateway 如何用三层治理栈把两类入口收敛为同一层。 第一层&#…

作者头像 李华
网站建设 2026/10/14 6:53:41

CSS伪类与伪元素完全指南:状态与虚拟元素的区别、优先级和实战

写CSS伪类和伪元素这块内容,我其实挺有感触的。我记得自己刚接触前端的时候,对着:hover和::before这两个东西完全分不清,总觉得它们长得差不多,用起来好像都能往页面上加点东西,直到后来被一个::before配合content属性…

作者头像 李华