简介:这份资源是钱学忠《数据库原理及技术》教材的配套习题答案,面向高校计算机相关专业学生、备考数据库课程考试的学习者,以及希望系统巩固数据库基础的在职开发者。内容围绕数据库设计、SQL语言、关系数据库理论、DBMS与数据库管理等核心章节展开,可帮助读者在完成课后练习后对照答案查漏补缺,理清E-R模型转换、关系代数演算、范式判定与规范化等易错知识点。资源以rar压缩包形式提供,整体约16.69MB,上游未提供具体文件数量与类型明细,下载后可直接解压查阅。目前已有693人学习下载,适合作为课程复习、习题核对与考前自测的辅助材料,配合教材使用能更高效地掌握数据库设计与管理的基本方法。
1. 数据库原理及技术(钱学忠)答案:从习题答案到能跑通的 SQL 实战
很多人搜「数据库原理及技术(钱学忠)答案」,其实不是想抄答案,而是被习题里的范式分解、事务调度、SQL 嵌套查询卡住了,想找个能对照验证的参考。我当年带新人做数据库课程设计时也踩过这个坑:答案背得滚瓜烂熟,一到真实建库就翻车,连一个多表连接都写不利索。这篇笔记不给你一份静态答案,而是把「答案」拆成可复现的验证路径——用本地数据库把每类典型题跑一遍,看到结果再回头理解原理。适合正在学数据库原理、准备考试或做课程设计的人,也适合想补 SQL 基本功的开发者。核心思路是:答案不是终点,能自己验证答案才是。
2. 先搭一个能验证答案的本地环境:选型与最小建库
2.1 为什么用 SQLite 起步,而不是直接上 MySQL
搜答案的人多半在 Windows 或 Mac 上,装 MySQL 要配服务、设密码、改字符集,光环境就能劝退一半人。SQLite 是单文件数据库,零配置,Python 标准库自带驱动,打开就能跑。对于验证「数据库原理及技术」里的 SQL 题、范式题、事务题,SQLite 覆盖了 90% 的场景。唯一要注意的是它默认不强制外键、事务隔离级别和 MySQL 有差异,涉及并发调度的题我会在后面单独说明用 MySQL 验证。
常见做法是:先用 SQLite 把 SQL 语法和查询逻辑跑通,再用 MySQL 验证事务和锁。我一般会建一个study.db文件,所有习题表都放里面,方便反复删改。
import sqlite3 # 连接数据库,文件不存在会自动创建 conn = sqlite3.connect('study.db') cursor = conn.cursor() # 建一张学生表,对应教材里最常见的 Student 关系 cursor.execute(''' CREATE TABLE IF NOT EXISTS student ( sno TEXT PRIMARY KEY, -- 学号,主键 sname TEXT NOT NULL, -- 姓名,非空 sage INTEGER, -- 年龄 sdept TEXT -- 系别 ) ''') # 建选课表,外键关联学生 cursor.execute(''' CREATE TABLE IF NOT EXISTS sc ( sno TEXT, cno TEXT, grade REAL, PRIMARY KEY (sno, cno), FOREIGN KEY (sno) REFERENCES student(sno) ) ''') conn.commit() print("建表完成")这段代码做了三件事:连接数据库、建学生表、建选课表。PRIMARY KEY定义主键,NOT NULL约束非空,FOREIGN KEY声明外键。注意 SQLite 默认外键约束是关闭的,如果要验证参照完整性,需要执行PRAGMA foreign_keys = ON;。参数上,sno用 TEXT 而不是 INTEGER,是因为学号常有前导零,用整数会丢零。
2.2 插入测试数据:让答案有据可查
没有数据的答案都是空谈。我习惯先插一批边界数据:有成绩为 NULL 的、有没选课的学生、有选了课但没成绩的。这样验证连接查询和空值处理时才能看出差别。
# 插入学生数据,故意留一个没选课的学生 students = [ ('S001', '张三', 20, '计算机'), ('S002', '李四', 21, '计算机'), ('S003', '王五', 22, '数学'), ('S004', '赵六', 20, '数学'), ] cursor.executemany('INSERT OR REPLACE INTO student VALUES (?,?,?,?)', students) # 插入选课数据,故意留一个 NULL 成绩 sc_data = [ ('S001', 'C01', 85), ('S001', 'C02', 90), ('S002', 'C01', 78), ('S003', 'C01', None), # 选了但没成绩 ] cursor.executemany('INSERT OR REPLACE INTO sc VALUES (?,?,?)', sc_data) conn.commit()INSERT OR REPLACE保证重复执行不会报主键冲突,适合反复调试。executemany批量插入比循环单条快得多。这里故意让 S003 的成绩为 NULL,后面验证AVG、COUNT时就能看到 NULL 的影响——这是教材习题里最爱考的坑。
2.3 验证环境是否就绪的三条命令
建完表和插完数据,别急着写复杂查询,先用三条命令确认环境正常:
# 1. 查所有学生 cursor.execute('SELECT * FROM student') print(cursor.fetchall()) # 2. 查选课数 cursor.execute('SELECT COUNT(*) FROM sc') print(cursor.fetchone()) # 3. 查外键是否生效 cursor.execute('PRAGMA foreign_keys') print(cursor.fetchone())如果第一条输出四条学生记录,第二条输出 4,第三条输出(0,)说明外键没开。外键没开不影响大部分查询题,但涉及删除级联的题会出错。这三步花不了一分钟,却能省掉后面「为什么删除报错」的排查时间。
3. 把教材典型题拆成可执行 SQL:查询、范式与事务
3.1 嵌套查询与连接查询:同一道题两种写法对比
教材里最常见的题是「查询选修了 C01 课程的学生姓名」。标准答案一般给嵌套查询,但实际工作中连接查询更常用。两种都跑一遍,才能理解为什么考试考嵌套、生产用连接。
# 写法一:嵌套查询,对应教材标准答案 cursor.execute(''' SELECT sname FROM student WHERE sno IN ( SELECT sno FROM sc WHERE cno = 'C01' ) ''') print("嵌套查询结果:", cursor.fetchall()) # 写法二:连接查询,生产环境更常见 cursor.execute(''' SELECT DISTINCT s.sname FROM student s JOIN sc ON s.sno = sc.sno WHERE sc.cno = 'C01' ''') print("连接查询结果:", cursor.fetchall())嵌套查询先执行子查询拿到选 C01 的学号集合,再在外层过滤学生。连接查询直接把两张表按学号拼起来再过滤。结果一样,但执行计划不同:数据量大时,IN子查询可能被优化成半连接,而JOIN更直观。DISTINCT不能省,因为一个学生可能选多门课导致重复行。教材答案常漏DISTINCT,这是扣分点也是实际 bug 来源。
3.2 范式分解:从函数依赖到 3NF 的验证方法
范式题光看答案记不住,得动手验证。假设有关系R(学号, 姓名, 系别, 系主任),函数依赖是学号→姓名, 学号→系别, 系别→系主任。判断是否满足 3NF,要看有没有传递依赖。
# 用代码模拟函数依赖检查 dependencies = { '学号': ['姓名', '系别'], '系别': ['系主任'] } def find_transitive(deps): """找出传递依赖:A->B, B->C 则 A->C 是传递依赖""" result = [] for a, bs in deps.items(): for b in bs: if b in deps: for c in deps[b]: result.append(f"{a} -> {b} -> {c}") return result print("传递依赖:", find_transitive(dependencies)) # 输出:['学号 -> 系别 -> 系主任']这段代码把函数依赖写成字典,遍历找出「A 决定 B,B 又决定 C」的链。输出显示存在传递依赖,所以R不满足 3NF,需要拆成R1(学号, 姓名, 系别)和R2(系别, 系主任)。参数上,字典的键是决定因素,值是依赖它的属性。这个方法能验证大部分范式题,比死记定义靠谱。
3.3 事务与隔离级别:用 MySQL 看脏读和不可重复读
SQLite 的隔离级别和教材讲的 MySQL 有差异,验证事务题建议切到 MySQL。下面用两个会话模拟脏读:会话 A 改数据不提交,会话 B 读到了未提交的数据。
-- 会话 A:开启事务,修改成绩但不提交 START TRANSACTION; UPDATE sc SET grade = 100 WHERE sno = 'S001' AND cno = 'C01'; -- 此时不执行 COMMIT -- 会话 B:在另一个连接里查询 SELECT grade FROM sc WHERE sno = 'S001' AND cno = 'C01'; -- 如果读到 100,说明发生了脏读MySQL 默认隔离级别是REPEATABLE READ,理论上不会脏读。要复现脏读需要把会话 B 的隔离级别降到READ UNCOMMITTED:SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;。参数上,隔离级别从低到高是READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE。教材常考「哪个级别避免哪种读」,实际验证一遍比背表格强。
4. 避坑与排查:验证答案时最容易翻车的五个点
4.1 现象:查询结果比预期多出重复行
原因:多表连接时没有加DISTINCT,或者连接条件写漏了。比如学生表和选课表连接,一个学生选三门课就出现三行。解决:先确认业务要不要去重,要就去重,不要就检查连接条件是否完整。我一般会先用COUNT(*)和COUNT(DISTINCT sno)对比,差得多就是重复了。
4.2 现象:AVG 算出来的平均分和手算不一致
原因:AVG会忽略 NULL 值,但分母是「非 NULL 的行数」,不是总行数。比如 S003 成绩为 NULL,AVG(grade)只算有成绩的行。解决:如果 NULL 应该算 0,用AVG(COALESCE(grade, 0));如果 NULL 应该排除,确认分母是否符合预期。教材答案常不说明这点,实际考试和工作中都是坑。
4.3 现象:外键约束导致删除失败
原因:SQLite 默认不开外键,MySQL 开了但没设级联。删除主表记录时,从表还有引用就报错。解决:要么先删从表,要么建表时加ON DELETE CASCADE。验证时先执行PRAGMA foreign_keys确认状态,别在没开外键的环境里测级联删除。
4.4 现象:事务回滚后数据还在
原因:用了CREATE TABLE或ALTER TABLE这类 DDL 语句,在 MySQL 里 DDL 会隐式提交,回滚无效。解决:把 DDL 和 DML 分开执行,事务里只放INSERT、UPDATE、DELETE。验证回滚时用SELECT确认数据真的回去了。
4.5 现象:范式判断结果和答案对不上
原因:函数依赖集没找全,或者对「部分依赖」「传递依赖」的定义理解有偏差。解决:先把所有函数依赖列出来,画成有向图,再逐条检查。我习惯用前面那段 Python 代码跑一遍,机器不会漏掉传递链。如果还不对,检查是不是把「候选键」找错了。
5. 进阶技巧:用 EXPLAIN 看懂答案背后的执行计划
5.1 为什么同一个答案性能差十倍
教材答案只关心结果对不对,不关心快不快。但实际工作中,嵌套查询和连接查询在数据量大时性能可能差一个数量级。用EXPLAIN能看到数据库怎么执行你的 SQL,这是从「会写答案」到「写对答案」的关键一步。
-- MySQL 中查看执行计划 EXPLAIN SELECT sname FROM student WHERE sno IN (SELECT sno FROM sc WHERE cno = 'C01'); EXPLAIN SELECT DISTINCT s.sname FROM student s JOIN sc ON s.sno = sc.sno WHERE sc.cno = 'C01';输出里重点看type列:ALL是全表扫描,ref是用索引,eq_ref是主键或唯一索引连接。如果嵌套查询的子查询出现DEPENDENT SUBQUERY,说明子查询对外层每一行都执行一次,数据量大时会很慢。连接查询如果type是ALL,说明没走索引,需要给sc.cno加索引。
5.2 给习题表加索引:一条命令改变执行计划
-- 给选课表的课程号加索引 CREATE INDEX idx_sc_cno ON sc(cno); -- 再次查看执行计划 EXPLAIN SELECT DISTINCT s.sname FROM student s JOIN sc ON s.sno = sc.sno WHERE sc.cno = 'C01';加索引后再看EXPLAIN,sc表的type应该从ALL变成ref,rows列扫描行数明显下降。参数上,索引不是越多越好,每个索引都会拖慢写入。习题表数据少看不出差别,但理解这个机制后,做课程设计时就知道该给哪些列加索引。
5.3 用查询结果反推答案是否合理
最后一个技巧:拿到任何答案,先想「这个结果合理吗」。比如查平均分,如果结果大于 100 或小于 0,肯定错了。查选课人数,如果超过学生总数,说明连接条件写错了。我习惯在验证答案时加一层「合理性检查」:
# 合理性检查:平均分应在 0-100 之间 cursor.execute('SELECT AVG(grade) FROM sc WHERE grade IS NOT NULL') avg = cursor.fetchone()[0] assert 0 <= avg <= 100, f"平均分异常:{avg}" print(f"平均分 {avg:.2f},合理")这段代码用assert做断言,平均分超出范围直接报错。参数上,WHERE grade IS NOT NULL排除空值干扰。这个习惯帮我抓过好几次答案抄错的情况——不是逻辑错,是数字抄错了。
我自己的教训是:早年复习数据库原理时,把答案背得一字不差,结果上机考试遇到一道「查询平均分最高的学生」,我写了嵌套查询但忘了处理并列第一,直接翻车。后来我养成了一个习惯:每道题跑三遍——第一遍照答案写,第二遍换种写法,第三遍用EXPLAIN看执行计划。三遍下来,答案才真正变成自己的。希望帮到你。
本文还有配套的精品资源,点击获取