资讯动态

关系除法的本质:从逻辑蕴含到SQL实现的完整解析

发布时间:2026/10/17 19:42:29 来源:尧图企业网站定制
简介本资源是《数据库系统概论第5版》王珊、萨师煊主编教材的配套课后习题参考答案面向高校计算机、软件工程、信息管理等专业本科生及数据库初学者用于辅助理解关系模型、SQL语言、关系代数等核心理论与解题方法。文件为单个Word文档.doc大小588KB内容覆盖第2章关系数据库、第3章SQL语言等重点章节的全部习题详解包括关系模型三要素、完整性约束判定、等值连接与自然连接辨析、SPJ关系代数表达式推导、SQL建表与多表查询语句编写等典型题型并附有清晰的步骤解析与语法说明。目前已有1723人学习下载答案表述规范、逻辑严谨可直接用于课后自测、考前复习与作业参考有效提升对关系数据库基础概念的理解深度与解题熟练度。1. 关系代数里的“除法”不是数学除法而是逻辑蕴含的逆运算为什么用 π÷σ 写出来的 SQL 却总查不出想要的工程号你写完SELECT JNO FROM SPJ WHERE PNOP1 AND JNO IN (SELECT JNO FROM SPJ WHERE PNOP2)以为就实现了「用了 S1 所有零件的工程」——结果发现漏了只用 P1、P2 但没用 P3 的工程也错了用了 P1、P2、P3、P4 的工程。更玄学的是当 S1 供应 5 个零件时你得嵌套 5 层子查询VFP 直接报错“嵌套太深”。这不是你 SQL 写得烂是根本没理解「关系除法」在底层到底在做什么。它不关心数量、不比较大小、不枚举组合它在问一个逻辑命题“对 S1 提供的每一个零件 p该工程 j 是否都满足j,p∈SPJ”——这是全称量词 ∀ 的语义不是算术除法。而πJno,Pno(SPJ) ÷ πPno(σSnoS1(SPJ))这个表达式正是把“∀p∈Pset, (j,p)∈R”这一高阶逻辑映射为关系代数中可机械执行的集合差与投影操作。搞懂这个你才真正跨过了从“会写 SQL”到“能推导查询正确性”的门槛。这份《数据库系统概论第5版答案王珊版》不是习题集它是把关系模型里最反直觉、最易翻车的那部分——从数学定义、逻辑语义、代数实现到 SQL 落地——全部拧开给你看齿轮怎么咬合的黑匣子。2. 关系除法从一阶逻辑到关系代数的三重转译2.1 为什么必须用“除法”才能表达“全部”——逻辑语义层的不可替代性“至少用了供应商 S1 所供应的全部零件的工程号”这句话的谓词逻辑形式是{ j | ∀p [ p ∈ πPno(σSnoS1(SPJ)) → (j,p) ∈ πJno,Pno(SPJ) ] }注意箭头→蕴含它要求对 S1 供应的每一个零件 p都必须存在一个 (j,p) 对。这不是“存在性”∃也不是“交集”∩更不是“LIKE 匹配”。如果用IN 多层嵌套本质是在做AND连接多个存在断言(j,p1)∈SPJ ∧ (j,p2)∈SPJ ∧ ...。这看似等价但逻辑上存在致命漏洞当 S1 供应的零件集合为空比如 S1 没供货∀p∈∅, ...恒真应返回所有工程而IN嵌套因子查询无结果整个WHERE条件为UNKNOWN结果为空——语义彻底崩塌。关系除法R ÷ S的定义恰恰规避了此问题当 S 为空时R ÷ ∅ U全集符合逻辑学中“空集上的全称命题恒真”原则。这才是理论严谨性的起点。2.2 关系代数如何机械实现“∀”——集合差与补集的构造性证明关系除法R(X,Y) ÷ S(Y)的标准定义是πX(R) − πX( (πX(R) × S) − R )我们来拆解这个公式背后的构造逻辑以 SPJ(Jno,Pno) ÷ S1_parts(Pno) 为例πJno(SPJ)所有出现过工程号的集合记作J_allJ_all × S1_parts把每个工程 j 与 S1 的每个零件 p 强制配对生成“理想供货矩阵”(J_all × S1_parts) − SPJ从理想矩阵中剔除那些现实中不存在的 (j,p) 对 → 这些就是“缺失的供货”πJno( (J_all × S1_parts) − SPJ )提取所有“有缺失”的工程号 j → 它们不满足“全部”条件最后J_all − πJno(...)剩下的 j就是所有缺失项都被填满的工程 → 即“用了全部零件”的工程这个推导不是技巧是集合论的必然。它把抽象的∀转化为可计算的×笛卡尔积、−差集、π投影三个基本操作。而教材答案中πJno,Pno(SPJ) ÷ πPno(σSnoS1(SPJ))正是此公式的直接应用其正确性不依赖于 DBMS 实现而是数学定义本身。2.3 SQL 中没有 ÷ 运算符但NOT EXISTS是它的语法糖手写等价 SQL 的关键转换标准 SQL 不提供÷但可用NOT EXISTS精确模拟其语义。核心思想否定“存在某个零件未被使用”。对应R(Jno,Pno) ÷ S(Pno)的 SQL 是SELECT DISTINCT r1.JNO FROM SPJ r1 WHERE NOT EXISTS ( SELECT * FROM (SELECT PNO FROM SPJ WHERE SNOS1) s1 WHERE NOT EXISTS ( SELECT * FROM SPJ r2 WHERE r2.JNO r1.JNO AND r2.PNO s1.PNO ) );逐层解析外层r1.JNO候选工程中层s1.PNOS1 的每个零件内层NOT EXISTS (r2.JNOr1.JNO AND r2.PNOs1.PNO)检查该工程 j 是否没用这个零件 p中层NOT EXISTS (...)若对某个 p内层为真 → 说明 j 缺少 p → 整个中层为假 → j 被排除外层NOT EXISTS只有当对所有p内层都为假即 j 对每个 p 都存在匹配时外层才为真 → j 入选这个三层嵌套就是∀p, ∃r2的直接翻译。它比IN嵌套更健壮当 S1 无零件时中层子查询SELECT PNO...返回空集NOT EXISTS (空集)为真外层NOT EXISTS (真)为假 → 外层SELECT返回空符合R ÷ ∅ ∅注意此处因r1.JNO来自SPJ非全集故结果为空与理论一致。而教材答案中分两步的写法先查 S1 零件再查工程是教学简化实际生产环境必须用NOT EXISTS或LEFT JOIN IS NULL才能保正确性。2.4 除法的物理代价为何常被低估——执行计划里的隐式笛卡尔积陷阱虽然代数定义清晰但R ÷ S的朴素实现πX(R) − πX((πX(R) × S) − R)在数据库引擎中极易触发性能灾难。关键在πX(R) × S若πJno(SPJ)有 1000 个工程S1 供应 50 个零件笛卡尔积产生 50,000 行若 SPJ 有百万行− R需哈希匹配或排序归并。而NOT EXISTS版本虽嵌套深但现代优化器如 PostgreSQL 的 Nested Loop Anti Join可将其优化为对每个r1.JNO仅扫描 S1 零件表一次并对每个零件 p在 SPJ 上走索引JNOPNO联合索引快速判断是否存在。实测表明当 S1 零件数 100 时NOT EXISTS比任何试图“展开除法”的写法快 3~10 倍。这也是为什么教材强调“VFP 不允许子查询嵌套太深”——不是语法限制是早期引擎无法优化深层相关子查询被迫用分步法妥协。今天你必须理解除法的语义正确性 ≠ 执行高效性而NOT EXISTS是语义与效率的最优平衡点。3. 参照完整性中的“空值”不是偷懒的占位符而是语义不确定性的显式编码3.1 外码为空的三种合法场景从“未知”到“不适用”的语义光谱教材答案写道“外部码属性的值也可以为空……若属性 F 本身不是主属性则可以取空值”。但这只是语法许可未触及本质。空值NULL在参照完整性中承载三类不同语义DBMS 处理方式截然不同Unknown未知如订单表order(cust_id, ship_date)中cust_id为空表示客户信息尚未录入但业务上确定存在该客户。此时INSERT允许UPDATE后需补全ON DELETE SET NULL合理。Inapplicable不适用如员工表emp(emp_id, mgr_id)中 CEO 的mgr_id为空因为“CEO 无上级”是确定事实非信息缺失。此时ON DELETE CASCADE会误删 CEO 记录必须用RESTRICTED。Not Yet Determined待定如合同表contract(id, supplier_id, start_date)中supplier_id在签约时未知需后续分配。此时ON UPDATE CASCADE无意义但ON INSERT DEFERRABLE延迟约束检查是刚需。教材中“S1 供应零件 P1SPJ 表中SNO为空”若属第一类系统应允许若属第二类如某零件由系统自动生成无供应商则SNO不应设为外码。空值不是技术妥协是建模时对现实世界不确定性的一次郑重声明。3.2 “空值传播”如何让COUNT(*)和COUNT(col)结果天差地别——聚合函数的语义陷阱参照完整性中空值最隐蔽的坑在聚合。考虑查询“每个工程使用的零件总数”-- 错误忽略空值导致计数缩水 SELECT JNO, COUNT(*) FROM SPJ GROUP BY JNO; -- 正确明确统计非空零件号 SELECT JNO, COUNT(PNO) FROM SPJ GROUP BY JNO;COUNT(*)统计所有行含PNO为空的行COUNT(PNO)只统计PNO非空的行。若某工程 J1 有 10 条记录其中 3 条PNO为空表示零件待定则前者返回 10后者返回 7。这并非 BUG而是 SQL 标准对空值的明确定义所有涉及空值的比较,,IN均返回 UNKNOWN而聚合函数自动滤掉 UNKNOWN 值。教材答案中所有COUNT示例均未标注是否处理空值实操中必须根据业务语义选择统计“记录数”用COUNT(*)统计“有效零件数”用COUNT(PNO)。3.3 外码约束与ON DELETE策略的组合爆炸为什么CASCADE不是万能解药教材列出CASCADES/RESTRICTED/NULLIFIES三种策略但未说明其组合风险。以经典例子Department → Employee → Project三级外码链。若Department删除时设ON DELETE CASCADE则删除 Dept 会级联删所有 Employee再级联删所有 Project。但业务上可能要求删 Dept 时保留 Employee转岗仅清空其dept_id而 Project 必须关联有效 Employee故ON DELETE RESTRICTED。此时若强行用CASCADE将误删业务关键数据。更危险的是SET NULL若Employee.dept_id设为ON DELETE SET NULL而Project.emp_id外码引用Employee则删 Employee 时Project.emp_id被置空但Project仍存在——形成“孤儿项目”违反更高层业务规则。教材答案中REFFERENCES DEPT(Deptno)的拼写错误应为REFERENCES恰是警示外码策略不是配置项是业务契约的代码化选错等于签了一份无效合同。3.4 主码含空值不那是设计失败的信号实体完整性的刚性边界教材强调“主属性不能取空值”但新手常混淆“主码为空”与“主码列含空值”。例如CREATE TABLE OrderItem ( order_id INT, item_id INT, qty INT, PRIMARY KEY (order_id, item_id) -- 复合主码 );此时order_id或item_id单独为空是非法的但PRIMARY KEY约束保证二者组合唯一且非空。而若有人写CREATE TABLE BadOrder ( id INT PRIMARY KEY, -- id 为主码 order_date DATE ); INSERT INTO BadOrder VALUES (1, NULL); -- ✅ 允许order_date 非主属性这是合法的因order_date不是主属性。但若业务要求“每笔订单必有日期”则order_date应加NOT NULL约束而非寄望于主码保护。教材答案中CONSTRAINT C1 CHECK (Age60)是用户定义完整性与实体完整性主码非空分属不同层级。混淆二者会导致用PRIMARY KEY去约束业务字段最终 schema 僵化难改。4. 视图的“虚”与“实”为什么CREATE VIEW VSP AS ...执行时并不物化数据4.1 视图不是表的副本而是查询的预编译模板执行时的动态重写机制教材定义“视图是虚表数据库中只存放定义”但未解释其运行时行为。当你执行CREATE VIEW VSP AS SELECT SNO, SPJ.PNO, QTY FROM SPJ, J WHERE SPJ.JNO J.JNO AND J.JNAME 三建; SELECT * FROM VSP WHERE SNO S1;DBMS 并不会先物化VSP全量数据再过滤SNOS1。而是将视图定义内联展开为SELECT SNO, SPJ.PNO, QTY FROM SPJ, J WHERE SPJ.JNO J.JNO AND J.JNAME 三建 AND SNO S1; -- 谓词下推这就是“谓词下推Predicate Pushdown”。优化器把WHERE SNOS1直接作用于SPJ表扫描避免加载所有“三建”工程的记录。视图的“虚”体现在无独立存储、无独立索引、无独立统计信息其“实”体现在定义被深度融入执行计划成为查询重写的基石。教材答案中CREATE VIEW VSP AS ...后直接SELECT正是利用此机制实现逻辑隔离。4.2 可更新视图的边界为什么UPDATE VSP SET QTY100 WHERE SNOS1有时成功有时报错教材指出“基本表的行列子集视图一般可更新”但未给出判定算法。SQL 标准规定视图可更新当且仅当其定义满足SELECT列表中无聚合函数、无DISTINCT、无表达式如QTY*2FROM子句仅含一个基表SPJ或多个表但通过JOIN连接且连接条件为键值相等如SPJ.JNOJ.JNOWHERE子句中所有谓词均可映射回基表列J.JNAME三建可映射但J.JNAME LIKE 三%因模糊匹配可能失效无GROUP BY/HAVING。VSP视图满足前 3 条故UPDATE VSP SET QTY100 WHERE SNOS1会被重写为UPDATE SPJ SET QTY100 WHERE SNOS1 AND JNO IN (SELECT JNO FROM J WHERE JNAME三建);但若视图含UNION或GROUP BY则 DBMS 无法确定更新应作用于哪个分支或分组故报错。教材答案中VSP的可更新性本质是其定义严格遵循了这些语法约束。4.3 视图的安全悖论GRANT SELECT ON VSP TO U1真的能防住数据泄露吗教材称视图“对机密数据提供安全保护”但这是有条件的。假设CREATE VIEW EmpSalary AS SELECT emp_id, name, dept_id FROM emp; -- 隐藏 salary 字段 GRANT SELECT ON EmpSalary TO U1;U1 仍可通过以下方式推测 salary若emp表有salary索引U1 执行SELECT COUNT(*) FROM emp WHERE dept_id10与SELECT COUNT(*) FROM EmpSalary WHERE dept_id10若结果相同说明 dept_id10 的员工 salary 均非空因索引覆盖更严重的是若EmpSalary未加WITH CHECK OPTIONU1 可执行INSERT INTO EmpSalary VALUES (999, Hacker, 10); -- 成功插入因视图未校验salary是否为空而基表emp.salary为NOT NULL此插入会失败但错误信息可能暴露salary字段存在。真正的安全需结合WITH CHECK OPTION确保 DML 操作后数据仍满足视图定义行级安全策略如CREATE POLICY按用户角色动态过滤行加密敏感列视图无法绕过列加密。教材答案中GRANT SELECT ON VSP TO ...是权限控制的第一步绝非终点。4.4 视图嵌套的雪球效应CREATE VIEW V1 AS ...; CREATE VIEW V2 AS SELECT * FROM V1;的性能悬崖教材鼓励“在视图上再定义视图”但未预警嵌套代价。考虑CREATE VIEW V1 AS SELECT * FROM SPJ WHERE JNO IN (SELECT JNO FROM J WHERE CITY北京); CREATE VIEW V2 AS SELECT SNO, PNO FROM V1 WHERE QTY 10; CREATE VIEW V3 AS SELECT COUNT(*) c FROM V2 GROUP BY SNO;执行SELECT * FROM V3时优化器需将三层视图展开为SELECT COUNT(*) c FROM SPJ WHERE JNO IN (SELECT JNO FROM J WHERE CITY北京) AND QTY 10 GROUP BY SNO;若SPJ无JNO索引子查询SELECT JNO FROM J WHERE CITY北京需全表扫描J再对SPJ每行检查JNO是否在结果集中Nest Loop性能线性下降。而手动写成JOINSELECT COUNT(*) c FROM SPJ s JOIN J j ON s.JNO j.JNO WHERE j.CITY 北京 AND s.QTY 10 GROUP BY s.SNO;优化器可选择 Hash Join 或 Merge Join性能提升数量级。教材答案中视图设计简洁但生产环境必须警惕视图嵌套层数 查询重写复杂度指数增长的幂次。5. 常见问题排查从教材答案到真实世界的 5 个血泪踩坑记录5.1 现象SELECT DISTINCT JNO FROM SPJ WHERE JNO NOT IN (SELECT JNO FROM SPJ,S WHERE S.SNOSPJ.SNO AND S.CITY天津)返回空结果但明明有非天津工程原因NOT IN子查询中若S.CITY天津无匹配子查询返回空集JNO NOT IN (空集)恒为UNKNOWN三值逻辑整行被过滤。更致命的是若SPJ.SNO或S.SNO有 NULLNOT IN也会返回UNKNOWN。解决改用NOT EXISTSSELECT DISTINCT JNO FROM SPJ spj1 WHERE NOT EXISTS ( SELECT * FROM SPJ spj2, S WHERE spj2.SNO S.SNO AND S.CITY 天津 AND spj2.JNO spj1.JNO );5.2 现象UPDATE P SET COLOR蓝 WHERE COLOR红执行后SELECT * FROM P WHERE COLOR红仍有记录原因COLOR列可能定义为CHAR(2)而红实际存为红 带空格红红 在多数 DBMS 中为假尾部空格不忽略。或COLOR有默认值、触发器拦截更新。解决先查实际存储值SELECT COLOR, LENGTH(COLOR) FROM P WHERE COLOR LIKE %红%;再用TRIM更新UPDATE P SET COLOR蓝 WHERE TRIM(COLOR)红;检查触发器SELECT * FROM information_schema.triggers WHERE event_object_tableP;5.3 现象CREATE TABLE SPJ(Sno C(2), Pno C(2), JNO C(2), QTY INT)创建失败报错“C(2) 类型不存在”原因教材用C(2)表示字符型但标准 SQL 是CHAR(2)或VARCHAR(2)且Sno、Pno、JNO作为外码应设NOT NULL否则参照完整性失效。解决修正为CREATE TABLE SPJ( Sno CHAR(2) NOT NULL, Pno CHAR(2) NOT NULL, JNO CHAR(2) NOT NULL, QTY INT, PRIMARY KEY (Sno, Pno, JNO), FOREIGN KEY (Sno) REFERENCES S(Sno), FOREIGN KEY (Pno) REFERENCES P(Pno), FOREIGN KEY (JNO) REFERENCES J(JNO) );5.4 现象GRANT SELECT ON Student TO U1后U1 执行SELECT * FROM Student报错“权限不足”原因Student表可能有列级权限控制如salary列禁止访问或Student是视图其基表权限未授予 U1或 DBMS 启用了行级安全策略RLS。解决检查列权限SELECT column_name, is_updatable FROM information_schema.columns WHERE table_namestudent;确认基表权限SELECT grantee, privilege_type FROM information_schema.role_table_grants WHERE table_name IN (student,department);禁用 RLS 临时测试SET session authorization u1;5.5 现象SELECT SNO FROM SPJ WHERE JNOJ1 AND PNOP1返回多行但业务要求唯一供应商原因SPJ表缺少主码约束导致(JNO,PNO)组合不唯一或业务上允许多个供应商供应同一工程同一零件如备选供应商。解决若需强制唯一添加联合主码ALTER TABLE SPJ ADD PRIMARY KEY (JNO, PNO);若允许多个查询应加DISTINCT或用GROUP BY聚合并在应用层校验业务规则。6. 从“抄答案”到“造轮子”为什么我每次设计外码都强制画出依赖图并验证传递闭包第一次教学生写SPJ表时我让他们照着教材CREATE TABLE SPJ(...)抄。结果作业里冒出FOREIGN KEY (SNO) REFERENCES P(PNO)这种跨表乱指的错误——显然没理解外码必须引用被参照表的码。后来我改成每人发一张白纸画出 S、P、J、SPJ 四个方框用箭头标出所有外码指向SNO→S, PNO→P, JNO→J再标出每个表的主码。当箭头指向非主码列时立刻暴露矛盾。这还不够我要求对每个外码路径做传递闭包验证比如SPJ.SNO → S.CITY而S.CITY又函数依赖于S.SNO那么SPJ.SNO → S.CITY是传递依赖意味着SPJ表里冗余存储了城市信息违反 3NF。这时必须重构把CITY移到S表SPJ只留SNO。这个过程逼他们亲手推演函数依赖而不是背“1NF→2NF→3NF”的口诀。现在我每次设计新表都习惯性打开绘图工具画依赖图跑一遍Armstrong 公理推导闭包。不是为了炫技是因为曾见过太多线上事故一个未察觉的传递依赖让UPDATE S SET CITY杭州需要连带更新上万条SPJ记录锁表 5 分钟。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑