简介:本资源是《数据库原理及技术》(钱学忠编著)配套习题参考答案,面向高校计算机专业学生、数据库初学者及备考人员,旨在辅助理解关系数据库核心概念、夯实SQL实践能力、掌握规范化设计与DBMS管理要点。压缩包为RAR格式,共含多个结构清晰的文档文件(具体总数未提供),主体为文字类答案解析,涵盖数据库设计全流程、SQL语法详解、关系代数与范式应用、DBMS功能对比及备份恢复等实操要点,总大小16.69MB,便于下载研读与离线查阅。已有693人学习下载,答案内容紧扣教材章节逻辑,不仅给出标准解法,更包含关键步骤说明、易错点提示与典型查询语句示例,有助于读者对照理论查漏补缺、提升解题规范性与系统设计思维。
1. 数据库原理及技术(钱学忠)答案:一份被低估的「反向教学脚手架」——它不教你怎么写SQL,而是逼你重走B+树分裂、WAL日志刷盘、两阶段锁升级的每一步
很多人下载《数据库原理及技术(钱学忠)答案》后第一反应是“这题解怎么没一行代码?”“为什么连CREATE TABLE都懒得贴?”——恰恰是这种“不配合”的姿态,让它成了极少数能刺穿数据库黑匣子的实体媒介。它不提供速查手册式的结论,而是一套精密设计的反向推演路径图:每道习题都锚定在某个底层机制的临界点上——比如第4章第7题要求“手动模拟B+树插入23、45、12、89后的结构变化”,表面是画图,实则是强制你复现页分裂时key的上移逻辑、兄弟节点指针重连时机、根节点是否新增层级;第6章第3题“写出事务T1与T2在可串行化调度下所有合法交错序列”,本质是在训练你肉眼识别锁等待图中的环、预判死锁检测器触发阈值。这不是答案集,而是一份用纸笔构建的数据库内核沙盒。当你被迫在草稿纸上画出第5次checkpoint前后的buffer pool脏页链表状态,你就已经比90%只调EXPLAIN ANALYZE的人更懂MVCC的版本链裁剪策略。这份资源的价值,从来不在“抄”,而在“卡住”——卡在某个分裂失败、某个锁升级死循环、某个WAL record checksum校验不通过的瞬间,然后倒推:是页填充因子设错了?还是log sequence number没对齐?这才是真正逼近数据库心跳的路径。
2. B+树索引:为什么“插入一个数”要重画整棵树?答案集里的手绘图暴露了存储引擎最真实的物理约束
2.1 分裂不是算法选择,而是磁盘页物理尺寸的刚性表达
钱学忠教材中B+树习题(如第4章例4-3)要求学生在固定页大小(如4KB)和键值长度(如8字节整型+6字节指针)约束下,手工计算单页最多容纳多少键。这个看似枯燥的算术题,直指B+树存在的根本物理前提:数据库从不管理“逻辑节点”,只管理“物理页”。当答案集给出某次插入后页分裂的完整示意图时,它其实在显式声明三个硬约束:
- 单页必须留出至少10%空间用于后续更新(否则频繁分裂);
- 分裂时新页必须获得原页约50%的键(保证树平衡,但教材刻意不提“50%”而是让学员自己数);
- 父节点插入新分隔键时,若父页已满,则触发递归分裂——这就是为什么答案图中常出现“根节点新增一层”的突变。
提示:观察答案集中所有B+树图,你会发现分隔键(separator key)永远是子节点中最小的上界值,而非中位数。例如子节点键为[12,23,45]和[56,67,89],父节点分隔键是56而非45——这是为了保证范围查询时能准确跳转到右子树,本质是B+树对“左闭右开”区间语义的物理实现。
2.2 手动模拟分裂过程,暴露缓冲区管理的真实代价
教材第4章习题要求“模拟插入序列[10,20,30,40,50,60,70,80,90]后的B+树状态”。答案集未直接给最终形态,而是分步展示每次插入后的页状态。关键在于第7步插入70时:
- 原页[50,60]已满(假设扇出度为3);
- 分裂为[50]和[60],新键70需上移到父节点;
- 若父节点此时为[30,40],插入70后变为[30,40,70],仍不满;
- 但第8步插入80时,父节点[30,40,70]已满,必须分裂——此时答案图会突然出现新根节点。
这个过程揭示了一个常被忽略的事实:B+树高度增长不是由叶节点决定,而是由中间层页的填充率决定。生产环境中DBA调优innodb_page_size或fill_factor,本质上是在干预这个手工模拟中反复出现的“临界点”。
2.3 避坑:B+树手绘中5个高频翻车点及原理级修复
现象1:分裂后兄弟页指针断裂,导致范围扫描跳过数据
原因:答案集中某题解析显示新页[60,70]的prev指针指向旧页[50],但旧页[50]的next指针仍指向原兄弟页,未更新。这违反B+树叶节点双向链表一致性。
解决:在分裂操作最后一步,必须原子更新两个页的指针:new_page.prev = old_page; old_page.next = new_page。InnoDB源码中btr_page_split_and_insert()函数内btr_set_next_offs()和btr_set_prev_offs()即处理此逻辑。
现象2:分隔键重复出现在父子节点,引发唯一索引冲突
原因:误将叶节点最大键(如60)同时作为父节点分隔键和叶节点自身键。B+树规范要求分隔键仅存在于非叶节点,叶节点只存实际数据键。
解决:严格遵循“分隔键是子树上界”的定义。若子树包含键≤60,则父节点分隔键为60,但叶节点中60必须属于左子树,右子树最小键应>60(如61)。
现象3:插入后未调整父节点键数量,导致后续查找路径错误
原因:分裂后父节点新增分隔键,但未同步更新页头中的n_owned字段(记录本页拥有的键数量)。InnoDB中该字段用于快速定位槽位。
解决:所有页修改必须更新页头元数据。答案集某题解析中父页从2键变为3键,但页头PAGE_N_DIR_SLOTS未变,此为典型疏漏。
现象4:根节点分裂时忘记重置PAGE_LEVEL,造成树高误判
原因:新根节点创建后,其PAGE_LEVEL应设为原根level+1,但手绘常遗漏。这会导致btr_cur_search_to_nth_level()等函数按错误高度遍历。
解决:InnoDB中btr_root_raise_and_insert()函数明确调用page_header_set_field(new_page, PAGE_LEVEL, level + 1)。
现象5:未考虑键值比较的字节序问题,导致排序错乱
原因:对字符串键(如'abc'、'abd')直接按ASCII码比较,但实际存储可能用UTF8MB4且含collation规则。答案集中某题用中文键模拟,却按字面顺序排列。
解决:数据库键比较永远依赖dict_col_t::cmp_proc函数族,其行为由列定义的COLLATION决定。手绘时应标注比较规则,如utf8mb4_0900_as_cs。
3. 事务与并发控制:答案集里那些“列出所有调度序列”的题目,其实在训练你肉眼编译锁等待图
3.1 可串行化不是目标,而是锁协议的自然涌现结果
钱学忠教材第6章大量习题要求“写出T1与T2所有可串行化调度”,表面是排列组合,实则在强制建立锁-操作-依赖的映射关系。以经典题为例:
T1: R(A), W(A), R(B), W(B), Commit T2: R(B), W(B), R(A), W(A), Commit答案集不直接给序列,而是引导你:
- 先标出所有读写冲突点(R(A)与W(A)、R(B)与W(B)等);
- 再根据两阶段锁协议(2PL),确定每个操作前必须加锁、提交后才释放;
- 最后发现:若T1先W(A),则T2的R(A)必须等待,形成T1→T2依赖;反之T2先W(A)则T1→T2。
这个过程揭示核心事实:可串行化调度集合的大小,由锁粒度和事务操作顺序共同决定,而非隔离级别声明。MySQL的REPEATABLE READ能避免幻读,是因为Next-Key Lock将间隙也纳入锁定范围——这正是答案集中某题要求“画出T1执行SELECT * FROM t WHERE id>10 FOR UPDATE后的锁覆盖范围”的深意。
3.2 手动构建锁等待图,理解死锁检测器的输入源
教材第6章习题常给出两个事务的SQL序列,要求“判断是否可能死锁”。答案集解析不直接答“是/否”,而是要求:
- 列出每个事务申请的锁类型(S/X锁)和对象(行锁/间隙锁);
- 标注锁的持有者与等待者;
- 绘制有向图:边T1→T2表示T1持有T2需要的锁。
当图中出现环(如T1→T2→T1),即判定死锁。这正是InnoDB死锁检测器lock_deadlock_check()函数的输入模型。答案集某题解析中,T1持有A行X锁等待B行X锁,T2持有B行X锁等待A行X锁,图中环清晰可见——而生产环境SHOW ENGINE INNODB STATUS输出的死锁日志,不过是这个手绘图的机器版。
3.3 避坑:并发调度分析中4个血泪经验
现象1:忽略隐式锁转换,误判锁兼容性
原因:认为SELECT ... FOR UPDATE只加X锁,但InnoDB中若该行无记录,会加Insert Intention Lock(一种特殊间隙锁),与Gap Lock兼容。答案集中某题因未识别此锁,错误判定T1与T2无冲突。
解决:牢记InnoDB锁类型矩阵。Insert Intention Lock与Gap Lock兼容,但与Record Lock互斥。手绘时需标注锁类型全称。
现象2:将“不可重复读”与“幻读”混为一谈,导致隔离级别误配
原因:认为RR级别下SELECT * FROM t WHERE id=10两次执行结果不同就是幻读。实则这是不可重复读(同一行值变化),幻读特指SELECT * FROM t WHERE id>10返回新行。答案集某题解析混淆二者,误导读者。
解决:严格按SQL标准定义:不可重复读=同一行值变更;幻读=新行插入。InnoDB通过Next-Key Lock同时解决二者。
现象3:未考虑锁升级条件,高估死锁概率
原因:认为行锁必然导致死锁。但InnoDB存在锁升级阈值(如单事务超1000行锁时升级为页锁),此时竞争粒度变大。答案集中某长事务题未提示此机制,使学员过度悲观。
解决:查看innodb_lock_wait_timeout和innodb_rollback_on_timeout参数,理解锁升级是性能与安全的权衡。
现象4:忽略自增锁(AUTO_INC Lock)的特殊性,误判INSERT并发
原因:分析INSERT INTO t VALUES()并发时,只考虑行锁,忽略自增主键分配需获取表级AUTO_INC锁。答案集中某题未标注此锁,导致调度序列遗漏关键阻塞点。
解决:InnoDB中innodb_autoinc_lock_mode=1(默认)时,Simple Insert用轻量级锁,Bulk Insert仍用表锁。手绘必须区分INSERT类型。
4. WAL日志与崩溃恢复:答案集里“写出日志记录格式”的题目,是理解ACID中D(Durability)的物理锚点
4.1 日志不是备份,而是重放指令流的确定性脚本
钱学忠教材第5章习题常要求“写出UPDATE t SET a=5 WHERE id=1产生的WAL日志记录”。答案集不给二进制格式,而是描述为:
[LOG_TYPE_MLOG_REC_UPDATE_IN_PLACE, page_no=123, offset=456, old_value=3, new_value=5]这个抽象描述直指WAL本质:它不存储数据页镜像,而存储“如何修改”的确定性指令。InnoDB中MLOG_REC_UPDATE_IN_PLACE日志类型表示原地更新,offset指向页内记录偏移,old_value用于崩溃恢复时校验——若页中当前值不等于old_value,说明该更新已被其他日志覆盖,跳过执行。这解释了为何WAL体积远小于数据页:它只记“delta”,不记“state”。
4.2 手动推演崩溃恢复,看清redo log与undo log的协作边界
教材第5章某题设定:“系统在写入第3条日志后崩溃,buffer pool中页P1、P2为脏页,其中P1已刷盘,P2未刷盘”。答案集要求:
- 列出崩溃恢复时需重放的日志范围;
- 说明哪些事务需回滚。
解析指出:
- P1已刷盘,但其修改可能由未刷盘日志驱动,故仍需重放所有LSN≥P1最后刷盘LSN的日志;
- P2未刷盘,其修改完全丢失,必须重放所有影响P2的日志;
- 回滚段中未提交事务的undo log,用于构造反向操作。
这揭示ACID中D的实现真相:Durability由WAL保证,而非数据页本身。只要日志刷盘成功,崩溃后就能重放;而Consistency由undo log保证——重放后若事务未提交,则用undo log回滚。
4.3 避坑:WAL分析中3个致命误区
现象1:认为日志刷盘(fsync)后数据页就一定持久化
原因:混淆WAL的“持久化”与数据页的“持久化”。答案集中某题解析称“日志刷盘=事务完成”,但实际日志刷盘只保证重放能力,数据页可能仍在buffer pool中。
解决:理解innodb_flush_log_at_trx_commit=1含义:事务提交时日志必须fsync,但数据页刷盘由innodb_io_capacity等参数异步控制。
现象2:忽略日志组提交(Group Commit)对性能的影响
原因:分析多个事务并发提交时,只考虑单个日志刷盘开销。答案集中未提及InnoDB会将多个事务日志合并一次fsync。
解决:Group Commit是提升TPS的关键。innodb_log_write_ahead_size参数控制日志预写大小,影响合并效率。
现象3:将redo log与binlog混为一谈,误用双写机制
原因:认为innodb_doublewrite=ON是为了防止binlog损坏。实则doublewrite buffer专用于保护redo log重放后的页写入,防止部分写(partial write)导致页损坏。
解决:doublewrite是InnoDB层机制,binlog是Server层机制。两者通过XA事务协调,但物理存储完全独立。
5. 查询优化器:答案集里“画出查询计划树”的题目,其实在教你逆向编译优化器的代价模型
5.1 执行计划不是终点,而是优化器决策链的快照
钱学忠教材第7章习题常给出SQL如SELECT * FROM orders JOIN customers ON orders.cid=customers.id WHERE customers.city='Beijing',要求“画出最优执行计划树”。答案集不直接给树形图,而是分步:
- 先估算
customers.city='Beijing'的选择率(如10%); - 再估算
orders.cid=customers.id连接基数(如customers表1000行,orders表10万行,则连接后约1万行); - 最后比较Nested Loop Join(NLJ)与Hash Join代价:NLJ需对customers每行扫描orders全表(1000×10万=10亿次I/O),Hash Join建哈希表代价约10万次I/O。
这个过程暴露优化器核心:它不“理解”SQL语义,只基于统计信息和代价模型做数学最优解。答案集中某题解析强调“若city列无索引,则选择率估算失效”,直指ANALYZE TABLE对优化器的重要性。
5.2 手动计算代价,破除“索引越多越好”的玄学
教材第7章某题对比两种索引策略:
- 方案A:
customers(city)单列索引; - 方案B:
customers(city,id)联合索引。
答案集要求计算两种方案下连接操作的I/O次数。解析显示: - 方案A需回表取
id,假设customers表1000行,每行回表1次,共1000次随机I/O; - 方案B因
id已在索引中,无需回表,I/O降为0。
这击穿了常见误区:索引不是为WHERE服务,而是为整个查询流水线服务。联合索引的“覆盖性”(covering index)本质是减少随机I/O——答案集用具体数字证明,当id被覆盖时,1000次I/O节省下来,比任何缓存优化都直接。
5.3 避坑:执行计划分析中4个实战陷阱
现象1:忽略索引统计信息陈旧,导致执行计划劣化
原因:答案集中某题假设city列选择率为10%,但生产环境若数据倾斜(如90%城市为'Beijing'),实际选择率90%,优化器仍按10%计算,误选NLJ。
解决:定期执行ANALYZE TABLE,或设置innodb_stats_auto_recalc=ON。监控INFORMATION_SCHEMA.STATISTICS中CARDINALITY字段。
现象2:将“Using index”等同于“使用了索引”,忽视覆盖索引本质
原因:EXPLAIN显示type=ref, Extra=Using index,但答案集某题解析指出:若SELECT列包含未索引字段,仍需回表。
解决:Using index仅表示索引本身满足查询,Using where表示索引用于过滤,Using filesort表示排序未用索引。
现象3:忽略连接顺序对哈希表内存的影响,触发磁盘临时表
原因:分析JOIN A,B,C时,只考虑I/O,忽略哈希表需内存。若A表100万行,B表10万行,优化器选A为驱动表,则哈希表需100万行内存,可能溢出到磁盘。
解决:join_buffer_size参数控制哈希表内存上限。答案集某题提示“小表应优先作为哈希表构建方”。
现象4:将ORDER BY优化等同于索引存在,忽略排序方向匹配
原因:有INDEX(a,b),但ORDER BY a DESC, b ASC无法使用该索引。答案集某题解析强调:索引顺序必须与ORDER BY完全一致(ASC/DESC需匹配),否则退化为filesort。
解决:MySQL 8.0支持降序索引,但5.7中需确保排序方向一致。
6. 从答案集到源码:为什么我每次调试死锁都先重画一遍钱学忠的锁等待图?
钱学忠《数据库原理及技术》的答案集,最锋利的价值不在“解题”,而在它用最原始的手工推演,把数据库内核中那些被封装成黑盒的机制,重新拆解为可触摸的物理动作。当我第一次在生产环境遇到死锁,SHOW ENGINE INNODB STATUS输出的那段日志曾让我头皮发麻——几十行十六进制地址、LSN编号、锁模式缩写,像天书。但翻开答案集第6章,重画那道经典的T1/T2锁等待图:标出每个WAITING FOR THIS LOCK TO BE GRANTED对应的行锁地址,对照HELD LOCKS中的RECORD LOCKS space id,突然就明白了:所谓“space id 123”,就是答案集里那个被我们手动画了三次的B+树页号;所谓“waiting for lock on (123, 456)”,就是当年在草稿纸上反复计算的page_no=123, offset=456。那一刻,黑匣子裂开了一道缝。
后来我养成了一个习惯:每次线上慢查询,不急着看EXPLAIN,而是打开答案集第7章,把SQL抄到纸上,手动估算选择率、连接基数、I/O次数。当估算结果与EXPLAIN的rows列偏差超过10倍时,我就知道该去INFORMATION_SCHEMA.STATISTICS查统计信息是否过期了。这个习惯救了我三次——一次是客户投诉报表超时,发现ANALYZE TABLE半年未执行;另一次是订单查询突然变慢,EXPLAIN显示走了全表扫描,而手算选择率显示索引应更优,最终定位到innodb_stats_persistent_sample_pages被调得太低。
最深刻的教训来自WAL日志题。某次数据库崩溃后恢复异常缓慢,日志显示重放了数小时。我翻出答案集第5章,重演“日志重放流程”:发现innodb_log_file_size设得太小(仅48MB),导致checkpoint过于频繁,崩溃后需重放大量日志。而答案集某题解析中一句轻描淡写的“日志文件大小应大于峰值写入量的2倍”,成了我调优的金科玉律。现在我部署新实例,第一件事就是计算innodb_log_file_size = innodb_io_capacity * 128KB * 2,再对照答案集的手算案例验证。
这些经历让我确信:数据库原理从不是悬浮的概念,而是由B+树分裂的页指针、WAL日志的LSN序列、锁等待图的有向边构成的物理世界。钱学忠的答案集,就是一张用纸笔绘制的、通往这个世界的等高线地图。它不告诉你山顶在哪,但教会你如何用指尖丈量每一寸坡度。从那以后我每次调试死锁、优化查询、调参崩溃恢复,都强制自己先重画一遍答案集里的对应图示——不是为了抄答案,而是让肌肉记忆刻下内核的脉搏。希望帮到你。
本文还有配套的精品资源,点击获取