ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

数据库关系代数与SQL实战入门:从课本习题到可执行查询

数据库关系代数与SQL实战入门:从课本习题到可执行查询

简介:本资源是《数据库系统概论(第5版)》王珊、萨师煊主编教材的配套课后习题详细答案,面向高校计算机、软件工程、信息管理等专业本科生及数据库初学者,用于巩固关系模型、关系代数、SQL语言等核心理论知识,辅助课程学习与期末备考。文件为单个Word文档(.doc),共1份,大小588KB,内容覆盖第2章关系数据库与第3章SQL语言全部重点习题,包括关系模型三要素、完整性约束判定、等值连接与自然连接辨析、SPJ关系代数表达式推导、SQL建表与多表查询语句编写等典型题型,并附有清晰解题步骤与关键注释。已有1723人学习下载,答案严格对应教材章节逻辑,术语规范、推导严谨,可直接用于作业参考、考前梳理与概念自查,是理解关系数据库底层原理与提升SQL实战能力的实用学习材料。

1. 别被“数据库系统概论”吓住:它不是天书,是你手边那本能查、能练、能抄答案的“关系代数练习册”

你刚点开这份叫《数据库系统概论第5版答案(王珊版)》的文档,心里可能嘀咕:“这名字怎么像大学期末考前夜在打印店狂敲的PDF?全是‘关系模型’‘范式’‘参照完整性’……我连SQL SELECT都写不利索,看这个有啥用?”
别急。先说个大实话:这份文档根本不是教材,也不是讲义,它是一份“带解题过程的课后习题标准答案集”——而且是专为零基础初学者设计的“脚手架式答案”。
它解决的问题特别具体:当你对着课本第二章那道“求供应工程J1零件的供应商号码SNO”的题发懵时,它不跟你讲什么叫“投影π”,而是直接给你写出πSno(σJno=‘J1’(SPJ)),再一行行翻译成“先从SPJ表里挑出JNO='J1'的行(这是选择σ),再只留下SNO这一列(这是投影π)”;当你被“等值连接和自然连接的区别”绕晕时,它用一句话点破:“等值连接是‘手动指定用哪个字段相等’,自然连接是‘自动找名字相同、类型相同的字段,连完还把重复列删掉’——就像你微信加好友输手机号(等值),和扫对方二维码自动通过(自然)。”
它适合谁?适合三类人:

  • 正在啃王珊《数据库系统概论》第5版、但卡在“关系代数符号看不懂”“SQL嵌套查询写不出来”的本科生;
  • 准备软考中级数据库系统工程师、想快速过掉“关系理论+SQL语法”基础模块的在职人;
  • 想转行做数据分析或后端开发,需要补上“数据库底层逻辑”这一课、但讨厌纯理论空讲的自学者。
    它不承诺让你一夜成为DBA,但它能让你今天下午就搞懂“为什么外码可以为空”“为什么视图不能随便UPDATE”,并亲手把课本P42那道SPJ四表联查题跑通。接下来,我们就从最扎手的第一关开始:不背定义,先动手画出你的第一个关系图。

2. 先搞懂一个概念:关系模型不是抽象理论,而是你每天都在用的“Excel多表联动”

很多新手一看到“关系模型由关系数据结构、关系操作集合、关系完整性约束三部分组成”就头皮发麻。别急着记这三句话——我们把它拆成你手机里正在用的功能。

2.1 关系数据结构:就是你熟悉的“表格”,但比Excel更较真

想象你用Excel管理班级通讯录:

  • A列是学号(唯一标识一个人),B列是姓名,C列是班级号。
  • 这张表本身,就是“关系数据结构”里的一个关系(Relation),也就是我们常说的“一张表”。
  • 表头A/B/C这些列名,叫属性(Attribute);每一行数据,叫一个元组(Tuple);整张表的结构定义(比如“学号是2位字符,不能为空”),叫关系模式(Relation Schema)。

提示:课本里说“关系是笛卡尔积的子集”,听着玄乎。其实就等于说:“这张表的所有行,必须是从所有可能组合中筛选出来的合法数据”。比如你班只有30人,那这张表就只能有30行,不能凭空冒出第31行——这就是“结构”在管事。

2.2 关系操作集合:不是命令行,而是“对表格动手指的5种基本动作”

课本第二章开头问:“关系代数的基本运算有哪些?”答案是并、差、笛卡尔积、投影、选择。别被名字吓退,它们对应的是你每天都在做的5件事:

运算名中文意思类比Excel操作课本例题中的样子白话解释
选择(σ)筛选行Ctrl+F找“城市=天津”σJno=‘J1’(SPJ)“从SPJ表里,只留下JNO这一列值等于'J1'的所有行”
投影(π)提取列只保留A列和C列πSno(σJno=‘J1’(SPJ))“先把上面筛出来的行,再只取SNO这一列”
笛卡尔积(×)两表暴力拼接把表1每行和表2每行配对S × P“S表有3行,P表有4行,结果就是12行(3×4),每行是S的一行+P的一行”
并(∪)合并去重把两个Sheet的数据粘一起,删重复行R ∪ S“把R表和S表的所有行合起来,重复的只留一份”
差(−)剔除交集用表1减去表2里有的行R − S“R表里有,但S表里没有的那些行”

你会发现:所有复杂查询,都是这5个动作的组合。比如课本P23第6题第4问:“求没有使用天津供应商生产的红色零件的工程号JNO”,答案是πJno(SPJ) − πJNO(σcity=‘天津’∧Color=‘红‘ (S∞SPJ∞P))。
拆开看:

  • 先算右边:σcity=‘天津’∧Color=‘红‘ (S∞SPJ∞P)→ 从S、SPJ、P三表连接结果里,找出“城市=天津且颜色=红”的记录;
  • 再πJNO→ 只取这些记录的JNO列;
  • 左边πJno(SPJ)→ 取SPJ表里所有JNO;
  • 最后“左边减右边” → 所有JNO里,去掉那些“被天津红零件污染过”的工程号。
    这根本不是数学推导,而是一套清晰的“数据清洗流水线”。

2.3 关系完整性约束:不是枷锁,而是你建表时自动开启的“防错开关”

很多人觉得“完整性约束”是DBA才管的事。错。它其实是你在CREATE TABLE时,悄悄按下的几个安全按钮:

  • 实体完整性:主键不能为空、不能重复。
    就像你填身份证号——系统强制要求你必须填,而且全世界不能有两个一样的。
    课本答:“若属性A是基本关系R的主属性,则属性A不能取空值”。

    实操提醒:建S表时Sno C(2) UNIQUE,这个UNIQUE就是实体完整性的落地。

  • 参照完整性:外键必须指向真实存在的主键,或为空。
    就像你填“部门编号”,系统会检查:这个编号在DEPT表里真的存在吗?如果不存在,就拦住你。
    课本答:“外码属性的值可以为空,但前提是它本身不是主属性”。
    举个生活例子:你填简历,“曾任职公司”这一栏可以空着(没工作过),但“身份证号”不能空(它是你的主键)。

  • 用户定义完整性:你自定义的业务规则。
    比如“职工年龄不得超过60岁”,课本P127第6题里用CONSTRAINT C1 CHECK (Age<=60)实现。
    这相当于在Excel单元格里设“数据验证”:只允许输入0-60的数字,超了就弹窗报错。

这三类约束,本质是同一套逻辑:用声明式规则代替人工校验。你不用每次INSERT前自己写代码检查“这个SNO在S表里有没有”,数据库替你做了。

2.4 避坑:新手写关系代数和建表时最容易翻车的5个地方

现象 → 原因 → 解决,一条一条来,全是血泪经验:

  1. 现象:用关系代数写“求供应红色零件的供应商”时,写了πSno(σCOLOR=‘红’(P)),结果查出来一堆空。
    原因:P表里只有零件信息(PNO, PNAME, COLOR),没有供应商SNO!你漏掉了SPJ表这个“桥梁”。
    解决:牢记“供应商→零件”不是直连,中间必须过SPJ表。正确写法是πSno(σCOLOR=‘红’(P∞SPJ)),先让P和SPJ连接,再筛选红色。

  2. 现象:SQL建表时复制课本答案CREATE TABLE S ( Sno C(2) UNIQUE,Sname C(6) ,Status C(2),CityC(4)),执行报错“Unknown data type C”。
    原因:课本用的是教学简化语法(C代表Character),真实数据库(MySQL/PostgreSQL)不认。
    解决:把C(2)改成CHAR(2),C(6)改成VARCHAR(6)。这是新手第一道坎,跨过去就海阔天空。

  3. 现象:写完SELECT * FROM SPJ,P WHERE SPJ.PNO=P.PNO,结果行数爆炸(比如SPJ有100行,P有10行,结果1000行)。
    原因:忘了加WHERE条件,导致笛卡尔积全量生成。课本P58第3题第4问就踩这个坑。
    解决:永远检查JOIN条件是否写全。用EXPLAIN(MySQL)或EXPLAIN QUERY PLAN(SQLite)看执行计划,如果显示“CROSS JOIN”,立刻回头补ON。

  4. 现象:给视图VSP插入数据INSERT INTO VSP VALUES('S1','P1',100),报错“Can't insert into a view”。
    原因:课本P79明确说“基本表的行列子集视图一般是可更新的”,但VSP是SELECT SNO,SPJ.PNO,QTY FROM SPJ,J WHERE ...—— 它涉及JOIN和WHERE,属于不可更新视图。
    解决:视图只用来查,增删改必须操作底层SPJ表。这是原则,不是bug。

  5. 现象:执行UPDATE P SET COLOR='蓝' WHERE COLOR='红'后,发现有些红色零件没变蓝。
    原因:字符串比较区分大小写或空格。课本答案写COLOR='红',但实际数据可能是'红 '(带空格)或'RED'(英文)。
    解决:先用SELECT COLOR, LENGTH(COLOR) FROM P WHERE COLOR LIKE '%红%'查真实存储格式,再针对性UPDATE。宁可多查一次,别信眼缘。

3. 先跑通第一个SQL:用课本SPJ四表,5分钟写出你的第一条“真正能执行”的查询

光看关系代数符号还是虚的。现在,我们用真实数据库(推荐SQLite,免安装,单文件)把课本P42第6题的查询跑通。目标:不抄答案,自己从建表开始,到查出结果,全程可复现。

3.1 第一步:创建四张表(照着课本答案改,但要改对数据类型)

打开SQLite命令行(或DBeaver/VS Code SQLite插件),逐条执行:

-- 创建S表(供应商):注意把课本的"C(2)"改成"TEXT","UNIQUE"保留 CREATE TABLE S ( Sno TEXT UNIQUE, Sname TEXT, Status TEXT, City TEXT ); -- 创建P表(零件):同理,"C(2)"→"TEXT","INT"保持不变 CREATE TABLE P ( Pno TEXT UNIQUE, Pname TEXT, Color TEXT, Weight INT ); -- 创建J表(工程项目) CREATE TABLE J ( Jno TEXT UNIQUE, Jname TEXT, City TEXT ); -- 创建SPJ表(供应情况):注意这里没有UNIQUE,因为(Sno,Pno,Jno)联合主键 CREATE TABLE SPJ ( Sno TEXT, Pno TEXT, Jno TEXT, Qty INT, -- 联合主键,防止重复供应记录 PRIMARY KEY (Sno, Pno, Jno) );

为什么用TEXT不用CHAR?因为现代数据库对变长字符串更友好,且避免CHAR(2)存'J1'时自动补空格导致匹配失败。这是课本没写的实战细节。

3.2 第二步:插入课本示例数据(最小可行集)

只插够验证第6题前两问的数据:

-- 插入S表数据(供应商) INSERT INTO S VALUES ('S1', '精益', '20', '天津'); INSERT INTO S VALUES ('S2', '盛锡', '10', '北京'); INSERT INTO S VALUES ('S3', '东方', '30', '上海'); -- 插入P表数据(零件) INSERT INTO P VALUES ('P1', '螺母', '红', 12); INSERT INTO P VALUES ('P2', '螺栓', '绿', 17); INSERT INTO P VALUES ('P3', '螺丝', '红', 14); -- 插入J表数据(工程) INSERT INTO J VALUES ('J1', '三建', '北京'); INSERT INTO J VALUES ('J2', '一汽', '长春'); INSERT INTO J VALUES ('J3', '船舶', '上海'); -- 插入SPJ表数据(供应关系) INSERT INTO SPJ VALUES ('S1', 'P1', 'J1', 200); INSERT INTO SPJ VALUES ('S1', 'P2', 'J1', 100); INSERT INTO SPJ VALUES ('S2', 'P1', 'J2', 300); INSERT INTO SPJ VALUES ('S2', 'P3', 'J3', 400); INSERT INTO SPJ VALUES ('S3', 'P1', 'J1', 500);

关键点:INSERT INTO SPJ VALUES(‘ S2’,‘ J6’,‘ P4’,200)这种课本答案里的空格(‘ S2’)是典型陷阱!务必删掉,否则WHERE 'S2' = ' S2'永远为假。

3.3 第三步:执行课本P42第6题第1问(最简单起点)

题目:“求供应工程J1零件的供应商号码SNO”
课本答案:SELECT DIST SNO FROM SPJ WHERE JNO='J1'
(注:课本笔误,DIST应为DISTINCT)

-- 执行查询(注意:DISTINCT不是DIST) SELECT DISTINCT SNO FROM SPJ WHERE JNO='J1';

预期结果:

SNO --- S1 S3

为什么是S1和S3?因为SPJ表里有两条JNO='J1'的记录:(S1,P1,J1)和(S3,P1,J1)。DISTINCT去重,所以S1只出现一次。

3.4 第四步:升级挑战——课本第3问(带JOIN的查询)

题目:“求供应工程J1零件为红色的供应商号码SNO”
课本答案:SELECT SNO FROM SPJ,P WHERE JNO='J1' AND SPJ.PNO=P.PNO AND COLOR='红'

-- 执行(注意表别名让语句更清晰) SELECT DISTINCT SPJ.SNO FROM SPJ JOIN P ON SPJ.PNO = P.PNO WHERE SPJ.JNO = 'J1' AND P.Color = '红';

预期结果:

SNO --- S1 S3

解释:SPJ表里JNO='J1'的记录有(S1,P1,J1)和(S3,P1,J1);P表里Color='红'的有P1和P3;JOIN后,只有P1能匹配上,所以S1和S3都供应了红色零件P1。这就是“连接”的威力——它自动把分散在不同表里的信息缝合起来。

3.5 第五步:验证课本第5问(最难的“除运算”)

题目:“求至少用了供应商S1所供应的全部零件的工程号JNO”
课本答案分两步:
A.SELECT DISTINCT PNO FROM SPJ WHERE SNO='S1'→ 得到(S1供应的零件:P1,P2)
B.SELECT JNO FROM SPJ WHERE PNO='P1' AND JNO IN (SELECT JNO FROM SPJ WHERE PNO='P2')

我们直接执行B步(A步结果已知):

-- 找出既用了P1又用了P2的工程号 SELECT DISTINCT JNO FROM SPJ WHERE PNO = 'P1' AND JNO IN ( SELECT JNO FROM SPJ WHERE PNO = 'P2' );

预期结果:

JNO --- J1

为什么是J1?因为SPJ表里:J1用了P1(S1,P1,J1)和P2(S1,P2,J1);J2只用了P1(S2,P1,J2),没用P2;J3只用了P3。所以只有J1满足“S1供应的所有零件(P1,P2)它都用了”。

恭喜!你刚刚用真实SQL跑通了课本前三道核心查询。这不是死记硬背,而是亲手验证了“关系代数→SQL→真实数据”的完整链条。

4. 别再死磕“范式”:用一张表的演变,看懂1NF、2NF、3NF到底在解决什么问题

第六章的“函数依赖”“BCNF”“多值依赖”是新手最大心理障碍。课本定义密密麻麻,但它的出发点特别朴素:怎么设计一张表,让它不容易存错、改错、删错数据?我们用一个真实场景来演示。

4.1 从“一张大杂烩表”开始:原始需求与它的灾难

假设你要建一张“学生选课成绩表”,老板说:“就一张表,包含所有信息!” 你可能这样设计:

学号姓名系名系主任课程号课程名成绩
001张三计算机王教授C001数据库85
001张三计算机王教授C002操作系统92
002李四数学陈教授C001数据库78

问题立刻爆发:

  • 数据冗余:张三的系名、系主任重复存了2次;李四的系名、系主任也重复。如果王教授退休了,你得改2行;
  • 更新异常:想把“计算机系”主任改成“李教授”,漏改一行,数据就自相矛盾;
  • 删除异常:如果张三退学了,删掉他所有选课记录,系主任信息也跟着消失了;
  • 插入异常:新成立一个“人工智能系”,还没招学生,你无法插入系信息(因为学号不能为空)。

这就是未规范化的表(0NF)——所有字段挤在一起,毫无结构。

4.2 拆成两张表:解决冗余,进入1NF和2NF

第一步,把“学生基本信息”和“选课成绩”分开:

学生表(Student):

学号姓名系名
001张三计算机
002李四数学

选课表(SC):

学号课程号成绩
001C00185
001C00292
002C00178

现在满足1NF了吗?
课本定义:“每一个分量是不可再分的数据项”。是的——学号是原子值(不是“001,002”这种列表),成绩是数字,没有逗号分隔的多值。✅

但还有问题:系名→系主任的依赖没解决。
在Student表里,“系名”决定“系主任”(计算机系→王教授),但“系名”不是主键(学号才是),这就是部分函数依赖:非主属性(系主任)依赖于主键的一部分(系名),而不是整个主键(学号)。

解决方案:再拆!

  • 系表(Department):| 系名 | 系主任 |
  • 学生表(Student):| 学号 | 姓名 | 系名 |(系名变成外键)

此时Student表:主键是学号,所有非主属性(姓名、系名)都完全依赖于学号;系名作为外键,指向Department表。这就消除了部分依赖,达到2NF。

4.3 再拆一次:解决传递依赖,进入3NF

现在Student表里还有隐患:如果“系名”变了(比如“计算机系”改名“智能科学系”),你得改Student表里所有相关行。更好的方式是用“系号”作主键:

系表(Department):

系号系名系主任
D01计算机王教授
D02数学陈教授

学生表(Student):

学号姓名系号
001张三D01
002李四D02

为什么这是3NF?
课本定义:“不存在非主属性对码的传递函数依赖”。

  • Student表主键是学号;
  • 非主属性是姓名、系号;
  • 姓名完全依赖学号(没问题);
  • 系号完全依赖学号(没问题);
  • 系号→系主任的依赖,在Department表里,不经过Student表传递。
    所以,所有依赖都是“直接”的,没有“学号→系号→系主任”这种链条。✅

这就是3NF的核心:让每个表只负责一件事,且所有信息都直接由主键决定。不是炫技,是防错。

4.4 避坑:范式学习中最常见的3个误解

  1. 误解:“范式越高越好,必须做到BCNF”。
    真相:BCNF会过度拆分,导致大量JOIN影响性能。实际项目中,3NF是黄金标准。课本P152第7题说“各关系模式都只有一个码……所以属于BCNF”,那是教学简化,真实业务表很少追求BCNF。

  2. 误解:“1NF就是不能有重复列”。
    真相:1NF的关键是“原子性”。比如“电话”字段存“1381234,1395678”就是违反1NF(逗号分隔多值);但存JSON字符串{"mobile":"138****1234","work":"010-12345678"},只要数据库把它当一个不可分割的字符串,就符合1NF。

  3. 误解:“函数依赖X→Y,必须Y的值由X计算得出”。
    真相:函数依赖是业务规则,不是数学公式。比如“身份证号→姓名”,不是用身份证号算出姓名,而是业务上规定:同一个身份证号,永远对应同一个人名。这是语义约束,不是算法。

5. 下一步学什么:从“抄答案”到“自己出题”,我的3步渐进路线

你已经能读懂课本答案、能建表、能跑通SPJ查询、能理解范式背后的防错逻辑。下一步不是去啃《数据库系统实现》,而是把这份答案集变成你的“肌肉记忆训练器”。我第一次学的时候,也是从这份答案起步,后来带新人发现:真正的入门门槛不在知识深度,而在“敢不敢把课本题当成真实需求来解”。以下是我亲测有效的3步法,每一步都对应一个具体动作:

5.1 第一步:把课本习题当“需求文档”,重写SQL(重点练“翻译能力”)

不要满足于看懂答案。拿出纸笔,把课本P42第6题当成产品经理给你的需求:

“老板要查:所有供应‘三建’工程的供应商,以及他们供应的零件颜色和重量。”

你的任务:

  • ① 识别涉及哪些表?(SPJ、J、P、S)
  • ② 识别过滤条件?(J.JNAME='三建')
  • ③ 识别要查的字段?(S.SNO, P.Color, P.Weight)
  • ④ 写出JOIN路径?(SPJ→J找工程,SPJ→P找零件)
  • ⑤ 最后写出完整SQL(不看答案!)。

做完后,对照课本P59第4题第9问的答案SELECT S.SNO, P.Color, P.Weight FROM SPJ,J,P,S WHERE SPJ.JNO=J.JNO AND SPJ.PNO=P.PNO AND SPJ.SNO=S.SNO AND J.JNAME='三建'。
差距在哪?是少写了某个JOIN条件?还是字段名写错了表前缀?把这些错误记在本子上,下次同类题自动预警。

5.2 第二步:给答案“挑刺”,尝试优化(重点练“工程思维”)

课本答案是教学导向,不是性能导向。比如P59第4题第4问:

“求没有使用天津供应商生产的红色零件的工程号JNO”
课本答案:SELECT DISTINCT JNO FROM SPJ WHERE JNO NOT IN (SELECT JNO FROM SPJ,P,S WHERE S.CITY='天津' AND P.COLOR='红' AND S.SNO=SPJ.SNO AND P.PNO=SPJ.PNO)

这个NOT IN在大数据量下会很慢。你的任务:

  • ① 用LEFT JOIN + IS NULL重写(更高效):
SELECT DISTINCT SPJ.JNO FROM SPJ LEFT JOIN ( SELECT SPJ2.JNO FROM SPJ SPJ2 JOIN S ON SPJ2.SNO = S.SNO JOIN P ON SPJ2.PNO = P.PNO WHERE S.City = '天津' AND P.Color = '红' ) AS bad_jno ON SPJ.JNO = bad_jno.JNO WHERE bad_jno.JNO IS NULL;
  • ② 思考:为什么这个更快?因为NOT IN遇到NULL会返回空结果,而LEFT JOIN逻辑更确定。

这不是炫技,是培养“数据量意识”——你写的SQL,未来可能跑在百万级数据上。

5.3 第三步:反向出题,给自己挖坑(重点练“设计能力”)

合上答案,打开空白文档,做这件事:

  • ① 选一个现实场景(比如“图书馆借阅系统”);
  • ② 设计3张表:Book(书号、书名、作者)、Reader(读者号、姓名、单位)、Borrow(读者号、书号、借期、还期);
  • ③ 给自己出3道题:
    • Q1:查所有“计算机类”书籍的借阅次数(需JOIN Book和Borrow);
    • Q2:查从未借过书的读者(用NOT EXISTS或LEFT JOIN);
    • Q3:查借阅过《数据库系统概论》且还期超过30天的读者(需日期计算)。
  • ④ 写出答案,再用SQLite建表、插测试数据、验证。

从“解题者”变成“出题者”,你就真正入门了。因为出题要思考:数据怎么组织(范式)、查询怎么关联(JOIN)、边界怎么处理(NULL、空集)。这比刷100道题都管用。

从那以后我每次拿到新业务需求,都强制走一遍这个流程:先画ER图(哪怕就3个框),再想清楚主外键,最后才写第一行SQL。不是为了显得专业,而是因为跳过这三步写的代码,90%会在上线后半夜把你叫醒。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进