资讯详情 数据库课后习题答案可执行验证:关系代数、范式分解与B+树索引
📅 2026/10/27 10:45:47
简介面向学习《数据库原理与设计》万常选版教材的高校学生与备考者这套课后习题答案覆盖第2至6章及第9章可作为课堂同步练习与期末复习的对照材料。压缩包共7个文件以3份doc解答文档为主另含2个sql示例脚本、1份txt作业说明和1个zip扩展资料包整体约85KBdoc文档对应关系模型与关系代数、E-R建模、关系数据理论与模式求精等章节sql脚本则可用于建表与完整性约束的动手验证。目前已有3906人学习下载在数据库课程学习者中具有较高认可度。读者可据此核对习题思路理解从现实世界到E-R图、再到关系模式的抽象过程掌握函数依赖分析与1NF至BCNF的规范化判定并借助脚本在真实数据库中复现练习适合作为课程学习与考试准备的辅助参考。1. 数据库课后习题答案为什么抄来的多半是错的翻开任何一份流传的数据库课后习题答案前五页基本都是对的从关系代数那一章开始错法就开始分化有人把投影写成不去重有人把除运算里的 NOT EXISTS 层数搞反有人在范式分解题里给出了一个无损但不保持依赖的方案还浑然不知。这些错误不会在纸面上暴露因为纸面答案没有读者只有抄写者。这篇讲的是另一条路把数据库课后习题答案从“记忆材料”变成“可执行结论”。每道题先在本地库里跑一遍、用手写脚本算一遍再回头对教材答案对不上就以跑出来的为准。适合三类人期末突击想把题型一次吃透的学生、复试或面试前要复盘关系代数与范式的人、以及替别人看作业的助教。工具只要一个单文件数据库加二十行 Python不需要装服务端。2. 关系代数与 SQL 双答案对照把习题搬进本地库跑一遍同一道题在教材里通常有两份答案一份关系代数式一份 SQL。做题时最省力的做法是只写一份然后抄另一份的形。这种做法的代价是两份答案只要有一处语义不同你就同时错了两遍而且不知道自己错在哪。把两边都落到可执行的层面差异会自己跳出来。2.1 同一道题为什么会有两个答案关系代数和 SQL 建立在同一个关系模型上理论上等价但落到纸面和落到数据库上中间隔着三层语义差。第一层是集合与包。关系代数里关系是集合投影之后自动去重SQL 默认是包语义SELECT sname FROM student出来几行就是几行。教材答案按集合写你按 SQL 跑行数对不上是必然的DISTINCT不是可选项。第二层是空值。关系代数里没有 NULL 这个成员教材习题表里一旦出现空成绩、空系别纸面推导会默认这些行“不参与比较”而 SQL 会按三值逻辑处理WHERE grade 80直接把 NULL 行过滤掉WHERE grade 80也过滤掉。很多人对答案时发现少了一行其实是空值在起作用。第三层是除运算。教材用R ÷ S表达“对所有”的语义SQL 没有对应算子必须用双重NOT EXISTS展开或者用分组计数凑两种写法在空集上结果不同。这一层是丢分重灾区。本地验证我一般用 SQLite单文件、不用起服务、语法覆盖选择投影连接分组这些高频题型。遇到FULL OUTER JOIN或者窗口函数的题切到 PostgreSQL 跑因为 SQLite 的老版本对前者支持有限跑出来不报错但结果不对这种坑不值得踩。2.2 三张最小表把选择、投影、连接跑通教材里绝大多数关系代数题都围绕“学生—课程—选课”这个三元结构把这三张表建起来后面所有题都能复用。-- 学生选课最小模型覆盖教材里大部分选择、投影、连接题 CREATE TABLE student ( sno TEXT PRIMARY KEY, -- 学号用 S1/S2 这种记号和教材写法直接对齐 sname TEXT NOT NULL, sdept TEXT, -- 系别允许为空专门用来复现空值题 age INTEGER ); CREATE TABLE course ( cno TEXT PRIMARY KEY, cname TEXT NOT NULL, credit INTEGER CHECK (credit 0) ); CREATE TABLE sc ( sno TEXT NOT NULL REFERENCES student(sno), cno TEXT NOT NULL REFERENCES course(cno), grade INTEGER CHECK (grade BETWEEN 0 AND 100), PRIMARY KEY (sno, cno) -- 复合主键一人一课只能选一次 ); INSERT INTO student VALUES (S1,A同学,计算机,20), (S2,B同学,计算机,21), (S3,C同学,数学,NULL), -- 年龄未知 (S4,D同学,数学,22); INSERT INTO course VALUES (C1,数据库,3),(C2,数据结构,4),(C3,离散数学,2); INSERT INTO sc VALUES (S1,C1,92),(S1,C2,85), (S2,C1,78), (S3,C2,NULL), -- 成绩未知用来验证聚合题的空值行为 (S4,C3,66);逻辑说明sno用文本而不是自增整数是为了和教材里的 S1、S2 记号一一对应抄答案时不用在脑子里做一层映射省下的注意力留给语义判断。sc的主键设成(sno, cno)复合主键正好对应“一个人一门课只能选一次”的业务语义同时它也让后面验证除运算时不必额外写DISTINCT——数据库已经替你去过重了。参数说明CHECK (grade BETWEEN 0 AND 100)这类约束在造测试数据时看着多余但它是后面第 4 章验证建表答案的基础设施。REFERENCES后面不写ON DELETE动作默认是 RESTRICT删除被引用的学生行会直接报错这一点恰好能用来验证参照完整性题。建好表之后把教材上第一道连接题落下来-- 题查计算机系学生的姓名和他们选的课名 -- 纸面写法 π_sname,cname(σ_sdept计算机(student) ⋈ sc ⋈ course) SELECT DISTINCT s.sname, c.cname FROM student s JOIN sc ON sc.sno s.sno JOIN course c ON c.cno sc.cno WHERE s.sdept 计算机 ORDER BY s.sname, c.cname;逻辑说明关系代数里“先做选择再连接”是划重点的优化思路SQL 里你不按这个顺序写优化器也会自己重排但纸面答案该按“选择—连接—投影”的顺序写因为教材判卷看的是推导过程。DISTINCT在这里不能省一个学生选了两门课投影到(sname, cname)上不会重复但如果题目改成只投影sname不去重就会比标准答案多出好几行。参数说明JOIN写成显式内连接而不是逗号加WHERE是为了让连接条件和过滤条件在语法上分开出问题时一眼能看出是哪一类条件写错。ORDER BY只影响可读性不影响结果集内容但对着答案逐行核对时排序能省掉你肉眼比对的时间。2.3 除运算与“全部”语义把关系代数翻译成双重 NOT EXISTS教材里最容易被抄错的一类题是“查询选修了全部课程的学生学号”。纸面答案是π_sno, cno(sc) ÷ π_cno(course)SQL 里没有这个算子用COUNT凑是一种写法但它在“课程表为空”时结果会翻车。稳妥的写法是双重否定-- 关系代数 R ÷ S 的 SQL 模板不存在一门课该学生没有选 SELECT DISTINCT sno FROM sc AS x WHERE NOT EXISTS ( SELECT 1 FROM course c WHERE NOT EXISTS ( SELECT 1 FROM sc y WHERE y.sno x.sno AND y.cno c.cno ) );逻辑说明读法是“对每一个学生 x不存在这样一门课 cx 没有选 c”。两层NOT EXISTS加一个普通EXISTS的语义嵌套是这类题的通用骨架。外层用sc做驱动而不是student是因为没选过任何课的学生在sc里根本没有行标准答案通常也不把他算进去如果题目要求把这种学生也算进来驱动表换成student即可。参数说明三个表别名x、c、y分别代表“待判定的学生”“被遍历的课程全集”“该学生的选课记录”别名写错不会报错只会静默地返回全集或者空集所以改这段代码时一定要跑一次小数据集对结果。这个骨架还有一个变体能一次覆盖教材里一整类题把内层的course换成任意集合。比如“查询至少选修了 S2 所选全部课程的学生”SELECT DISTINCT sno FROM sc AS x WHERE NOT EXISTS ( SELECT 1 FROM (SELECT cno FROM sc WHERE sno S2) c WHERE NOT EXISTS ( SELECT 1 FROM sc y WHERE y.sno x.sno AND y.cno c.cno ) );要改的只有一个派生表其余结构不动。这也是我建议把除运算模板背下来的原因——它把“换集合”变成了“换一行子查询”比每次重新推导省事得多。2.4 分组聚合题分母里到底算不算空值分组题的答案差异几乎全在分母上。教材写AVG(成绩)时不会讨论空值SQL 会忽略 NULL于是同一个班三个人、其中一个没成绩AVG的分母是 2COUNT(*)的分母是 3。这两种分母对应的是两道不同的题抄答案时最忌讳把两者混着用。-- 题查平均分高于 80 的课程号、平均分与有效人数 SELECT cno, AVG(grade) AS avg_grade, -- NULL 不计入分母 COUNT(grade) AS cnt_valid, -- 有效成绩人数 COUNT(*) AS cnt_all -- 选课人数含未出分 FROM sc GROUP BY cno HAVING AVG(grade) 80;逻辑说明HAVING作用在分组之后条件里可以直接用聚合函数WHERE作用在分组之前条件里不能出现聚合函数。教材题里“平均分大于多少的课”和“选课人数大于多少的课”看起来像同一类其实前者必须用HAVING后者可以用WHERE加子查询或者HAVING两种写法答案不唯一但推导过程必须自洽。参数说明GROUP BY cno之后SELECT列表里只能出现分组列和聚合函数SQLite 对这条规则比较宽松PostgreSQL 会直接报错。如果你的答案在 SQLite 上跑通了但在 PG 上报错不是答案错是分组列没写全按 PG 的严格模式改反而更接近标准答案。对照表把差异摊开看更清楚题型纸面答案常见写法SQL 正确写法差异根源平均成绩Σ成绩/选课人数AVG(grade)空值是否计入分母选课人数计数关系元组COUNT(*)或COUNT(grade)统计口径不同没选课的学生差集LEFT JOIN ... IS NULL差集要显式表达每门课的最高分分组取极值MAX(grade)极值天然忽略空值把这张表贴在显示器旁边分组题基本不会再错在同一处。3. 函数依赖与范式分解用闭包算法自己算出标准答案范式题的答案在流传版本里错得最有规律候选码少一个、分解多一个子模式、无损和保持依赖写反。原因是这类题手工推导的步骤长中间任何一步算错后面全错而且错得很像对的。把闭包计算交给代码是让这类题变成“运行一下就知道”的最短路径。3.1 手算闭包为什么会漏属性属性闭包 X⁺ 的定义是从 X 出发反复使用 F 中的函数依赖向右扩张直到集合不再变化。手算出错的根源有两个一是只扫一遍依赖就停手二是把“左部被包含”判断成了“左部包含”。举个具体的F {A→B, B→C, AC→D, D→E}求 A⁺。第一趟扫到 A→B得到 AB扫到 B→C 时如果已经用上了新得到的 B就能继续得到 C。手快的人第二趟才反应过来手更快的人直接漏掉。这类题属性数一般在 4 到 6 个子集总数最多 63 个全部枚举一遍的成本远低于手算复核的成本。候选码的判定条件是两条X⁺ 等于全部属性集 U且 X 的任意真子集都不满足这一条。第二条是极小性最容易漏。很多人找到一个能推出全部属性的集合就写上去那是超码不是候选码两者在判卷时差一整分。3.2 三十行 Python 算出闭包、候选码与超码# 输入属性集与函数依赖输出闭包、候选码、超码 from itertools import combinations attrs set(ABCDE) # 每个依赖写成 (左部集合, 右部集合)顺序无关 F [(set(A), set(B)), (set(B), set(C)), (set(AC), set(D)), (set(D), set(E))] def closure(x, F): 求属性集 x 在 F 下的闭包必须迭代到不动点 res set(x) changed True while changed: # 一趟扫描是手算出错的头号原因 changed False for l, r in F: if l res and not r res: # 左部被包含 且 右部还没进来 res | r changed True return res def candidate_keys(attrs, F): keys [] # 按基数从小到大枚举保证先找到的一定是极小的 for k in range(1, len(attrs) 1): for combo in combinations(sorted(attrs), k): s set(combo) if closure(s, F) ! attrs: continue if any(set(c) s for c in keys): # 已有候选码的真超集不算 continue keys.append(s) return keys print(A , .join(sorted(closure({A}, F)))) print(候选码:, [.join(sorted(k)) for k in candidate_keys(attrs, F)])逻辑说明closure里的while changed是不动点循环只要这一轮还有新属性进来就再扫一遍这是和手算唯一但关键的差别。l res是集合的子集判断左部所有属性都在当前闭包里才能用这条依赖。candidate_keys按基数从小到大枚举配合“已有候选码的真超集跳过”的判定一次就能得到全部极小候选码不会把超码混进来。参数说明属性集用set存不能用字符串否则AB和BA会被当成两个不同的东西。F里左部写set(AC)属性顺序不影响判定但排序后打印出来更接近教材写法方便逐字核对。上面的例子跑完输出候候选码是A因为 A→B→C加上 A 和 C 推出 DD 推出 EA 单独就能覆盖全部属性。这个结论手算也能得到但一旦依赖列表长到七八条代码的优势就出来了。超码不用单独写函数任何包含候选码的集合都是超码。教材题里“求所有候选码”和“求某个属性集是否为超码”是两种问法前者用上面的函数后者只要一行closure(s, F) attrs。3.3 无损连接判定二分分解的捷径与多分量的表格法无损连接的判定教材给了两个方法表格法chase和定理法。定理法只适用于把 R 分解成两个子模式的情况判据是公共属性集的闭包是否包含其中一个子模式的全部属性。这条捷径能覆盖八成的习题剩下的多分量分解必须走 chase。用刚才的 F {A→B, B→C, AC→D, D→E}U ABCDE分解成 R1(AB)、R2(ACDE)。公共属性是 AA⁺ ABCDE 包含 R2 的全部属性所以这个分解是无损的。判据只要求包含其中一个包含哪个都行。多分量分解就得画表。以下判定流程适合手算也适合写成脚本阶段操作终止条件初始化建 k 行 n 列表格第 i 行第 j 列若属性 j 在子模式 Ri 中填 aⱼ否则填 bᵢⱼ表格建成应用依赖对每条 X→Y找 X 列取值相同的行把这些行的 Y 列改成一致优先改成 a无行可改判定出现一行全是 a 即无损否则有损一次判定保持依赖的判定是另一回事判据是把 F 投影到每个子模式上得到 Fi若 ∪Fi 能推出原来的每一条依赖则保持依赖。实操时对每条原依赖 X→Y计算 X 在 ∪Fi 下的闭包看 Y 是否被包含。这两件事必须分开判3NF 分解要求两者都满足BCNF 分解只保证无损。def preserves_dependency(F, subschemas): 判断分解是否保持函数依赖对每条 X→YX 在投影依赖并集下的闭包要含 Y proj [] for sub in subschemas: for l, r in F: if l sub and r sub: # 只保留完全落在子模式内的依赖 proj.append((l, r)) for l, r in F: if not r closure(l, proj): return False, f丢失依赖 {.join(sorted(l))}-{.join(sorted(r))} return True, 保持依赖逻辑说明投影这一步只保留左右部都在同一个子模式内的依赖这是定义判定这一步对每条原依赖重算闭包闭包里没有右部就说明这条依赖在分解后推不出来。返回字符串而不是布尔值是为了直接看到是哪条依赖丢了改分解方案时有的放矢。参数说明subschemas传的是子模式属性集的列表元素也是set如果只传一个子模式函数等价于判断单表投影是否保持全部依赖这种情况几乎总是 False可以作为反例验证。3.4 分解答案不唯一怎么确认自己那份“合法”范式分解的结果天然不唯一。3NF 合成法要先求最小依赖集最小依赖集本身就可能有多份BCNF 分解法每轮挑一条违反的依赖拆挑哪条不同结果就不同。流传答案和你算出来的不一样未必是你错。判断自己的答案是否合法查三件事缺一不可分解是否无损、是否保持函数依赖目标范式是 3NF 时必查、每个子模式是否真的达到了目标范式。前两条上面已经有代码第三条要对每个子模式单独求候选码再检查有没有非主属性对码的部分依赖和传递依赖。写答案时的顺序建议固定成先列候选码再求最小依赖集再写分解过程最后补一句“该分解具有无损连接性且保持函数依赖”。判卷看的是这条链是否闭合只给一个分解结果而不给依据即使答案对也容易被扣过程分。这一条经验来自一次批改同一道题十份作业里有六份分解结果不同其中只有两份写清了判定依据另外四份哪怕结果碰巧正确也没拿到分。4. ER 图转关系模式把文字答案写成能建表的 DDLER 图转关系模式是教材里最容易“看着会、写着错”的一类题。规则只有几条但每条都有边界情形1:1 联系往哪边并、弱实体的主码怎么组、多值属性什么时候必须独立成表。把这些规则落成 DDL让数据库的约束替你判卷比对着标准答案逐字比对可靠得多。4.1 五条转换规则与习题里的高频变体不同教材的表述有出入但判定逻辑一致。下面这张表按“结构 → 转换结果 → 键怎么处理”整理覆盖了这类题的绝大部分问法。ER 结构转换结果键的处理习题常见问法1:1 联系并入任一端或独立成表并入端加另一端的码作外键并加唯一约束为什么并入参与度全部的一端1:n 联系并入 n 端n 端加 1 端的主码作外键外键放在哪边为什么m:n 联系独立成表两端主码组合成主键联系属性放哪里多值属性独立成表属主主码 属性值构成主键能不能留在原表里弱实体独立成表属主主码 部分码构成主键主码为什么不能只用部分码1:1 联系往哪边并是最常被问的。规则不是“随便挑一边”而是优先并入参与度是「全部」的那一端因为全部参与意味着这一端的每个实体都必须参与联系并进去不会产生空值。两端都是部分参与时并入任何一端都可以答案不唯一但要说明理由。4.2 把一道 ER 题写成 DDL假设题目是部门与员工是 1:n员工与项目是 m:n 且联系上带“工时”属性员工可以有多个联系电话。-- 部门—员工 1:n 并入 n 端员工—项目 m:n 独立成表电话是多值属性 CREATE TABLE dept ( dno TEXT PRIMARY KEY, dname TEXT NOT NULL ); CREATE TABLE emp ( eno TEXT PRIMARY KEY, ename TEXT NOT NULL, dno TEXT NOT NULL REFERENCES dept(dno) -- 1:n 的外键在 n 端 ); CREATE TABLE proj ( pno TEXT PRIMARY KEY, pname TEXT NOT NULL ); CREATE TABLE works ( eno TEXT NOT NULL REFERENCES emp(eno), pno TEXT NOT NULL REFERENCES proj(pno), hours NUMERIC CHECK (hours 0), PRIMARY KEY (eno, pno) -- m:n 联系两端主码组合成主键 ); CREATE TABLE emp_tel ( eno TEXT NOT NULL REFERENCES emp(eno), tel TEXT NOT NULL, PRIMARY KEY (eno, tel) -- 多值属性属主码 值 );逻辑说明外键位置是这类题的判分点。1:n 的联系并到 n 端因为一个员工只属于一个部门把dno放进emp不会产生重复组反过来把eno放进dept就要存一个集合违反第一范式。m:n 必须独立成表因为联系本身有属性hours塞进任何一端都会丢失信息。参数说明works的主键是(eno, pno)而不是加一个自增 id因为业务上“一个人在一个项目上只能有一条工时记录”复合主键直接表达了这条约束。emp_tel的主键必须包含eno如果只用tel做主键两个员工共用一个号码就会被拒绝与题意不符。4.3 用“应该插不进去”的数据验证约束DDL 建完不等于答案对。真正的验证是往里面插一批故意违规的数据看数据库是不是全部拒绝。全部报错说明约束齐了有一条插进去了说明你漏了一个约束而漏掉的这个约束很可能就是标准答案里写的那一句。-- 下面每条都应该报错任何一条成功插入都说明答案缺约束 INSERT INTO emp(eno, ename, dno) VALUES (E9,X同学,D9); -- D9 不存在参照完整性 INSERT INTO emp(eno, ename, dno) VALUES (E1,Y同学, NULL); -- 若题目要求全部参与这行不该成功 INSERT INTO works(eno, pno, hours) VALUES (E1,P1, -3); -- 工时为负CHECK 拦截 INSERT INTO works(eno, pno, hours) VALUES (E1,P1, 10); INSERT INTO works(eno, pno, hours) VALUES (E1,P1, 20); -- 主键重复 INSERT INTO emp_tel(eno, tel) VALUES (E1, 123456); INSERT INTO emp_tel(eno, tel) VALUES (E1, 123456); -- 同一号码重复逻辑说明这几条语句是按“主键、外键、CHECK、唯一性”四类约束各来一条组织的。第一条验证外键指向的部门不存在时会不会被拒第二条验证“全部参与”这个语义在 DDL 里有没有体现——如果题目写明每个员工必须属于一个部门dno就应该加NOT NULL这一行必须失败第三、四条验证取值范围和复合主键。参数说明跑之前先插一条合法的部门和员工数据否则所有语句都因为父表为空而报错你分不清是约束生效还是数据缺失。测试完记得把成功的那些行删掉或者干脆用一个临时库文件反复重建避免脏数据影响后续题目的验证。4.4 参照完整性动作删除属主时该发生什么教材里常有一问“删除一个部门其下员工应如何处理”。这不是让你背三个关键字而是让你回到 ER 图看参与度。员工的参与度是全部意味着员工必须挂在某个部门下那删除部门时就不能把员工的外键置空只能级联删除或者拒绝删除。参与度是部分置空才是合理选择。参与度合理动作DDL 写法全部参与级联删除或拒绝删除ON DELETE CASCADE或默认RESTRICT部分参与外键置空ON DELETE SET NULL且外键列允许 NULL写答案时把这层对应关系写出来比只写一个关键字有说服力。验证方式也简单插两条数据删掉父行看子行是消失了、变 NULL 了、还是报错。三种结果对应三种答案跑一次就确定。5. 排查清单习题答案最容易翻车的五处这一节记录的是我在对答案过程中反复撞上的五类问题每一条都按“现象、原因、解决”写。它们共同的特征是不报错结果看着合理但和标准答案差一点。5.1 三条查询类坑空集、分母、去重坑一NOT IN子查询返回空集。现象一道“查询没有选修 C1 的学生”的题NOT IN写法跑出来零行换成NOT EXISTS立刻就对了。原因子查询结果里只要出现一个 NULLx NOT IN (..., NULL)对任何 x 都返回 UNKNOWNWHERE把 UNKNOWN 当假处理整张表被过滤干净。选课表里成绩允许为空但学号一般不为空所以这个坑往往藏在“子查询选了允许为空的列”这种细节里。解决涉及子查询的否定判断一律用NOT EXISTS它对 NULL 免疫。如果非要用NOT IN在子查询里加WHERE col IS NOT NULL但这等于给自己加了一道需要记住的规则不如直接换写法。坑二聚合函数的分母混进空值。现象一道求平均分的题你算出来 82.5答案写 78.3。原因AVG(grade)的分母是有效成绩数SUM(grade)/COUNT(*)的分母是选课人数两者在有空成绩时结果不同。教材写Σ成绩/人数时“人数”指的是有成绩的人数还是选课人数取决于题目前文很多人默认成后者。解决写答案时把分母显式写出来AVG(grade)和SUM(grade)/COUNT(grade)等价和SUM(grade)/COUNT(*)不等价。跑一次对比两条 SQL 的结果差异立刻现形。坑三投影后没去重行数多出来。现象一道只投影单列的题SQL 跑出来 5 行关系代数答案写 3 行。原因关系代数的投影是集合运算自动去重SQL 的SELECT是包运算。教材答案按集合写你按包跑多出来的就是重复行。解决所有单表投影、以及投影后可能产生重复的题一律加DISTINCT。判断标准很简单如果纸面答案的元组数少于 SQL 的行数先怀疑去重再怀疑连接条件。5.2 两条建模类坑范式概念混淆与约束失效坑四把“无损连接”和“保持函数依赖”当成一回事。现象范式题写了“该分解无损且保持依赖”被批注“只满足其一”。原因这是两个独立性质。无损说的是分解后自然连接能还原原关系不丢元组也不多出伪元组保持依赖说的是原来的函数依赖能不能从分解后的投影依赖里推出来。3NF 分解可以同时满足两者BCNF 分解只保证无损可能丢掉依赖。很多人做完 BCNF 分解顺手把两句话都写上直接错。解决判定顺序固定成“先判无损再判保持依赖最后判目标范式”。目标范式是 BCNF 时保持依赖那一句要么不写要么写“该分解不保持函数依赖丢失的是 X→Y”。写清楚丢失了哪条反而比含糊带过更容易得分。坑五CHECK约束对 NULL 放行。现象给成绩加了CHECK (grade BETWEEN 0 AND 100)插入 NULL 居然成功了你以为是约束没生效。原因SQL 的三值逻辑里NULL BETWEEN 0 AND 100的结果是 UNKNOWNCHECK只在结果为 FALSE 时拒绝UNKNOWN 放行。这不是 bug是标准行为。解决如果题意要求成绩不能为空就写grade INTEGER NOT NULL CHECK (grade BETWEEN 0 AND 100)用NOT NULL兜住空值CHECK只管范围。验证方式是插一条 NULL 看它失不失败别靠读文档确认。这五条的共同点是都不报错。数据库不会告诉你“你的答案语义错了”它只会安静地给你一个看起来合理的错误结果。所以每改一次 SQL跑一次小数据集、对一次行数是最便宜的自检手段。6. 用执行计划反推 B 树计算题顺手验证索引判断题B 树那章的习题分两类一类是纯计算给块大小、键长、指针大小求阶数和树高一类是判断问某条查询能不能用上某个索引。前一类靠公式后一类靠最左前缀原则两者都能用本地库做交叉验证。计算题的关键是先分清阶数 m 的定义m 表示每个节点最多有 m 个孩子对应最多 m−1 个关键字。教材有的用“最多 m 个关键字”定义差一位答案就全错。我吃过这个亏一道求树高的题算出来比标准答案少一层回头查了半天公式问题出在阶数的定义上。做题时第一件事是把题目里阶数的定义抄在草稿纸顶上。单节点能放多少关键字算法是(块大小 - 节点头开销) / (键长 指针大小)向下取整。树高则由叶子层往上推每层能挂的子节点数就是阶数用总记录数反复取对数直到收敛。索引判断题更适合用执行计划实证。SQLite 里跑-- 复合主键 (sno, cno) 上按 cno 单独查能不能走索引 EXPLAIN QUERY PLAN SELECT * FROM sc WHERE cno C1; -- 按 sno 查最左前缀命中 EXPLAIN QUERY PLAN SELECT * FROM sc WHERE sno S1;第一条输出的关键词是SCAN sc说明全表扫描索引用不上第二条输出SEARCH sc USING INDEX sqlite_autoindex_sc_1 (sno?)说明命中了复合主键的最左列。结论就是最左前缀原则的实证复合索引只有从最左列开始连续匹配才能用跳列、乱序都不行。把这两条输出抄进答案比背定义有说服力。换成 PostgreSQL 时看的是EXPLAIN输出里的Index Scan和Seq Scan判定逻辑一样。注意小表上优化器可能主动选择全表扫描因为走索引的代价更高这时候输出是Seq Scan不代表索引用不上加个SET enable_seqscan off再跑一次就能看到真实情况。这一步很容易被忽略我头几次做这类题就是被小表的执行计划误导过。最后一个习惯每做完一章习题把这一章用到的表和查询存成一个脚本文件命名带上章节号。下次遇到同类题改几个字段就能复用不用从零搭环境。这个习惯让我在复盘范式题时省下了大量重复劳动也让我意识到数据库课后习题答案真正的价值不在那些元组上而在你能不能把它们跑起来、跑对、并且知道为什么对。希望帮到你。本文还有配套的精品资源点击获取