数据库6-8章复习
📘 第6章:基于E-R模型的数据库设计 (共83页)
核心:如何画E-R图,以及如何将E-R图转化为数据库表。
- P1 - P6:数据库设计基础与导论
- 设计阶段的划分(概念设计、逻辑设计、物理设计)。
- 糟糕设计的表现(数据冗余、信息不完整)。
- P7 - P11:实体与实体集 (Entity Sets)
- 实体的定义、主键、以及在E-R图中的画法(矩形)。
- P12 - P19:联系与联系集 (Relationship Sets)
- 二元联系、多元联系、角色 (Roles) 的定义,以及在E-R图中的画法(菱形)。
- P20 - P22:复杂属性 (Complex Attributes)
- 简单/复合属性、单值/多值属性、派生属性的区分与画法。
- P23 - P32:映射基数与约束 (Mapping Cardinality & Constraints)
- 一对一、一对多、多对多 (1:1, 1:N, N:M) 的连线画法。
- 全部参与 (双线) 与 部分参与 (单线)。
- P33 - P36:主键的选择 (Primary Key)
- 如何为实体集和联系集选择主键。
- P37 - P40:弱实体集 (Weak Entity Sets)
- 弱实体的定义、标识实体、分辨符(虚下划线)及画法(双边框矩形和菱形)。
- P41 - P42:冗余属性与综合案例
- 一个完整的大学数据库 E-R 图全貌展示。
- P43 - P51:E-R图向关系模式(表)的转换 (Reduction to Relational Schemas)
- 强/弱实体集如何建表、复合/多值属性如何建表、多对多/一对多联系如何建表及合并简化。
- P52 - P63:扩展E-R特性 (Extended E-R Features)
- 特化与泛化 (P53-P59):实体继承(ISA),不相交/重叠约束,建表方案。
- 聚集 (Aggregation, P60-P63):把“联系”当成一个“实体”来处理复杂的关联。
- P64 - P71:设计常见问题与避坑 (Design Issues)
- E-R图的常见错误画法,何时用实体/何时用属性,二元与多元联系的转换。
- P72 - P76:E-R图符号总结与替代记法
- 全套符号总结、Chen记法、IDE1FX(鸦脚记法)对比。
- P77 - P83:UML类图对比 (UML Class Diagrams)
- UML类图与E-R图在画法和基数表达上的差异。
📕 第7章:关系数据库设计与规范化 (共101页)
核心:用数学理论(函数依赖)找出现有表结构的问题,并将其无损拆分(范式理论)。
- P1 - P10:规范化概述与无损分解
- 为什么需要规范化(解决更新异常与冗余)。
- 有损分解 vs 无损分解 (Lossless Decomposition) 的直观例子。
- P11 - P17:函数依赖基础 (Functional Dependencies, FDs)
- 什么是 $\alpha \rightarrow \beta$,它与超键/候选键的关系,平凡函数依赖。
- P18 - P21:无损分解与依赖保持的判定
- 如何用函数依赖证明分解是无损的;什么是依赖保持 (Dependency Preservation)。
- P22 - P27:Boyce-Codd 范式 (BCNF)
- BCNF的定义(左边必须是超键);BCNF分解实例及其无法保持依赖的局限性。
- P28 - P35:第三范式 (3NF) 与 更高范式引入
- 3NF的定义(允许右边是主属性以保持依赖);3NF的缺点;引入第四范式(4NF)的动机。
- P36 - P45:函数依赖理论深化 (FD Theory)
- 阿姆斯特朗公理 (P39-P41)。
- 属性集闭包算法 (Attribute Closure, P43-P45)(用来求候选键非常重要)。
- P46 - P54:正则覆盖与无关属性 (Canonical Cover)
- 去除多余的推导规则,计算最简化的函数依赖集。
- P55 - P58:依赖保持的算法验证
- 如何测试一个分解是否破坏了原来的函数依赖。
- P59 - P64:BCNF 分解算法
- 标准分解流程及实例演示。
- P65 - P73:3NF 分解算法及综合对比
- 如何基于正则覆盖生成3NF;BCNF 与 3NF 的终极对比。
- P74 - P85:多值依赖 (MVDs) 与 第四范式 (4NF)
- 解决表中有两组互相独立的多值数据导致的冗余($\alpha \rightarrow\rightarrow \beta$);4NF的定义与分解。
- P86 - P93:实际数据库设计杂项
- 反规范化(为了查询性能故意引入冗余)、交叉表问题、时态数据 (Temporal Data) 的建模。
- P94 - P99:(附录性质) 3NF分解算法的正确性证明
- P100 - P101:第一范式 (1NF)
- 属性不可再分的定义(放在最后作为补充)。
📗 第8章:复杂数据类型 (共37页)
核心:关系型数据库(二维表)之外的数据模型,为了适应Web和大数据时代。
- P1 - P5:半结构化数据与数组 (Semi-Structured Data)
- 为什么需要半结构化(灵活的Schema);数组数据类型(用于科学计算)。
- P6 - P11:JSON 与 XML
- JSON (P7-P8):基于键值对和数组,Web传输的主流格式,如何在SQL中处理JSON。
- XML (P9-P11):基于标签的树状结构,早期的主要交换格式。
- P12 - P16:知识表示与知识图谱 (Knowledge Representation)
- RDF (资源描述框架):主谓宾三元组 (Subject, Predicate, Object);查询语言 SPARQL。
- P17 - P21:面向对象数据库 (Object Orientation)
- 在数据库中自定义复杂类型 (Type) 和表继承;对象关系映射 (ORM,如 Hibernate)。
- P22 - P26:文本数据与检索 (Textual Data & Information Retrieval)
- TF-IDF 算法 (P23):计算关键词权重的经典算法。
- PageRank 算法 (P24-P25):Google 经典的网页排序(基于链接的随机游走)。
- 检索效果评估 (Precision & Recall)。
- P27 - P37:空间数据 (Spatial Data)
- 几何数据 (Geometric) 与 地理数据 (Geographic, GIS)。
- 点、线、多边形的表示方法;栅格(Raster) 与 矢量(Vector) 数据;空间查询种类。
好的!我们正式进入第6章的学习。
这前6页的内容虽然偏向理论,但它是整个数据库设计的核心思想指引。你可以把它想象成“建一栋大楼前的工程规划”。
我们来看看建设一个数据库,需要经历哪些步骤,以及要避开哪些致命的坑。
1. 数据库的设计全流程 (Design Phases, P3 - P4)
要从零开始为一家公司或学校建一个数据库,绝不是上来就写 CREATE TABLE 建表代码的,而是要经过一个严谨的“三步走”流程:
- 第一阶段:需求分析 (Initial Phase)
- 做什么:去跟未来的数据库用户聊天,搞清楚他们到底需要存什么数据。比如学校需要存学生信息、选课信息、老师工资等。
- 第二阶段:概念设计 (Conceptual Design)
- 做什么:选择一个数据模型(本章选用的就是 E-R模型),把刚才那堆杂乱的需求,翻译成一个抽象的概念模式 (Conceptual Schema)。
- 通俗理解:这就好比画一张建筑草图,告诉大家大楼有几层、哪里是楼梯、哪里是教室,并且要标明未来这里会发生什么业务(比如:学生选课、发工资)。
- 第三阶段:进入实现 (Final Phase - Implementation)
- 这个阶段又分为两步:
- 逻辑设计 (Logical Design):把刚才画的 E-R 草图,转换成真正的数据库表结构 (Schema)。
- 这里包含一个业务决定:我们要存哪些属性?(比如要不要存学生的曾用名?)
- 还包含一个计算机科学决定:这些属性应该怎么分配到不同的表中?(这是个技术活,分配不好会出大问题)。
- 物理设计 (Physical Design):决定这些数据在电脑硬盘上具体怎么存(建什么索引、用什么文件结构)。
- 逻辑设计 (Logical Design):把刚才画的 E-R 草图,转换成真正的数据库表结构 (Schema)。
- 这个阶段又分为两步:
2. 烂设计的两个“致命陷阱” (Design Alternatives, P5)
在上面提到的逻辑设计(即决定怎么建表)时,有无数种建表方案。但我们必须极力避免掉进两个大坑:
- 数据冗余 (Redundancy)
- 什么是冗余:同样的一份信息,你在数据库里存了好几遍。
- 为什么可怕:浪费存储空间还是次要的,最可怕的是会导致数据不一致 (Inconsistency)。
- 举个例子:如果你把“计算机系主任是张三”这个信息,在100个学生的选课记录里都存了一遍。哪天系主任换成了李四,你必须去改这100条记录。万一漏改了1条,数据库里就会出现“两个系主任”的矛盾情况。
- 信息不完整 (Incompleteness)
- 什么是不完整:表结构设计得太死板,导致某些现实中发生的业务,你竟然没法存进数据库。
- 举个例子:假设你的表规定“必须有学生的学号,才能存一门课的信息”。那如果一门新课刚开设,还没人选,这门课的信息就存不进去了!
3. 我们该用什么武器来设计? (Design Approaches, P6)
既然设计数据库到处都是坑,计算机科学家们为我们提供了两样“神器”(也就是我们第6章和第7章要学的东西):
- 武器一:实体-联系模型 (Entity-Relationship Model,简称 E-R 模型) —— 第6章核心
- 特点:感性、直观、图形化。
- 思路:把现实世界看作是由一个个实体 (Entities)(比如:学生、老师、课程)和它们之间的联系 (Relationships)(比如:选修、讲授)组成的。最后画出一张清晰的 E-R 图。
- 武器二:规范化理论 (Normalization Theory) —— 第7章核心
- 特点:理性、数学化、严谨。
- 思路:用一套数学公式(主要是“函数依赖”)去测试你设计出来的表是不是个“烂设计”。如果是,就用算法把它拆分成好设计。
小结: 前6页的潜台词就是:建库不能瞎建表,我们要先用直观的 E-R 图把业务理顺(第6章),然后再用严谨的规范化理论去检验它有没有冗余和异常(第7章)。
这就是导论部分的全部核心思想。准备好进入下一步 E-R图的三大基本组件(实体、联系、属性) 了吗?还是你对这一块的某个概念想深入聊聊?
好的!我们正式进入 E-R 模型(实体-联系模型)的具体画法和概念。
从 P7 到 P11,核心只讲了一件事:如何把现实世界里的“东西”抽象成 E-R 图里的基本砖块——实体集。
我们可以把这部分拆解为 3 个关键知识点来看:
1. 实体 (Entity) vs. 实体集 (Entity Set)
很多初学者容易把这两个词搞混,我们用大白话来区分:
- 实体 (Entity):现实世界中客观存在、且可以相互区分的具体对象。
- 举例:清华大学、你手里的那台 iPhone 15、老师爱因斯坦 (Einstein)。实体对应到数据库里,其实就是表里面的一行具体的数据 (Row/Record)。
- 实体集 (Entity Set):具有相同属性的所有实体的集合。
- 举例:所有大学的集合、所有手机的集合、所有老师的集合 (
instructor)。实体集对应到数据库里,其实就是一张表 (Table)。
- 举例:所有大学的集合、所有手机的集合、所有老师的集合 (
👉 在 P10 的 PPT 中给出了直观的例子: 左边是一个名为 instructor(讲师)的实体集,里面装着 (76766, Crick)、(22222, Einstein) 等一个个具体的“实体”;右边是一个 student(学生)实体集,装着不同的学生实体。
2. 属性 (Attributes) 与 主键 (Primary Key)
- 属性 (Attributes):我们怎么去描述一个实体呢?靠的就是属性。
- 举例:老师 (
instructor) 有教师编号 (ID)、姓名 (name)、工资 (salary)。这些就是属性。这对应数据库表里的列名 (Columns/Fields)。
- 举例:老师 (
- 主键 (Primary Key):在众多属性中,必须挑选出一个(或几个)属性的组合,它能够独一无二地标识出这个实体集里的每一个实体,绝对不能重复。
- 举例:同名同姓的老师有很多,所以
name不能当主键;但ID(工号)是唯一的,所以ID就是主键。
- 举例:同名同姓的老师有很多,所以
3. 最关键:如何在 E-R 图中画出来? (P11)
这是考试和画图最基础的规范,一定要牢记这些图形语法:
- 实体集用什么形状? —— 矩形 (Rectangles)。
- 矩形最上方(通常有一个分隔线)写上实体集的名字,比如
instructor。
- 矩形最上方(通常有一个分隔线)写上实体集的名字,比如
- 属性写在哪?
- 直接把属性列在矩形框的里面(下方区域)。
- 主键怎么标记?
- 在主键属性的下方画一条下划线 (Underline)。
👉 P11 经典图例剖析: 你可以看到幻灯片上画了两个长方形:
- 左边的长方形:顶上写着
instructor。内部写着ID,name,salary。其中ID的下方有一条下划线,说明它是讲师的主键。 - 右边的长方形:顶上写着
student。内部写着ID,name,tot_cred(总学分)。同样,ID下方有下划线,说明它是学生的主键。
小结: 这一块非常简单,记住这个对应关系就好: 实体集 = 表,实体 = 表里的一行,属性 = 表头的一列,画图用矩形,主键加下划线。
接下来,现实世界可不止有一座座孤立的“孤岛”(实体),学生要选课、老师要带学生,这就引入了 E-R 图的灵魂——P12-P19 的“联系与联系集” (Relationships)。 准备好往下走了吗?
太棒了!现在我们有了“实体集”(矩形)这些孤立的“岛屿”,接下来就要在岛屿之间修桥了。
从 P12 到 P19,核心讲的是:事物之间是如何发生关联的,以及如何在图上把它们画出来。
我们可以把这部分拆解为 5 个核心知识点:
1. 联系 (Relationship) vs. 联系集 (Relationship Set) (P12-P13)
就像实体和实体集的关系一样,联系也有个体和集合之分:
- 联系 (Relationship):指几个具体实体之间的关联。
- 大白话例子:学生 Peltier 认了老师 Einstein 做导师,这一次具体的绑定,就是一个“联系”。
- 联系集 (Relationship Set):同类联系的数学集合。
- 大白话例子:全校所有老师和学生之间构成的这种“指导与被指导”的网络,总称为
advisor(导师)联系集。 - 数学定义:它是实体集相互之间“笛卡尔积”的一个子集。简单说,就是把老师表和学生表拼在一起,挑出那些真正有师生关系的组合。
- 大白话例子:全校所有老师和学生之间构成的这种“指导与被指导”的网络,总称为
2. 在 E-R 图中画出联系集 (P14)
- 图形语法:用 菱形 (Diamonds) 代表联系集,并用实线把相关的实体集(矩形)连起来。
- P14 图例:左边是
instructor(老师),右边是student(学生),中间画一个菱形写着advisor。一条线连左边,一条线连右边。这就完美表达了“老师和学生之间存在导师关系”。
3. 联系集也可以有自己的属性!(P15-P16)
这是一个极其重要且容易被忽略的考点。
- 为什么联系会有属性? 假设学校想记录“学生是哪年哪月拜这位老师为师的”。这个“拜师日期 (date)”算谁的属性? 算老师的?不对,老师有好几个学生,日期各不相同。 算学生的?也不太对,万一学生换过导师呢? 实际上,这个“日期”只属于“拜师”这件事情本身! 所以它是“联系集”的属性。
- 图形语法 (P16):怎么画呢? 从中间那个代表联系集的菱形(
advisor)拉出一条虚线 (Dashed line),连到一个写着属性date的小方框上。
4. 角色 (Roles) 与“自环”联系 (P17)
通常,联系是发生在两个不同的实体集之间(如老师和学生)。但有时候,实体集会跟自己发生联系!
- 经典场景:大学里的课程 (
course) 经常有“先修课 (prereq)”要求。比如你想学《数据库》,得先学《数据结构》。这两门课都属于course实体集。 - 如何画图:在图上,菱形
prereq的左右两边都连向了同一个矩形course。 - 引入“角色 (Role)”:既然左右连的都是同一个矩形,怎么分清谁是基础课,谁是进阶课呢?我们就需要在连线上写上标签 (Labels),比如一条线上写
course_id(目标课),另一条线上写prereq_id(先修课)。这个标签就叫作角色 (Role)。
5. 联系的“度数”:二元与多元联系 (Degree of a Relationship Set, P18-P19)
联系牵涉到的实体集数量,叫作联系的度 (Degree)。
- 二元联系 (Binary relationship):绝大多数联系都是二元联系,也就是只牵扯2个实体集(比如老师和学生)。
- 非二元 / 三元联系 (Ternary relationship):有时候一个事件必须三方同时参与才说得清。
- P18-P19 经典例子:科研项目 (
project)。一个学生 (student)、在某位导师 (instructor) 的指导下、参与了某个具体的项目 (project)。 - 缺了谁这个逻辑都不完整,这就叫三元联系。
- 图形语法:在 E-R 图上,中间画一个名叫
proj_guide(项目指导)的菱形,然后向三个不同的方向引出实线,分别连向instructor、student和project这三个矩形。
- P18-P19 经典例子:科研项目 (
小结: 到这里,你已经掌握了建大楼的两大核心组件:矩形(实体集) 和 菱形(联系集),还学会了给它们加属性,甚至学会了三方联动(三元联系)和自己连自己(角色)。
接下来,现实世界的规则会给这些连线加上严格的限制,比如“一个学生只能有一个导师,但一个导师可以带多个学生”。这就是 P20 - P32 的“复杂属性与映射基数” 要解决的问题。
你看我们是继续往下进入这部分限制规则,还是说这一块你有什么疑问?
好的!我们之前讨论的属性(比如 ID、salary)都非常“单纯”,一个属性对应一个具体的值。但在现实业务中,很多属性要复杂得多。
从 P20 到 P22,核心只讲了一个问题:当属性不再“单纯”时,我们该如何分类,又该怎么在 E-R 图上画出来?
这一块可以说是考试中最喜欢考细节的地方。我们可以把复杂属性拆分为 4 大关键类型和特殊的图形画法:
1. 属性的四大细分类别 (P20)
- ① 简单属性 (Simple) vs. 复合属性 (Composite)
- 简单属性:不可再分的最基本单位,比如学生的学号
ID。 - 复合属性:可以继续往下“拆分”成子属性,就像俄罗斯套娃一样。
- 经典例子 (P21的树状图):
name(名字)可以拆成first_name(名)、middle_initial(中间名首字母)和last_name(姓)。address(地址)甚至能拆成省、市、街道,而“街道”还能继续拆成门牌号、街名、房间号。
- 经典例子 (P21的树状图):
- 简单属性:不可再分的最基本单位,比如学生的学号
- ② 单值属性 (Single-valued) vs. 多值属性 (Multivalued)
- 单值属性:一个实体在这个属性上只能有一个值,比如一个人的“出生日期”。
- 多值属性:一个实体在这个属性上可以有多个值。
- 经典例子:
phone_numbers(电话号码)。一个人可以有一个手机号、一个座机号、一个工作号码。
- 经典例子:
- ③ 派生属性 (Derived Attributes)
- 定义:这种属性不需要在数据库里真实存下来,而是可以通过别的属性“算”出来。
- 经典例子:
age(年龄)。你绝不能在数据库里存一个人的具体年龄(否则明年还得更新),你应该存date_of_birth(出生日期),然后用当前时间减去出生日期,动态算出年龄!这里的“年龄”就是派生属性。
- ④ 域 (Domain)
- 也就是我们常说的“数据类型”或“取值范围”。比如规定“性别”只能取“男、女”,这个集合就是该属性的域。
2. 怎么在 E-R 图中画出这些复杂属性? (P22 重点必考)
在最新版的 E-R 图画法中(直接写在矩形框内部),复杂属性有极其严格的符号规范:
看 P22 那个叫做 instructor(讲师)的矩形框,里面充满了细节:
- 复合属性怎么画? 👉 用缩进 (Indentation)
- 你看
name写在外面,下一行空几格缩进写了first_name、middle_initial、last_name。这表示这三项隶属于name。 - 再看
address,缩进里有street,而street下面再一次缩进了street_number等。这种层级关系一目了然。
- 你看
- 多值属性怎么画? 👉 加大括号
{ }- 你看电话号码是怎么写的:
{ phone_number }。只要属性外面包着大括号,就代表这个老师可能有好几个电话号码!
- 你看电话号码是怎么写的:
- 派生属性怎么画? 👉 加小括号
( )- 你看年龄是怎么写的:
age ( )。这就告诉写程序的程序员:建表的时候千万别建age这一列,用到的时候拿上面的date_of_birth算一下就行了。
- 你看年龄是怎么写的:
小结: 这三页非常直观!记住核心密码:复合看缩进,多值大括号 {},派生小括号 ()。
到这里,关于实体和内部属性的花样我们已经全部见识过了。 接下来,我们将进入 E-R 图中最考验逻辑能力的一关:P23 - P32:映射基数与约束 (Mapping Cardinality Constraints)。我们要去定义“一个老师到底能带几个学生”。
准备好开启这部分了吗?
现实世界中,事物之间的关系往往伴随着严格的“规矩”。比如:“一个学生只能有一个导师”、“一门课可以有多个学生选”。
从 P23 到 P32,核心就是教我们:如何在 E-R 图上把这些现实世界的“规矩(约束条件)”画出来。
这部分主要包含三大“规矩”:映射基数、参与度约束、以及更高级的 Min..Max 表示法。我们一点点拆解:
规矩一:映射基数 (Mapping Cardinalities, P23 - P29)
这是用来描述两个实体集(二元联系)之间,数量上的对应关系。总共有 4 种情况:一对一 (1:1)、一对多 (1:N)、多对一 (N:1)、多对多 (N:M)。
👉 【图形核心密码】:在 E-R 图中,怎么表示 1 和 N?
- 箭头 ($\rightarrow$) = “1” (最多一个)
- 直线 (—) = “多 / N” (可以有多个)
我们拿 instructor (老师) 和 student (学生) 之间的 advisor (导师) 联系来举例:
- 一对一 (1:1) (P26)
- 规则:一个学生最多认一个老师,一个老师最多带一个学生。
- 画法:菱形两边都是箭头。
instructor $\leftarrow$ advisor $\rightarrow$ student
- 一对多 (1:N) (P27)
- 规则:一个老师可以带多个学生,但一个学生最多只能有一个老师。
- 画法:指向老师的是箭头(体现学生只能找1个老师),指向学生的是直线(体现老师能带N个学生)。
instructor $\leftarrow$ advisor — student
- 多对一 (N:1) (P28)
- 规则:反过来,一个老师最多带一个学生,但一个学生可以认多个老师(罕见情况)。
- 画法:指向老师的是直线,指向学生的是箭头。
instructor — advisor $\rightarrow$ student
- 多对多 (N:M) (P29)
- 规则:一个老师可以带多个学生,一个学生也可以认多个老师。
- 画法:菱形两边全是直线,没有箭头!
instructor — advisor — student
规矩二:参与度约束 (Participation, P30)
箭头解决了“最多有几个”的问题,那“能不能没有”呢?这就是参与度约束。
- 全部参与 (Total Participation):规定实体集里的每一个实体,都必须参与到联系中。
- 画法:用 双直线 (Double line) 连接。
- 例子:学校规定,每一个学生都必须有一个导师!那么在 E-R 图上,
student和advisor之间就要画双线。
- 部分参与 (Partial Participation):实体可以参与,也可以不参与(默认情况)。
- 画法:用 单直线 (Single line) 连接。
- 例子:有些老师只搞科研不带学生。所以
instructor和advisor之间画单线即可。
规矩三:终极必杀技 - 数字区间表示法 (l..h, P31)
有时候,画箭头和双线还是不够精确。比如学校规定:“一个老师最少带1个学生,最多带15个学生”,箭头和直线怎么画都表达不出来。 于是引入了更高级的写法:在连线上直接写数字限制 最小值..最大值 (l..h)。
1..1:代表最小是1,最大也是1。意思就是“必须有且只能有1个”(这同时包含了“箭头”和“双线”的意思!)。0..*:代表最小是0,最大是不设限(用星号*表示)。意思就是“可以没有,也可以有无数个”。
👉 看 P31 的完美图例:
- 老师那边写着
0..*,学生那边写着1..1。 - 解读:一个老师可以带 0 到无数个学生;但每一个学生,必须有且只能有 1 个老师当导师。
补充:三元联系的特殊规矩 (P32)
在极其少见的情况下,我们会用到三个实体连在一起的联系(比如前面的“老师-学生-项目”)。
- 死规定:从一个三元联系(或更多元)的菱形出去,最多只能画一个箭头!
- 为什么?:因为如果有两个箭头,会产生严重的逻辑歧义(计算机科学家们为了避免大家吵架,直接在理论上把它给禁了,不允许出现多箭头)。
小结: 这一块是 E-R 图的逻辑灵魂。你只需记住这个口诀:“单线可以没有,双线必须参与;直线代表多,箭头代表一;想说得更准,就写 min..max。”
到此为止,E-R 图的画法其实已经全教完了。但是!你画完的图最终是要进数据库的,我们要怎么把一张图形变成代码里的“表 (Table)”呢?又怎么选主键呢? 这就是接下来 P33 - P51:主键选择与向关系模式的转换 (Reduction to Relational Schemas) 要干的事情。
准备好跨越从“图”到“表”的鸿沟了吗?
太棒了!我们在画图时已经知道,主键要在属性下面画一条“下划线”。但是在把 E-R 图转化为真正的数据库表时,如何科学地挑选这个主键? 这可是决定数据库会不会乱套的头等大事。
从 P33 到 P36,专门解决“挑主键”的问题。我们分两部分来看:实体集的主键 比较简单,联系集的主键 则是这部分的重头戏(考试必考)。
1. 实体集的主键选什么? (P34)
这个很好理解。实体集里的每一个实体(也就是表里的每一行)必须是独一无二的。
- 原则:我们必须在实体集里找出一个(或几个)属性的组合,保证在整个集合里,绝对没有两个实体在这个/些属性上取值完全一样。
- 举例:对于学生(
student),不能用名字当主键,因为会同名;必须用学号(ID)当主键,这就成了他在数据库里的“唯一身份证”。
2. 联系集的主键选什么? (P35 - P36 核心重点)
实体有自己的身份证,那实体和实体之间发生的“联系”(比如:拜师记录),它的身份证(主键)该怎么算呢?
👉 基本盘(超键 Superkey): 联系集的主键,一定是从它两端连接的实体的主键拼凑而来的。 比如,老师(instructor)的主键是 i_ID,学生(student)的主键是 s_ID。那么这两者建立的 advisor(导师联系),它的基本盘(超键)就是把这两者拼起来:{i_ID, s_ID}。 (注:联系集哪怕自己有属性,比如“拜师日期 date”,这个自有属性也绝对不能用来当主键。)
👉 终极规则(主键 Primary Key 的最终选择): 虽然基本盘是拼起来的,但我们真的需要把两个 ID 都作为主键吗?不一定!这完全取决于我们上一节学的“映射基数(箭头还是直线)”!
这里有 4 种情况,我们用大白话来推演其中的逻辑(P36,务必理解):
- ① 多对多 (N:M) —— 两边都是直线
- 规则:取双方主键的联合。
- 逻辑:一个老师带多个学生,一个学生认多个老师。如果你只拿
s_ID去查,会查出好几条记录(因为他有多个老师);只拿i_ID去查,也会查出好几条记录。所以,必须同时拿出{i_ID, s_ID},才能唯一定位到“某老师指导某学生”的这一条具体记录。 - 主键:
{i_ID, s_ID}
- ② 一对多 (1:N) / 多对一 (N:1) —— 一边箭头,一边直线
- 规则:谁是“多 (N)”的那一端(直线那一端),就拿谁的主键!
- 逻辑:假设 1个老师带N个学生,但 1个学生只能有1个老师。既然一个学生这辈子只能认1个老师,那么在这张
advisor联系表里,这个学生的s_ID永远只会现一次!既然它只出现一次,那它不就是天生的“唯一标识”吗?不需要加上老师的ID。 - 主键:
{s_ID}(也就是“多”的那一端的主键)。
- ③ 一对一 (1:1) —— 两边都是箭头
- 规则:随便挑一边的主键即可。
- 逻辑:1个老师只带1个专属学生,1个学生也只有1个专属老师。无论你拿老师的
i_ID,还是学生的s_ID去查,在联系表里都绝对只会出现一次。 - 主键:选
{i_ID}可以,选{s_ID}也可以。
小结与速记口诀:
关于联系集选主键,你只要记住一句话: “多对多拼一起,一对多选多的那一端,一对一随便选。” (原因就在于:“多”的那一端在业务逻辑上被“箭头”限制死了,它在联系关系里只会发生一次行为,所以它的 ID 就是唯一的。)
好了!到现在为止,我们解决的都是“身体健康、自给自足”的强实体集。 但在现实中,有些实体“离了别人就活不下去”,连主键都凑不齐,这就是我们接下来要看的极其特殊的存在: 👉 P37 - P40:弱实体集 (Weak Entity Sets)。
准备好看看这种“寄生虫”一样的数据结构是怎么在 E-R 图里生存的吗?
太棒了!前面我们遇到的实体(比如“学生”、“老师”)都是“经济独立、自给自足”的,它们靠自己的属性(比如 ID)就能在数据库里唯一证明自己的存在。这种叫做强实体集 (Strong Entity Set)。
但是,现实世界中有一种实体,它就像“寄生虫”或者“附属品”,如果脱离了某个“宿主”,它就失去了存在的意义,甚至连它自己是谁都分不清了。 这就是我们 P37 到 P40 要讲的核心:弱实体集 (Weak Entity Sets)。
我们把这4页的核心逻辑拆解为 3 个关键点来看:
1. 什么是弱实体?为什么需要它?(P37 - P38)
我们直接用 PPT 上的经典例子:课程 (course) 和 开课班次 (section)。
- 一门课(比如:CS-101 数据库原理)是一个强实体。
- 这门课在 2026年秋季开的“1班”、“2班”,就是班次 (
section)。
我们遇到了什么尴尬的问题? 如果我们在“班次”这个实体里,只存 sec_id(班次号,比如 1班)、semester(学期)、year(年份)。 你拿着这三个属性去数据库里查:“给我找 2026年秋季的1班!” 数据库会崩溃的,因为所有课程都有 2026年秋季的 1 班!
那有人说,把“课程号 (course_id)”也存进“班次”里当属性不就行了吗? 如果这样干,我们在画 E-R 图时,班次和课程之间的“联系”就显得多余了(因为你把联系的信息直接写死在实体里了),这在概念设计阶段是不优雅、冗余的。
完美的解决方案:弱实体诞生! 我们干脆承认“班次”是一个弱实体。它自己没有能力证明自己是谁,它必须依附于“课程”这个强实体而存在。(没有这门课,哪来的这门课的班次?)
2. 弱实体的“生存法则”与专业术语 (P38 - P39)
既然是依附于别人生存的,数据库科学给它定了一套严格的黑话(术语):
- 存在依赖 (Existence Dependent):弱实体的存在必须依赖于另一个实体。
- 标识实体 (Identifying Entity / Owner):那个被依附的“宿主”强实体(即
course)。它“拥有 (own)”这个弱实体。 - 标识联系 (Identifying Relationship):弱实体和宿主之间连接的那个纽带(菱形)。
- 分辨符 / 部分键 (Discriminator / Partial Key):弱实体自己用来区分同类的属性。
- 举例:对于“数据库原理”这门课来说,只要有
sec_id(1班、2班)、年份、学期,就能把这些班次分开了。所以{sec_id, year, semester}就是分辨符。
- 举例:对于“数据库原理”这门课来说,只要有
👉 弱实体的主键到底是什么? 弱实体的主键 = 宿主的主键 + 自己的分辨符。 (比如刚才那个班次的完整主键就是:course_id + sec_id + semester + year。借了宿主的身份证,加上自己的小名,终于拼成了一个合法的唯一身份。)
3. 在 E-R 图上怎么画?(P40 重点必考)
既然地位特殊,它在图上的待遇(画法)也和别人完全不一样,充满了“双黄蛋”:
看 P40 的那张图:
- 弱实体集怎么画? 👉 双线矩形 (Double rectangle)。
- 图中的
section外面套了两层矩形框,代表它是弱实体。
- 图中的
- 标识联系怎么画? 👉 双线菱形 (Double diamond)。
- 图中的
sec_course菱形也是两层边框。
- 图中的
- 分辨符(部分键)怎么画? 👉 虚下划线 (Dashed line)。
- 我们之前学过,强实体的主键是实线下划线。而弱实体因为自己那几个属性撑不起主键,只能算“半个主键”,所以它的
sec_id,semester,year下面画的是虚线的下划线!
- 我们之前学过,强实体的主键是实线下划线。而弱实体因为自己那几个属性撑不起主键,只能算“半个主键”,所以它的
另外,图上还有一个细节:弱实体指向菱形的那条线有一个箭头。这是因为一个弱实体必然对应唯一的一个宿主实体(这是典型的 N:1 或 1:1 关系中的“1”的一端)。
小结:
弱实体集其实很好理解,只要记住生活中的例子: 大楼 (building) 与 房间 (room)。 “101房间”是个弱实体(因为每个楼都有101),它的宿主是“大楼”。分辨符是“101”。它的绝对主键是“大楼编号 + 101”。画图时全用双实线框,自己的属性下面画虚线下划线。
到这里,E-R 模型里能用的“基本砖块”和“游戏规则”我们就全部学完了! 紧接着的 P41-P42 是对前面内容的总结(包含一个庞大的大学数据库综合版 E-R 图)。 在那之后,我们将迎来第6章最核心、最具有实操性的硬核技术 (P43 - P51):如何把画好的 E-R 图,翻译成真实的数据库表代码 (Reduction to Relational Schemas)?
准备好进入“画图变代码”的实战转换阶段了吗?
太棒了!这部分是我们在进入“写代码建表”之前的最后一次图纸审查。
P41 讲的是画图时极其容易犯的一个低级错误(冗余),而 P42 则是我们迄今为止学过的所有知识的大阅兵(全景综合案例)。
我们分两步来把它拿下:
1. 最后的避坑指南:冗余属性 (Redundant Attributes, P41)
在画 E-R 图的时候,新手特别容易犯一个“画蛇添足”的错误。
👉 看 P41 的反面教材:
- 我们有一个学生实体
student(包含属性:学号、姓名、总学分、所在系名 dept_name)。 - 我们有一个系别实体
department(包含属性:系名 dept_name、建筑名、预算)。 - 中间我们还画了一个菱形联系
stud_dept,把学生和系别连在了一起。
发现问题了吗? 既然你已经用中间的菱形(联系集)明确表示了“某学生属于某系”这个关系,那么在学生自己的属性框里,就绝不应该再写一次 dept_name(系名)!
- 为什么错?:在 E-R 模型(概念设计)中,事物之间的关联只能由“菱形(联系)”来承担。如果你把关联信息写成了实体自带的属性,这就叫概念上的冗余 (Redundant)。
- 怎么改?:如图所示,直接把
student框里的dept_name划掉(删除)。
📢 一个非常重要的伏笔 (BUT…): PPT 下方有一句话非常有意思:“当你把这张 E-R 图最终转换回数据库表 (Tables) 的时候,这个被删掉的属性可能又会加回来”。 这是因为概念图和物理表有区别。画图时追求绝对纯粹,不能冗余;但在建表时,为了体现“一对多”的关系,我们确实会把外键(系名)塞进学生表里(下一节马上就会学到)。但在当前的画图阶段,必须删掉!
2. 终极全景图:大学数据库 E-R 图 (P42)
这是第6章的“期中考试”。它把你前面学的所有零碎组件拼成了一张宏大的架构图。我们不需要死记硬背它,而是要用前面学的“X光眼”来扫视这张图,看看你能认出多少彩蛋:
👀 来玩个“找茬”游戏,你能在这张大图上找到以下 4 个高级特性吗?
- 找“弱实体集”与“虚线下划线”
- 看正下方:
section(开课班次)是双线矩形,它是弱实体。它连着双线菱形sec_course,指向了它的宿主course(课程)。 - 细节:
section里面的sec_id、semester、year下面画的全是虚线(它们是分辨符)!
- 看正下方:
- 找“全部参与”的强制规定
- 还是看
section,它连向宿主course的不仅是双线菱形,而且是双直线。这说明:任何一个班次,必须 (Total participation) 隶属于某一门课程。
- 还是看
- 找“自环”和“角色 (Role)”
- 看左下角:
course(课程)实体引出两条线,连向了同一个菱形prereq(先修课要求)。 - 连线上写着标签:
course_id(目标课)和prereq_id(先修课)。这就是我们说的角色。
- 看左下角:
- 找“联系集上的专属属性”
- 看中右方:学生 (
student) 选修 (takes) 了某个班次 (section)。选课这个动作(菱形takes)引出了一条虚线,连着一个属性叫grade(成绩)。 - 成绩既不属于学生(他选别的课成绩不同),也不属于班次(这个班别的学生成绩不同),它只属于“某学生选某班”的这个联系本身。
- 看中右方:学生 (
小结:
只要你能看懂 P42 这张大图上的双线、单线、箭头、虚线、角色名,恭喜你,你的数据库概念设计能力已经完全过关了!你已经具备了给一家大企业画业务蓝图的能力。
但是,计算机是看不懂“图画”的,它只认识表 (Tables)。
接下来,我们即将迎来第6章最具实操性、也是极其容易考大题的硬核板块: 👉 P43 - P51:从 E-R 图向关系模式(表)的转换 (Reduction to Relational Schemas)。
我们要把图上的矩形和菱形,一套公式变成 CREATE TABLE 能用的结构。准备好见证魔法了吗?
太棒了!现在我们迎来了第6章最激动人心、也是考试中最爱考大题的环节:“图纸落地”。
前文我们画了一堆矩形和菱形,但 MySQL 等数据库系统是个“脸盲”,它只认识表 (Tables / Relational Schemas)。 从 P43 到 P51,核心讲的就是一套“翻译规则”:如何把图形无损、甚至优化地翻译成真正的数据库表?
我们把这套翻译规则提炼为 4 大核心法则:
法则一:实体集怎么变表?及复杂属性的“降维打击” (P45 - P47)
1. 基础转换 (P45)
- 强实体集直接变身!矩形的名字就是表名,矩形里的属性就是表里的列 (Columns),主键依然是主键。
- 例如:
student(ID, name, tot_cred)。
- 例如:
2. 复合属性怎么建列?(P46)
- 数据库表的列必须是“扁平”的(这叫第一范式,我们第7章会提)。所以,像名字(名、中间名、姓)这种嵌套属性,必须展开 (Flattened out)。
- 做法:直接用子属性建列。比如变成
first_name,middle_initial,last_name。如果怕重名,可以加个前缀name_first_name。
- 做法:直接用子属性建列。比如变成
3. 多值属性怎么存?(P47 🚨 必考重点)
- 如果你照着原样建表,比如一个老师有 3 个电话,你怎么存?把 3 个电话逗号分隔塞进一个格子里吗?绝对不行!这违反了数据库最基本的原子性。
- 做法:单独为它建一张新表!
- 新表包含:该实体的主键 + 这个多值属性。
- 例如:老师的电话
{phone_number},我们要建一张新表inst_phone(ID, phone_number)。 - 如果爱因斯坦 (ID: 22222) 有两个电话,这张新表里就会存两行记录:
(22222, 111-1111)和(22222, 222-2222)。
法则二:弱实体集怎么变表?(P45)
弱实体自己是没有完整主键的,建表时如果只放自己的属性,数据就全乱套了。
- 做法:把它宿主(强实体)的主键拉过来,当成自己的列!
- 例如:开课班次
section,本来只有sec_id, semester, year。建表时,强行把课程的course_id加进来。 - 最终建表:
section (course_id, sec_id, semester, year)。这 4 个列共同组成它的主键。
法则三:菱形(联系集)怎么变表?(P48)
实体变成了表,那实体之间的联系怎么办?
- 做法:把两端实体的主键拿过来拼在一起,加上联系本身的属性,建一张新表!
- 例如:多对多的
advisor(导师)联系。左边是老师的主键i_ID,右边是学生的主键s_ID。 - 最终建表:
advisor(s_ID, i_ID)。表里的每一行,就代表一对真实存在的师生关系。如果有专属属性(比如拜师日期date),也加进这张表里。
法则四:神级优化!删掉多余的表 (P49 - P51 🚨 超级重点)
如果按照上面的规则“死翻”,图上有几个矩形和菱形,最后就会生成几张表。但这样会导致表太多,查询时极慢。数据库学家发明了“表的合并与消除”术。
1. 一对多 / 多对一 的合并 (P49 - P50)
- 问题:学生(多)属于系(一)。如果按照法则三,我们会生成一张
stud_dept表。查一个学生在哪个系,还得跨表查询(Join),太慢了! - 优化方案:取消这张关系表!直接把“一”那一端的主键,当作外键塞进“多”那一端的表里。
- 做法:因为一个学生只属于一个系,我们直接把系名 (
dept_name) 作为一列,加到student表里就行了!
- 做法:因为一个学生只属于一个系,我们直接把系名 (
- (还记得 P41 我们说在画 E-R 图时,学生框里绝对不能写
dept_name吗?现在到了建表阶段,为了性能优化,我们又堂而皇之地把它加进去了!这就是概念设计与逻辑设计的区别。) - 注意:如果是“部分参与”(比如有学生暂时没分系),那么加进去的这列会产生
Null(空值)。但在性能面前,这点牺牲是可以接受的。
2. 弱实体关联表的消除 (P51)
- 问题:弱实体
section和 宿主course之间有个双线菱形叫sec_course。按理说它要建一张表对吧? - 分析:如果给
sec_course建表,里面放什么?放左边的主键course_id,和右边的主键(course_id, sec_id, semester, year)。 - 发现:全是重复的废话!弱实体的表里早就包含了宿主的主键了。
- 优化方案:所有连接弱实体的菱形,一律不建表!直接抛弃!
总结:“图变表”的实战心法
当你拿到一张 E-R 图让你写表结构时,只需默念:
- 强实体直接建表。
- 多值属性单独建表(主键+多值)。
- 弱实体带着宿主的主键一起建表,连接弱实体的菱形直接无视。
- 多对多 (N:M) 的菱形必须建新表。
- 一对多 (1:N) 的菱形不建表,把“一”的主键塞给“多”当外键。
到这里,传统数据库设计的“看家本领”你已经全部掌握了! 但是,有些极其复杂的业务,比如“员工分为普通员工、经理、临时工,他们既有共性又有个性”,传统的 E-R 图就不够用了。
这就是接下来 P52 - P63:扩展 E-R 特性 (Extended E-R Features) 要讲的内容,我们将引入面向对象的思想:继承(特化/泛化)与嵌套(聚集)。
要一鼓作气把这章的核心难点打通关吗?
太棒了!传统的 E-R 模型就像是搭积木,但当业务系统变得庞大且复杂时,这种“扁平化”的模型就显得力不从心了。
比如:员工分为“教师”和“秘书”,他们都有姓名和工资(共性),但教师有职称,秘书有打字速度(个性)。传统 E-R 图没法优雅地表达这种父子层级关系。再比如,如果一个“联系”本身又要和别人发生“联系”呢?
这就是 P52 - P63:扩展 E-R 特性 要解决的两大高级难题,也就是引入了面向对象编程 (OOP) 中的继承和嵌套思想。
我们分两个核心模块来攻克它:
高级特性一:特化与泛化 (继承机制, P53 - P59)
这部分其实就是在数据库设计里实现 “Is-a (是一个)” 的父子类继承关系。
1. 基本概念:自顶向下 vs. 自底向上 (P53, P57)
- 特化 (Specialization):自顶向下。先定义一个大类(如“员工”),然后发现员工里有差异,细分出子类(如“教师”、“秘书”)。
- 泛化 (Generalization):自底向上。先发现了几个实体(如“本科生”、“研究生”),发现他们都有学号和姓名,于是提取出共性,抽象出一个大类(如“学生”)。
- 结论:不管怎么来的,它们在 E-R 图上的表现形式和处理逻辑是完全一样的,统称为继承。子类会无条件继承 (Attribute inheritance) 父类的所有属性和联系。
2. 怎么画图与两大约束条件 (P54, P58-P59)
- 图形语法:用一个代表 ISA (Is A) 的空心三角形,把父类和子类连起来。(见 P54,
instructor是一个employee,employee又是person)。 - 约束1:相交约束 (Overlapping vs. Disjoint)
- 不相交 (Disjoint):一个实体最多只能属于一个子类。比如你不能既是教师又是秘书。
- 重叠 (Overlapping):一个实体可以同时属于多个子类。比如一个
person可以既是employee又是student(学校里半工半读的助教)。
- 约束2:完整性约束 (Total vs. Partial)
- 全部 (Total):父类里的每一个实体,必须属于至少一个子类。(画图时加
total关键字和虚线)。比如:学生必须是本科生或研究生之一,不能是个黑户。 - 部分 (Partial):默认情况。父类实体可以不属于任何子类。(比如员工除了教师和秘书,还有门卫,但门卫没画出来)。
- 全部 (Total):父类里的每一个实体,必须属于至少一个子类。(画图时加
3. “继承”怎么建表?(P55 - P56 🚨 必考方案对比) 数据库表是不支持“继承”的,那怎么把有父子关系的实体集变成表呢?有两个方案:
- 方案 1:拼图法(各建各的表)
- 做法:父类建一张表(存共性属性),子类建一张表(只存父类的主键 + 自己的个性属性)。
- 优缺点:最省空间,没有冗余;但查询时需要用主键把两张表 Join(连接)起来,查询慢。
- 方案 2:独立法(大包大揽)
- 做法:不建父类表(或者只当普通表),给每个子类建一张包含所有属性(继承来的+自己的)的“大表”。
- 优缺点:查询速度极快(查一张表搞定);但如果子类是“重叠”的(比如某人既是学生又是员工),他的姓名、住址就会在两张表里各存一遍,造成数据冗余!
高级特性二:聚集 (Aggregation, 把“联系”当“实体”, P60 - P63)
这是一个非常烧脑但也非常巧妙的设计模式!
1. 遇到的痛点是什么?(P60) 还记得我们前面的那个三元联系吗?一个老师(instructor)带一个学生(student)做项目(project)。这三个实体用一个名叫 proj_guide 的菱形连在了一起。 新需求来了:学校要求给这组“指导关系”写一个评价(evaluation)。
- 谁评价?是评价老师吗?不是。评价学生?不是。评价项目?也不是。
- 评价的是“老师带学生做项目”这整个行为!
- 痛点:在传统的 E-R 图里,联系(菱形)和实体(矩形)才能连线。菱形和菱形之间是绝对不允许连线的!
2. 解决方案:聚集 (Aggregation, P62) 既然连不了,我们就把整个业务打包,伪装成一个大实体!
- 图形语法:在
instructor、student、project以及中间的菱形proj_guide的外面,画一个超级大的虚线框(或实线大方框)。 - 此时,在更高维度的视角下,这个大框就是一个整体的“虚拟实体”。
- 然后,我们就可以心安理得地新建一个菱形
eval_for,一头连着evaluation实体,另一头直接连向那个大框!
3. 聚集怎么变数据库表?(P63)
- 原理依然遵循“拼主键”法则:
- 大框里的核心主键是谁?是那个三元联系
proj_guide的主键(即s_ID, project_id, i_ID拼起来)。 - 评价实体的主键是
evaluation_id。
- 大框里的核心主键是谁?是那个三元联系
- 建表结果:新联系集
eval_for会变成一张表,包含四个外键:eval_for (s_ID, project_id, i_ID, evaluation_id)。完美解决了多重绑定的问题。
小结:
- 特化/泛化 (ISA) 解决了分类继承问题(建表时要注意防 Join 过多 或 防数据冗余)。
- 聚集 (大方框) 解决了“给关系建立关系”的套娃问题(把局部打包成一个整体当实体用)。
至此,E-R 模型的所有“兵器”——基础刀剑(实体/联系)、进阶暗器(弱实体/映射基数)、满级大招(继承/聚集)——你已经全套学完了!
第6章还剩下最后的尾声 P64 - P83:设计避坑指南 (Common Mistakes) 与 UML 类图对比。看完这部分,我们就能彻底告别画图,进入第7章的“数学推导与范式算法”阶段了。准备好收尾第6章了吗?
太棒了!我们现在进入了第6章的最后也是最考验“内功”的一个环节。
从 P64 到 P71,讨论的是数据库设计的哲学(Design Issues)。这就好比写作文,你已经掌握了所有的字词句法(矩形、菱形),但怎么把它们组合成一篇好文章,还是有很多讲究的。
这部分列举了初学者最容易犯的错误,以及在面对两难选择时的“设计准则”。我们把它总结为 4个灵魂拷问:
灵魂拷问 1:到底该用“属性”,还是该升级为“实体”?(P67)
这是一个经常让人纠结的问题。
- 纠结场景:老师(
instructor)有电话号码(phone)。我们是应该把phone当作老师框里的一个多值属性{phone_number},还是应该专门为“电话”画一个矩形实体? - 评判准则:取决于你想存多少关于“电话”的附加信息!
- 如果只存一个号码字符串(如
138xxxx),作为多值属性即可。 - 但是!如果除了号码,你还想记录这个号码的“位置(是办公室还是家里?)”、“办理的套餐”,那就必须把它升级为实体 (Entity)!
- 如果只存一个号码字符串(如
- 画法变化:一旦升级为实体,就需要画一个
phone矩形(包含号码、位置等属性),然后用一个名为inst_phone的菱形把instructor和phone连起来。
灵魂拷问 2:到底该用“实体”,还是用“联系”?(P68)
- 纠结场景:“学生注册课程”这件事(
registration)。我们是应该画一个叫“注册”的实体框?还是把它当成一个联结学生和班次的菱形(联系集)? - 设计准则:用来描述“实体之间发生的动作 (Action)”,优先使用联系(菱形)!
- “注册”是学生和班次之间发生的交互动作,理所应当用菱形。
- 延伸问题:属性放哪?
- 如果是“指导(
advisor)”动作发生的“日期(date)”,它绝不能放在学生身上(万一换导师了呢?),必须挂在代表动作的联系(菱形)上。
- 如果是“指导(
灵魂拷问 3:你在图里画蛇添足了吗?(常见的冗余错误)(P65-P66)
我们在前面讲弱实体时其实已经提过这个经典的初级错误:
- 错误示范 (P65 a):你画了
student矩形,里面写了属性dept_name(系名)。然后你又画了个菱形stud_dept把它和department矩形连了起来。 - 为什么错:既然你已经画了菱形去连接系别,这就代表了归属关系。你在学生属性里再写一遍
dept_name,这就是概念级的数据冗余。 - 记住:在 E-R 图阶段(概念设计阶段),绝对不要用属性去替代联系的作用。有桥(菱形)走桥,别在自己包里装别人家的土。
灵魂拷问 4:二元联系 vs. 多元联系,怎么选?怎么转?(P69-P71)
我们之前学过三元联系(比如:老师 - 学生 - 项目),但这总是让人看着头大。
1. 能不能把三元联系拆成几个二元联系?(P69)
- 情况 A:最好拆开。比如“父母(
parents)”联系,连接小孩、爸爸、妈妈三方。把它拆成“小孩-爸爸”和“小孩-妈妈”两个二元联系更好!为什么?因为允许部分信息的缺失(比如单亲家庭,只知道妈妈不知道爸爸)。如果绑死在三元联系里,缺失一方这数据就废了。 - 情况 B:绝对不能拆。像之前的
proj_guide(老师带学生做项目)。一旦拆成(老师-学生)和(学生-项目),你就搞不清“这个学生做的这个特定项目,到底是谁指导的”了。这种天然捆绑的逻辑,必须保持三元联系。
2. 实在不行,如何强行把 N 元联系转换为 二元联系?(P70 理论操作) 如果你非要把一个三元联系(比如 A、B、C 连着一个菱形 R)变没,怎么做?
- 做法:引入一个“假人(人造实体集 E)”。
- 在中间创建一个新的矩形实体
E。 - 然后画三个小菱形($R_A, R_B, R_C$),分别把
E和 A、B、C 连起来。 - 原先挂在三元联系上的属性,全部转移给这个“假人实体 E”。
- (这种方法在底层逻辑上很严密,但为了防止找不到 E 的主键,通常要把 E 设为弱实体。)
小结:
P64-P71 这部分其实是前人踩坑无数后总结出的最佳实践 (Best Practices):
- 属性需要细节了,就变身实体。
- 描述交互动作的,用菱形联系。
- 连了菱形,就别在框里写对方的名字(防冗余)。
- 三方绑定能拆就拆(防缺失),拆不了就用假人转换。
到这里,第6章的全部核心内容我们已经拿下了! 剩下的 P72-P83 主要是一个全章的设计决策大总结,以及介绍了两种不同的画图流派(Chen 氏记法和 UML 类图),看看同一套逻辑用不同的符号怎么画。
我们要迅速把这最后一部分收尾,为迎接充满数学挑战的第7章(范式与规范化)做好准备吗?
太棒了!我们现在来到了第6章的收尾部分。
如果把画 E-R 图比作写文章,前面我们学了各种修辞手法,那么 P72 到 P76 就是一本“语法大纲”与“方言词典”。
这几页非常实用,尤其是 P73-P74,就是你期末考试的终极作弊纸(Cheat Sheet)。我们把这部分拆解为 3 个板块来消化:
1. 架构师的“六大灵魂拷问”总结 (P72)
P72 (E-R Design Decisions) 是对我们前面学过的所有纠结点的终极大总结。当你拿到一个现实业务需求时,作为架构师,你必须在脑海里过一遍这6个决定:
- 用属性还是用实体?(电话号码只是存个数字,还是需要记录归属地?)
- 用实体还是用联系?(“注册”是个死板的档案,还是个动态的交互动作?)
- 用二元联系还是三元联系?(能拆成两两关联吗?拆了会丢失信息吗?)
- 是强实体还是弱实体?(它能自己证明自己是谁吗?还是要抱大腿?)
- 要不要用特化/泛化?(业务里有明显的父子层级、分类关系吗?)
- 要不要用聚集?(你需要对一个“联系”再次进行关联和评价吗?)
把这六个问题想清楚,你的数据库大厦地基就稳了。
2. 终极作弊纸:E-R 符号大全 (P73 - P74)
这两页是全书 E-R 符号的汇总,非常适合复习。我们挑最核心的快速过一遍:
- 基础三件套:矩形(实体集)、菱形(联系集)、无向线段(连线)。
- 主键与分辨符:实线下划线(主键 Primary key)、虚线下划线(弱实体的分辨符 Discriminator)。
- 弱实体的标志:双线矩形、双线菱形。
- 基数与参与度约束:
- 箭头 $\rightarrow$ 代表“一”(最多 1 个)。
- 直线 — 代表“多”(不限制)。
- 双线代表“全部参与”(必须有)。
l..h代表最少到最多的精准区间。
- 高级特性:
- 带
ISA的空心三角形代表继承。 - 超大号矩形框包住局部,代表聚集 (Aggregation)。
- 带
3. E-R 图的“方言”:Chen 氏记法与“鸦脚”记法 (P75 - P76 扩展重点)
我们课本里教的这套画法,只是学术界的一种标准。但在真实的历史发展和目前的工业界中,大家用的图可能长得不太一样。这里介绍了两种著名的“方言”:
方言一:Chen 氏记法 (Chen Notation, P75) 这是美籍华人计算机科学家陈品山(Peter Chen,E-R模型的发明者)最初发明的原始记法。
- 最大区别:属性画在外面!
- 课本里,我们把属性(ID、姓名等)写在矩形框的内部。
- 在 Chen 氏记法里,属性是用椭圆形 (Ellipses) 小气泡画出来的,挂在实体的周围,看起来像章鱼的触手。
- 气泡的变体:
- 复合属性:一个气泡连着几个小气泡(像树枝)。
- 多值属性:双线椭圆(比如多个电话号码)。
- 派生属性:虚线椭圆(比如通过生日算出的年龄)。
- 主键:气泡里的文字带下划线。
方言二:IDE1FX 与“鸦脚记法” (Crows feet notation, P76) 这是目前企业界、大厂真实在用的画法!如果你以后用 Navicat、MySQL Workbench 等软件导出数据库模型,你会发现全是这种“鸦脚”图。
- 最大区别:不画菱形,基数画在连线端点上!
- 多对多 (N:M):连线两端都长出了三个分叉,看起来就像乌鸦的脚爪!鸦脚在哪边,哪边就是“多 (Many)”。
- 一对一 (1:1):连线两端都是直勾勾的平头,没有分叉,代表“1”。
- 一对多 (1:N):一端是平头(1),另一端是鸦脚分叉(N)。
- 对比 Chen 记法:Chen 记法会在连线上写上
1和*(星号代表多)。而鸦脚记法纯靠图形端点的形状来区分。
小结:
看懂了 P75-P76,以后你去公司上班,看到老员工扔给你一张连着“乌鸦脚”的表结构图,你就知道那也是 E-R 图的一种变体,鸦脚指着的方向就是“多 (N)”的一端。
至此,第6章只剩下最后一部分了:P77 - P83:UML 类图 (Unified Modeling Language)。 这部分主要是给学过面向对象编程(Java/C++)的同学做的一个概念映射,看看软件工程里的 UML 类图和数据库里的 E-R 图有啥区别。
你想快速把这最后 7 页收个尾,给完整的第 6 章画上句号吗?
太棒了!终于来到了第 6 章的压轴部分。
从 P77 到 P83,课程发生了一个视角的跨越:从数据库专家的视角,切换到了软件工程师 (程序员)的视角。
在软件工程(比如写 Java 或 C++ 代码)中,大家最常用的设计图叫做 UML (统一建模语言)。UML 中有一种图叫 类图 (Class Diagrams),它和数据库里的 E-R 图有着千丝万缕的联系,但又有几处极其“坑人”的区别。
考试中往往会给出 E-R 图,让你找对应的 UML 图,反之亦然。我们把核心区别拆解为 4 个知识点:
1. 基础转化:实体变“类”,属性带“权限” (P78, P81)
- E-R 图的实体集 (Entity) 👉 UML 的类 (Class)。
- 画法区别:
- E-R 图的矩形只写名字,属性放在下方的白板里。
- UML 的类是一个被分成三层的矩形:最上层写类名(如
E),中间层写属性(如A1),最下层写方法/函数(如+M1())。
- 访问权限(面向对象特色):UML 的属性前面会有符号:
-代表 Private(私有)+代表 Public(公开)#代表 Protected(受保护)
2. 联系与属性的转化:虚线框登场 (P78, P80)
- E-R 图的联系 (菱形 R) 👉 UML 的关联 (连线)。
- UML 里不画菱形,就是两个类之间拉一条直线,在线上写个名字
R。
- UML 里不画菱形,就是两个类之间拉一条直线,在线上写个名字
- 联系自带的属性怎么办?(重点)
- E-R 图:从菱形拉一条虚线连接属性。
- UML:从连线上拉一条虚线,连接一个独立的矩形框(关联类 Association Class)。这种画法明确表示,这组属性既不属于左边,也不属于右边,只属于这条连线。
3. 最大考点陷阱:映射基数位置的反转 (P78 🚨)
这是这几页幻灯片中最重要的一句话:“Note reversal of position in cardinality constraint depiction” (注意基数约束表示位置的反转)。
怎么理解这个“反转”?
- E-R 图的逻辑是“从自己出发”:如果左边的实体框旁边写着
0..*,右边写着0..1,意思是“左边实体自身能参与的最少/最多联系次数”。 - UML 的逻辑是“看对方”:UML 类图在线的两端标数字,看的是对面!
- 举例:如果一条线连着
员工和部门。在 UML 中,如果部门那一端的线上写着1..*,意思是:对于当前的这一个员工,他在对面能对应1到多个部门。
- 举例:如果一条线连着
- 考点提示:因为这个相反的逻辑,当你把带区间
min..max的 E-R 图翻译成 UML 图时,数字的位置必须对调(左边移到右边,右边移到左边)!
4. 高级特性的转化对应 (P79, P81)
- 特化/泛化 (继承):
- E-R 图:带
ISA的空心三角形。 - UML:带空心大箭头的实线,直接指向父类。(UML 也会在线旁边直接写上
overlapping或disjoint代表约束规则)。
- E-R 图:带
- 弱实体 (Weak Entity):
- E-R 图:双线大矩形。
- UML:使用面向对象里的组合 (Composition) 概念来表示。会在标识实体(宿主)那一端的连线上,画一个实心的黑色小菱形
◆,代表宿主牢牢地控制(拥有)着另一端的弱实体。
尾声:除了画图,数据库设计还要考虑什么?(P82)
到了 P82,作者进行了一个宏观的升华。虽然我们用了整整一章在研究 Schema(表结构)的设计,但在真实的工程中,除了数据结构,你还需要考虑:
- 功能需求 (Functional Requirements):你的系统到底要跑什么业务?
- 数据流与工作流 (Data Flow, Workflow):数据是怎么产生、流转和消费的?
- 模式演进 (Schema Evolution):未来业务变化了,这套表结构好不好修改(加字段、拆表)?
🎉 第 6 章完结撒花!
至此,第 6 章《基于 E-R 模型的数据库设计》 我们已经一页不落地全部拆解完毕!
总结一下你在这一章学到的“屠龙技”:
- 你学会了将现实世界的混乱业务,抽象为严谨的实体与联系,并绘制出具有高级基数约束和继承、聚集特性的 E-R 架构蓝图。
- 你掌握了一套万能的“图转表”公式,能够把任何图形转化为最优的强弱实体表、多值属性表、外键连接表。
接下来是什么? 人画的图总是会犯错的,会有看不见的冗余和异常潜伏在里面。 第 7 章《关系数据库设计与规范化》,将完全摒弃感性的图形,拿出一套纯粹的数学推导公式(函数依赖、求闭包)和计算机算法(范式分解),来像X光一样扫描我们的表结构,并进行无损的切割。
准备好深呼吸,开启极其硬核的第 7 章(101页大关)了吗?我们依然可以一块一块地打透它!
欢迎来到第7章:关系数据库设计与规范化 (Normalization)!
如果说第6章画 E-R 图是一门“艺术”,那么第7章就是一门纯粹的“数学与工程学”。 人脑设计的表结构总会有瑕疵,第7章的目的,就是发明一台“X光机”,扫描我们的表结构,发现并切除肿瘤(数据冗余),且保证切除手术后人还能活得好好的(无损分解)。
我们来看看 P1 - P10 这头10页的“术前诊断”讲了什么,可以拆解为 4 个核心看点:
1. 什么样的表是“烂设计”?(P4)
在 P4 给出了一个极其糟糕的建表案例: 假设我们嫌麻烦,直接把“讲师 (instructor)”和“系别 (department)”揉成了一张大表 in_dep。 表里的列是:(ID, name, salary, dept_name, building, budget)。
它带来的致命后果是什么?
- 信息重复 (Repetition of information):计算机系 (Comp. Sci.) 的大楼是 Taylor,预算是 100000。因为计算机系有几十个老师,所以这个大楼和预算信息在这张表里被硬生生抄了几十遍!如果系里预算变了,我们要改几十行数据,极容易改漏导致数据不一致(更新异常)。
- Null 值问题:假设学校刚成立了一个“人工智能系”,但目前还没有招到任何老师。你想把这个系存进表里?对不起,因为没有老师,
ID和name都不存在,你只能用大量空值(Null)填补,甚至因为缺少主键根本存不进去!(插入异常)。
(注:P5 补充说明了一下,并不是所有大表都会导致重复,前提是合并的逻辑是对的。但通常大表都是灾难。)
2. 治疗方案:分解 (Decomposition),但有奇毒!(P6 - P7)
既然大表不好,那我们把它拆(分解 Decomposition)成两张小表不就行了? 把 in_dep 拆回 instructor 表和 department 表,这就叫分解。
但是,瞎拆会引发核灾难!我们看 P6-P7 的“有损分解 (Lossy Decomposition)”悲剧:
假设有一张员工表 employee,里面有 (ID, name, street, city, salary)。 有个小白程序员说:“这表太宽了,我把它劈成两半吧!”
- 表 1:
employee1 (ID, name) - 表 2:
employee2 (name, street, city, salary)
他拿 name(名字)作为两张表的纽带。 灾难发生了 (看 P7 的图): 数据库里有两个人都叫 Kim(一个是 57766 号的 Kim,住 Main 街;另一个是 98776 号的 Kim,住 North 街)。 当系统尝试把这两张表自然连接 (Natural Join) 拼回原状时,由于 name 都是 Kim,电脑根本分不清哪个 Kim 住哪里,于是进行了全排列组合交叉拼接。
结果:本来只有 2 个 Kim,拼回来之后变成了 4 个 Kim!凭空捏造出了根本不存在的假数据(虚假元组 spurious tuples)。这在数据库里叫作 有损分解 (Lossy Decomposition)。 (注意:“有损”不是指数据变少了,而是指信息失真了,拼不回原来的样子了!)
3. 终极追求:无损分解 (Lossless Decomposition, P8 - P9)
既然瞎拆会失真,数据库设计的核心目标就变成了:寻找无损分解。
- 数学定义 (P8):一个表 $R$ 拆分成 $R_1$ 和 $R_2$。如果我们把 $R_1$ 和 $R_2$ 进行自然连接 ($\bowtie$),得到的结果和原表 $R$ 一模一样(一列不差,一行不乱),这就叫无损分解: \(\Pi_{R_1}(r) \bowtie \Pi_{R_2}(r) = r\)
- 如果不等:拼回来的表比原表多出了一些莫名其妙的组合(即原表 $r$ 是拼装表的子集),那就是有损的。
怎么才能保证无损呢? (P9 伏笔) 在 P7 的失败案例中,是因为拿 name(会重复的属性)作为纽带去拆分;如果在 P9 中,我们用不会重复的属性(包含主键的机制)去拆分,就能完美复原。这就引出了后面的理论。
4. 规范化理论登场 (Normalization Theory, P10)
P10 正式宣告了第 7 章的主要任务。我们有了两个根本目标:
- 判断题:给你一张表 $R$,你要能通过数学公式,判断它是不是一个“好”表。
- 解答题:如果它不是一个好表,你要用算法把它无损地拆分成 ${R_1, R_2, …, R_n}$ 几个好表。
那我们靠什么数学工具来做判断和拆分呢? 靠的是“依赖关系”:
- 函数依赖 (Functional Dependencies, FDs) —— 第7章的绝对霸主,解决 99% 的问题。
- 多值依赖 (Multivalued Dependencies, MVDs) —— 解决最后 1% 的疑难杂症(对应后面的第四范式)。
小结:
前 10 页非常直白地告诉我们:表设计不好会有冗余,治冗余就得拆表,但拆表很容易拆出数据失真(有损)。我们亟需一套严格的数学理论来指导我们“无损拆表”。
接下来,我们就将引入数据库科学里最伟大的发明之一:函数依赖 (Functional Dependencies)。它是所有推导算法的地基。
准备好进入 P11 - P17:函数依赖基础,看看“决定”与“被决定”的数学之美了吗?
太棒了!准备好迎接数据库理论中最伟大、也是最核心的数学发明了吗?
从 P11 到 P17,我们正式引入函数依赖 (Functional Dependencies, 简称 FDs)。 如果说数据库是一座大厦,表是砖块,那么函数依赖就是这栋大厦的物理定律。后续所有的查错、拆表、算范式,全是基于这几页的理论。
我们把这 7 页的硬核理论拆解为 4 个极简的知识点:
1. 什么是函数依赖? (P11 - P13 🚨核心定义)
在现实世界中,数据和数据之间是有“规矩”的。 比如:只要我知道了你的学号 (ID),我就一定能唯一确定你的名字 (name)。 在数据库里,我们把这种规矩写成数学公式:ID $\rightarrow$ name。
- 大白话翻译:
X $\rightarrow$ Y读作“X 决定 Y”,或者“Y 依赖于 X”。 - 数学严谨定义 (P13 极其重要): 对于表 $R$ 里的任何两行数据(元组 $t_1$ 和 $t_2$),只要它们在 $X$ 上的值相等,它们在 $Y$ 上的值就必须绝对相等! \(t_1[X] = t_2[X] \implies t_1[Y] = t_2[Y]\) (就像世界上不能有两个学号完全一样,但名字却不同的人。如果有,数据库就报错!)
👉 看 P13 的经典考试题(图表验证): 幻灯片给了一个小表,A列是 1, 1, 3,B列是 4, 5, 7。
- 问:
A $\rightarrow$ B成立吗?- 绝对不成立!因为第一行和第二行的 A 都是
1,但是它们对应的 B 一个是4,一个是5。同一个A对应了不同的B,渣男!违反了函数依赖!
- 绝对不成立!因为第一行和第二行的 A 都是
- 问:
B $\rightarrow$ A成立吗?- 成立!因为 B 列的值是
4, 5, 7,全都不一样。既然前提(B相等)都凑不齐,自然就不可能违反规则。
- 成立!因为 B 列的值是
2. 函数依赖与“键 (Key)”的降维打击 (P15)
我们在第6章学过“主键、超键、候选键”,当时是用大白话说“能唯一标识一行”。 现在有了函数依赖,我们可以用数学公式把“键”重新定义一遍:
- 什么是超键 (Superkey) $K$?
- 数学定义:如果
K $\rightarrow$ R成立,那么 K 就是超键。 - 解释:如果属性 $K$ 能决定表 $R$ 里的所有其他属性,那它就是超键。
- 数学定义:如果
- 什么是候选键 (Candidate key)?
- 数学定义:首先
K $\rightarrow$ R(它是超键),其次,K 里面没有任何一个真子集 $\alpha$ 能够 $\alpha \rightarrow R$。 - 解释:超键去掉任何一个多余属性后,就再也“决定”不了全表了,说明它是最精简的(Minimal),这就是候选键。
- 数学定义:首先
👉 更牛的用途: 函数依赖不仅能定义键,还能捕捉键之外的“地下情”! 比如在之前的烂表里,主键是 ID。但表里竟然还存在 dept_name $\rightarrow$ building(系名 决定 大楼)。这种“非主键决定非主键”的情况,就是导致数据冗余的罪魁祸首!函数依赖就像照妖镜,把它们全照出来了。
3. 闭包 (Closure) 与 逻辑推导 (P14)
这也是一个核心数学概念:闭包 ($F^+$)。
- 现实场景:你给数据库设定了两条规矩(函数依赖集 $F$):
A $\rightarrow$ B(学号 决定 身份证号)B $\rightarrow$ C(身份证号 决定 户籍地)
- 逻辑推导:就算你不告诉数据库,数据库也能根据简单的逻辑(传递性)自己推导出来:
A $\rightarrow$ C(学号 决定 户籍地)! - 什么是闭包 $F^+$?:把你给的所有基本依赖,加上数据库自己能推理出来的所有可能的依赖,汇总成一个巨大的集合,这个全集就叫闭包,用 $F^+$ 表示。
4. 平凡的函数依赖 (Trivial FDs, P17)
最后补充一个概念,什么叫“平凡”的依赖?说白了就是“正确的废话”。
- 定义:如果
X $\rightarrow$ Y中,Y 本来就是 X 的子集($Y \subseteq X$),这就叫平凡函数依赖。 - 举例:
ID, name $\rightarrow$ ID(知道你的学号和名字,我当然能确定你的学号!废话!)name $\rightarrow$ name(知道你的名字,我就能确定你的名字。废话!)
- 意义:虽然是废话,但在数学证明里,平凡的函数依赖在任何表、任何情况下都是绝对 100% 成立的。
小结与避坑提示 (P16):
- P16 提醒我们:函数依赖必须是业务逻辑(Real-world constraints)决定的,绝不能看图说话!
- 比如你正好看到一张表,里面只有两个老师,恰好他们的名字不同,ID也不同。你不能由此断定
name $\rightarrow$ ID成立!因为未来可能会招来同名的老师。规则必须是业务敲定的。
到这里,关于函数依赖的“基础三观”我们已经建立完毕。 接下来,我们将马上拿起这把武器,去证明之前提到的“分解”到底是有损还是无损的 (P18 - P21:无损分解与依赖保持的判定)。
准备好看看我们如何用 $X \rightarrow Y$ 来鉴定一张表拆得对不对了吗?
太棒了!有了“函数依赖”这把照妖镜,我们终于可以来评判一个“拆表方案”到底是好是坏了。
从 P18 到 P21,引入了数据库规范化拆表时必须通过的两大终极考试:
- 无损分解 (Lossless Decomposition):底线要求,数据绝对不能失真。
- 依赖保持 (Dependency Preservation):性能要求,不能为了拆表导致每次插入数据都卡得要死。
这部分是考试出题的重灾区,我们用大白话把它彻底讲透:
考试一:如何判定“无损分解”? (P18 - P19 🚨 必考公式)
我们在前面讲过,瞎拆表会导致自然连接时“凭空捏造”出假数据(有损)。那怎么拆才是无损的呢? P18 给出了一个极其伟大且好用的数学判定定理:
如果把大表 $R$ 拆成了两张小表 $R_1$ 和 $R_2$,只要满足以下两个条件之一,这次拆分就是绝对无损的!
- 条件 1:$R_1 \cap R_2 \rightarrow R_1$
- 条件 2:$R_1 \cap R_2 \rightarrow R_2$
👉 【大白话翻译这个神级公式】: $R_1 \cap R_2$ 指的是两张小表的交集,也就是它们共同拥有的那个列(连接的桥梁)。 这个公式的意思是:这座“桥梁”,必须是其中某一张小表的主键(或超键)! (只要桥梁在其中一张表里是唯一不重复的,连回去的时候就不会发生发散式的乱拼。)
👉 【实战演练 P19】:
- 我们有一个大表 $R(A, B, C)$。
- 业务规矩(函数依赖集 F):
A $\rightarrow$ B,B $\rightarrow$ C。
拆法 1:拆成 $R_1(A, B)$ 和 $R_2(B, C)$
- 找桥梁(交集):两张表共同拥有的是 B。
- 套公式:查一下现有的规矩,B 能决定什么?我们有
B $\rightarrow$ C。既然 B 能决定 C,加上平凡依赖B $\rightarrow$ B,所以B $\rightarrow$ BC。 - 结论:BC 正好就是 $R_2$!也就是 桥梁 B 决定了 $R_2$。公式成立,这是无损分解!
拆法 2:拆成 $R_1(A, B)$ 和 $R_2(A, C)$
- 找桥梁(交集):两张表共同拥有的是 A。
- 套公式:查一下规矩,A 能决定什么?我们有
A $\rightarrow$ B,加上平凡依赖A $\rightarrow$ A,得出A $\rightarrow$ AB。 - 结论:AB 正好就是 $R_1$!也就是 桥梁 A 决定了 $R_1$。公式成立,这也是无损分解!
考试二:什么是“依赖保持”? (P20 - P21 隐形的性能杀手)
无损分解只是保证了数据拼回去不失真。但是,拆表还会带来一个严重的性能副作用。
- 痛点 (P20):数据库在每次
INSERT或UPDATE数据时,都必须检查有没有违反业务规矩(函数依赖)。如果规矩涉及的几个属性恰好在同一张表里,系统一秒钟就检查完了;如果这几个属性被你拆分到了不同的表里,系统就必须先把表 Join(连接)起来才能检查,这会极其消耗性能(Costly)! - 定义:如果一次拆表,导致某个函数依赖被生生切断,跨越了两张表,我们称这种拆表 “未能保持依赖 (NOT dependency preserving)”。
👉 【实战演练 P21】: 我们来看一个极其纠结的案例。
- 大表:
dept_advisor (s_ID, i_ID, dept_name)(学生ID,导师ID,系名) - 业务规矩 (FDs):
i_ID $\rightarrow$ dept_name(老师唯一属于某个系,所以老师ID决定系名)s_ID, dept_name $\rightarrow$ i_ID(学校规定:一个学生在一个系里,最多只能认一个导师。所以 学生+系名 决定 导师)
这个表由于存在 i_ID $\rightarrow$ dept_name,会导致极其严重的冗余(每次存这个老师带别的学生,系名都要重写一遍)。 为了消除冗余,我们大笔一挥,把表拆了:
- 表1:老师和系
(i_ID, dept_name) - 表2:学生和老师
(s_ID, i_ID)
完蛋了!依赖保持被破坏了! 请看规矩 2:s_ID, dept_name $\rightarrow$ i_ID。 现在 s_ID 在表2里,dept_name 在表1里。如果你现在想给某个学生新增一个导师,数据库为了检查“他是不是在这个系里有两个导师”,必须每次把两张表 Join 起来才能验证!系统会被直接拖垮。
小结:
这就是数据库设计中最让人又爱又恨的博弈:
- 想消除冗余 $\implies$ 就必须拆表。
- 拆表 $\implies$ 必须满足交集是超键,保证无损分解。
- 拆得太狠 $\implies$ 可能会破坏依赖保持,导致验证规矩时性能崩溃。
P21 的结尾给我们留下了一个巨大的悬念:“既想消除一切冗余,又想保持所有依赖”,在某些极端情况下,是根本做不到的!
这也是为什么接下来数据库界诞生了两大绝世高手:
- 完美主义者:BC范式 (Boyce-Codd Normal Form, P22-P27) —— 宁可牺牲依赖保持,也必须把所有冗余杀得片甲不留!
- 现实主义者:第三范式 (3NF, P28-P31) —— 允许表里保留一点点冗余,但坚决要维护依赖保持!
你准备好进入 P22-P27,去会会这位眼里容不得沙子的完美主义杀手 BCNF (Boyce-Codd 范式) 了吗?
太棒了!我们现在正式请出数据库规范化理论中的“完美主义杀手”——Boyce-Codd 范式 (简称 BCNF)。
从 P22 到 P27,核心就讲了一件事:BCNF 眼里揉不得一粒沙子,它要榨干表里哪怕最后一点点由于函数依赖引起的冗余,就算为此付出代价也在所不惜!
我们把这部分拆解为 3 个核心招式:
招式一:什么是 BCNF?(绝对纯洁的判定标准,P23-P24)
怎么判断一张表是不是 BCNF?P23 给出了极其严苛的数学定义。 对于表里任何一个非平凡的函数依赖 X $\rightarrow$ Y,X 必须是这小张表的超键 (Superkey)!
👉 【大白话翻译】: 在 BCNF 的世界里,“谁敢决定别人,谁就必须能代表全表!” 也就是箭头左边的属性,必须是唯一标识符(主键/候选键/超键)。如果一个阿猫阿狗(非主键属性)也敢跑来决定别人,那就是犯了 BCNF 的大忌!
👉 【看 P24 的反面教材】:
- 表:
in_dep (ID, name, salary, dept_name, building, budget)。主键是ID。 - 业务规矩:
dept_name $\rightarrow$ building, budget(系名决定大楼和预算)。 - BCNF 警察来查房了:发现
dept_name只是个普通的列,它根本不是这整张表的主键(系名不能代表一个特定的老师),但它居然敢在表里“决定”大楼和预算! - 定罪:违反 BCNF!正是因为
dept_name不是主键还带着小弟,导致同一个系名重复出现时,大楼和预算被反复抄写,产生严重冗余。
招式二:BCNF 的“切除手术” (Decomposition 算法,P25)
既然查出了“肿瘤(违规的函数依赖)”,怎么无损地把它切除呢?P25 给出了标准手术刀算法:
假设大表 $R$ 中,发现了违规依赖 $\alpha \rightarrow \beta$。 一刀劈成两张表:
- 第一张表(割除肿瘤):把作乱的 $\alpha$ 和它的跟班 $\beta$ 单独拎出来建一张表:$(\alpha \cup \beta)$。在这里,$\alpha$ 光荣地成为了新表的主键。
- 第二张表(保留原貌):在原来的大表 $R$ 里,把跟班 $\beta$ 删掉,但必须把带头大哥 $\alpha$ 留下来当联络员(外键):$R - (\beta - \alpha)$。
👉 【给 P24 的表做手术】:
- 违规依赖:
dept_name $\rightarrow$ building, budget - 第一张表:
(dept_name, building, budget)。这张表里,dept_name是主键,完美符合 BCNF。 - 第二张表:原来的大表删掉大楼和预算,留下系名。变成
(ID, name, dept_name, salary)。这张表里,ID是主键,也完美符合 BCNF。 - 结果:冗余彻底消失,且通过
dept_name依然能无损连接(完美符合上节课学的“交集是超键”的无损分解条件)。
招式三:BCNF 的“阿喀琉斯之踵”——痛失依赖保持 (P26-P27 🚨 终极考点)
手术很成功,但是病人瘸了。 BCNF 追求极致的无冗余,但有时候,这会导致我们上一节提到的“依赖保持”被残忍打破! P27 再次把那个纠结的例子拿了出来,这是数据库理论中最经典的悖论:
- 纠结表:
dept_advisor(s_ID, i_ID, dept_name)(学生ID,老师ID,系名) - 两条规矩 (FDs):
i_ID $\rightarrow$ dept_name(老师唯一属于某系)s_ID, dept_name $\rightarrow$ i_ID(学生在一个系只能认一个老师)
BCNF 警察来查房了: 看到第一条规矩 i_ID $\rightarrow$ dept_name。i_ID 不是整张表的超键(因为一个老师带多名学生),居然敢决定系名?砍掉!
强行做 BCNF 手术:
- 表 1:
(i_ID, dept_name) - 表 2:
(s_ID, i_ID)
悲剧发生了: 冗余确实没了。但是请看第二条规矩 s_ID, dept_name $\rightarrow$ i_ID。 现在 s_ID 在表2里,dept_name 在表1里。这条至关重要的约束规则被活生生地跨表撕裂了! 每次新增一组师生关系,数据库为了检查有没有违反第二条规矩,必须疯狂做全表 Join 连接,系统卡死。
P27 的最终宣判: “It is not always possible to achieve both BCNF and dependency preservation.” (在某些极端情况下,我们绝对不可能同时得到 BCNF 和依赖保持!)
小结:
BCNF 是个眼里揉不得沙子的洁癖,为了消除由函数依赖带来的哪怕一丝冗余,它会毫不犹豫地切表,哪怕切断了业务检查的命脉(依赖保持)。
面对这种“鱼与熊掌不可兼得”的死局,数据库学界是怎么妥协的呢? 这就是接下来 P28 - P31:现实主义的妥协 —— 第三范式 (3NF) 要登场解决的问题了!
准备好看看 3NF 是如何在 BCNF 的基础上“放宽底线、网开一面”,从而保住依赖保持的吗?
太棒了!前面我们看到 BCNF 是个极度的完美主义者,为了消灭冗余,它甚至不惜切断业务依赖(失去依赖保持)。
但在真实的工程世界里,“性能”往往比“绝对没有冗余”更重要。于是,数据库界推出了一套现实主义的妥协方案 —— 第三范式 (3NF)。
从 P28 到 P35,我们不仅会看到 3NF 是如何巧妙破局的,还会发现一个惊天反转:就算是完美的 BCNF,在某些极端数据面前也会翻车(从而引出更高的第四范式 4NF)。
我们分 4 个回合来啃下这部分:
回合一:3NF 的“法外开恩”(放宽标准,P28)
怎么判断一张表是不是在 3NF 里? P28 给出了定义:对于任何一个非平凡的函数依赖 X $\rightarrow$ Y,只要满足以下三个条件之一,就是 3NF:
- 它是平凡的(废话依赖)。
X是超键(到这里为止,跟 BCNF 是一模一样的!)- 👉 3NF 的独家“免死金牌”:哪怕
X不是主键/超键,只要右边的Y是某个候选键的一部分(主属性),我 3NF 就放你一马!
(用大白话解释:BCNF 规定非主键绝对不能决定别人。但 3NF 说,如果你决定的那个“别人”,碰巧是最高管理层(候选键)的一员,那我就睁一只眼闭一只眼,算你合法!)
回合二:拯救那个崩溃的例子(P29 绝地反击)
让我们立刻用 3NF 的“免死金牌”,去拯救那个被 BCNF 搞崩溃的经典例子(P29):
- 表:
dept_advisor (s_ID, i_ID, dept_name) - 规矩 1:
s_ID, dept_name $\rightarrow$ i_ID - 规矩 2:
i_ID $\rightarrow$ dept_name(违规分子)
查候选键:这表有两个候选键 {s_ID, dept_name} 和 {s_ID, i_ID}。 BCNF 判死刑:规矩2 里,i_ID 不是超键,砍掉拆表! 3NF 判无罪:规矩2 里,i_ID 确实不是超键,但是!它决定的 dept_name,正好包含在候选键 {s_ID, dept_name} 里! 符合“免死金牌”条件!
结果:这张表满足 3NF,不需要拆表! 完美保住了原表结构,也就顺理成章地保住了“依赖保持”,以后每次插入数据,不用再跨表 Join 验证了!
回合三:妥协的代价 —— 允许轻微冗余(P30 - P32 总结对比)
天下没有免费的午餐。3NF 既然放过了那个违规的依赖,就必须默默承受它带来的副作用:
- 冗余重现 (P30):看那个 J, K, L 的表。由于 3NF 放行了,导致当 $j_1$ 和 $j_2$ 都对应同一个 $l_1$ 时,数据 $k_1$ 被重复抄写了两次。(存在部分冗余)
- 空值困扰:如果想存一组新的 $(L, K)$ 关系,但目前没有对应的 $J$,就只能插入 Null。
👉 终极对决:BCNF vs 3NF (P31 - P32) 作为架构师,你该怎么选?
- 理论界的首选是 3NF:因为数学证明了,把一张大表拆成 3NF,一定可以同时做到“无损分解” + “依赖保持”。而 BCNF 有时做不到。
- 现实工程的无奈 (P32 重点):由于目前世面上的关系型数据库(如 MySQL, Oracle)在底层的 SQL 语法中,极难跨表去检查函数依赖(除非写代价极高的断言/触发器)。所以,为了性能,我们强烈倾向于 3NF,宁可吃下一点点冗余,也绝不能让依赖断裂。
回合四:惊天反转,BCNF 也有搞不定的事?(4NF 的引出,P33 - P35)
到了这里,你以为只要满足了 BCNF(彻底消灭函数依赖冗余),数据库就天下太平了吗? P33 举了一个极其可怕的例子,狠狠地打了 BCNF 的脸!
- 奇葩表:
inst_info (ID, child_name, phone)- 业务场景:一个老师(22222)有两个孩子(David, William),同时他还有两个电话(1234, 4321)。
- 诡异的数据组合:你看表里存了啥?为了把信息存全,出现了极度恐怖的笛卡尔积交叉组合!David 和 1234 一行,David 和 4321 一行,William 和 1234 一行…
- BCNF 警察来查房 (P34): 警察查了一圈函数依赖(FD)。孩子名字能决定电话吗?不能。电话能决定孩子吗?不能。ID 能单独决定某一个孩子吗?也不能。 结论:这张表里竟然没有任何非平凡的函数依赖!所以它完美符合 BCNF 标准!
- 灾难后果 (P34-P35): 虽然它是合法的 BCNF,但如果我们现在要给这位老师新增一个电话号码,我们就必须在表里新增两行数据(分别配对 David 和 William)。这特么简直是插入异常的噩梦!
为什么会这样? 因为这根本不是“决定(单值依赖)”的问题,这是两组互相独立的多值属性(多值依赖,Multivalued Dependencies)被强行揉在同一张表里产生的问题!
既然基于“函数依赖”的 BCNF 治不了它,我们就需要引入更高级的兵器。 这就暗示了我们要使用:P35 的 第四范式 (4NF)!必须把孩子和电话拆成两张独立的表。
小结:
到这里,我们明白了:
- 3NF 是为了兼顾性能(依赖保持),在 BCNF 基础上所做出的伟大妥协。
- BCNF 虽然在单值依赖上做到了完美,但在独立的多值属性面前束手无策。
刚才全是概念的探讨!接下来,数据库学家要把这套理论变成可以直接拿来编程跑的纯数学算法了! 👉 我们将进入 P36 - P45:函数依赖理论与求闭包算法 (Closure Algorithm)。 这是期末考试 100% 会考推导的大题(比如:给你几个 FDs,让你推导出一共有哪些候选键)。
你准备好拿出纸笔,进入真正的数学推导环节了吗?
太棒了!请准备好纸和笔,因为从 P36 到 P45,我们将完全进入纯数学推导与算法的领域。
前面我们一直靠“肉眼”去判断哪张表好、哪张表坏。但在实际的数据库软件中,计算机是没有直觉的,它需要绝对严谨的公式和算法。这10页的核心目标只有一个:给你一堆基础的规则,教你怎么写个算法,把隐藏在背后的所有秘密规则全挖出来(求闭包),顺便把候选键给算出来!
我们把这部分拆解为 3 个绝招来练:
绝招一:数据库的“物理定律” —— 阿姆斯特朗公理 (Armstrong’s Axioms, P38 - P41)
假设你是一家公司的老板,你定下了一条死规矩(函数依赖 F):员工号 $\rightarrow$ 部门(知道员工号就能确定他所在的部门)。同时还定了一条:部门 $\rightarrow$ 楼层。 哪怕你没说,大家也能在脑海里自然推导出:员工号 $\rightarrow$ 楼层。这个推导出来的全集,就叫 闭包 ($F^+$)。
为了让计算机也能像人一样推理,数学家提出了阿姆斯特朗公理(3条基本定律 + 3条引申定律,P39 和 P41):
【三大基本定律 (P39)】:
- 自反律 (Reflexivity):如果 $\beta \subseteq \alpha$,那么 $\alpha \rightarrow \beta$。
- 大白话:总体一定能决定局部。比如
(姓名, 年龄) $\rightarrow$ 姓名。这是废话,但也得写进代码里。
- 大白话:总体一定能决定局部。比如
- 增广律 (Augmentation):如果 $\alpha \rightarrow \beta$,那么两边加上同一个属性 $\gamma$,结论依然成立:$\gamma\alpha \rightarrow \gamma\beta$。
- 大白话:学号能决定姓名,那“学号+性别”一定能决定“姓名+性别”。
- 传递律 (Transitivity):如果 $\alpha \rightarrow \beta$,且 $\beta \rightarrow \gamma$,那么 $\alpha \rightarrow \gamma$。
- 大白话:学号决定专业,专业决定辅导员,所以学号决定辅导员。
【三大引申定律 (P41 极其好用)】: 基于上面三条,又推导出了做题时最爱用的三条捷径:
- 合并律 (Union):如果 $\alpha \rightarrow \beta$ 且 $\alpha \rightarrow \gamma$,那么 $\alpha \rightarrow \beta\gamma$。
- (左边一样,右边可以捏在一起)
- 分解律 (Decomposition):如果 $\alpha \rightarrow \beta\gamma$,那么 $\alpha \rightarrow \beta$ 且 $\alpha \rightarrow \gamma$。
- (右边可以随意拆开。注意:左边绝对不能拆!)
- 伪传递律 (Pseudotransitivity):如果 $\alpha \rightarrow \beta$,且 $\gamma\beta \rightarrow \delta$,那么 $\alpha\gamma \rightarrow \delta$。
- (把左边里的 $\beta$ 替换成等价的 $\alpha$)
绝招二:避开“算力黑洞” —— 属性集闭包算法 ($\alpha^+$, P42 - P43)
面临的困境 (P42): 如果想算出一个数据库所有的规则(即求 $F^+$ 的全集),这在计算上是一个极其恐怖的噩梦。如果有 10 个属性,排列组合出来的规则可能是天文数字,计算机会算死机的。
破局之法:属性集闭包算法 (P43 🚨 期末必考第一大题)! 实际工程和考试中,我们几乎从来不去算整个 $F^+$。我们更关心的是:给定几个特定的属性(比如 A 和 B),拿着这把钥匙,到底能顺藤摸瓜解锁多少个其他的属性? 这就叫属性集 $\alpha$ 的闭包,记作 $\alpha^+$。
【滚雪球算法步骤 (P43)】:
- 初始状态:手里有啥,雪球就是啥。
result = 原始属性。 - 开始遍历规则库(一遍遍地过):
- 如果看到一条规则的左边(前提),已经完全包含在你的雪球里了,那就把这根藤上结的瓜(右边的属性)吞进你的雪球里。
- 一直绕圈遍历,直到雪球再也滚不大(没有新属性加进来)为止。
绝招三:实战演练与三大神级用途 (P44 - P45)
光说不练假把式,我们直接来看 P44 的满分实战题:
- 表:$R = (A, B, C, G, H, I)$
- 规则库 $F$:
A $\rightarrow$ B,A $\rightarrow$ C,CG $\rightarrow$ H,CG $\rightarrow$ I,B $\rightarrow$ H
题目:求 AG 的闭包,即 $(AG)^+$
- 第1轮:初始雪球
result = AG- 看规则
A $\rightarrow$ B和A $\rightarrow$ C:我们雪球里有A,条件满足!吞下B和C。雪球膨胀为:ABCG。 - 看规则
CG $\rightarrow$ H和CG $\rightarrow$ I:我们雪球里现在有CG了!条件满足!吞下H和I。雪球膨胀为:ABCGHI。
- 看规则
- 第2轮:再过一遍,发现能吞的都吞了,全表所有属性
A,B,C,G,H,I全在雪球里了。算法结束!$(AG)^+ = ABCGHI$。
👉 【这东西到底有啥用?P45 给出了 3 大神级用途】:
- 用途一:判断超级大 Boss(找超键和候选键)
- 既然 $(AG)^+$ 把表里的所有属性都找齐了,说明拿着
AG就能决定全表。所以AG绝对是一个超键 (Superkey)! - 那它是不是最精简的候选键 (Candidate key) 呢? 试着剥夺它的权力:
- 单独求 $(A)^+$,发现等于
ABCH,凑不齐。 - 单独求 $(G)^+$,发现等于
G,也凑不齐。
- 单独求 $(A)^+$,发现等于
- 结论:少一个都不行,所以
AG就是候选键!(这是找候选键的唯一标准算法)。
- 既然 $(AG)^+$ 把表里的所有属性都找齐了,说明拿着
- 用途二:验证某条依赖到底成不成立(查岗)
- 假设有人问:“在这个表里,
AG $\rightarrow$ I成立吗?” - 根本不用去翻那些复杂的传递公理,直接算左边的闭包!$(AG)^+$ 里面包不包含
I?包含!那AG $\rightarrow$ I就绝对成立。太好用了!
- 假设有人问:“在这个表里,
- 用途三:算某个集合下的所有闭包依赖
小结:
把 P36-P45 揉碎了看,其实就教了一招:求属性的闭包 (Attribute Closure)。 考试只要考这章,必定有一道 10 分大题是给你一堆依赖,让你求候选键。你只要会这个“滚雪球”算法,分分钟拿下。
但是,别人给你的规则库(函数依赖 F)可能是很冗长、有水分的(比如同时给你 A->B, B->C, A->C,其实 A->C 是废话,可以推出来)。 如果带着这些累赘去拆表,效率会极低。
所以,在用这些依赖去进行真正的“范式分解”之前,我们要先对规则库进行一次“终极瘦身”! 👉 这就是接下来 P46 - P54:正则覆盖 (Canonical Cover) 与无关属性 的重头戏。这也是全书最烧脑的数学简化过程。
准备好进入这套“挤水分”的终极化简算法了吗?
太棒了!请系好安全带,因为从 P46 到 P54,我们将迎来数据库规范化理论中最烧脑、但也最有意思的“微操”环节:求正则覆盖(Canonical Cover,有时也叫最小函数依赖集)。
在上一节,我们学会了用“闭包(雪球算法)”来挖掘隐藏的规则。但是,别人给我们的原始规则库(函数依赖集 $F$)往往是极其臃肿的,里面充满了废话和重复。如果我们带着这些累赘去拆表,计算机会卡死的。
这 9 页的核心任务只有一个:给规则库“挤水分”,删掉所有的废话,得到一个最精简、最干练,但威力与原版完全一样的规则集合(也就是正则覆盖 $F_c$)。
我们分 4 步来把这堆水分榨干:
第一步:找出规则里的“混子” —— 什么是无关属性?(Extraneous Attributes, P47 - P48)
一条规则(函数依赖)长这样:左边 $\rightarrow$ 右边。 有时候,左边或右边会混进一些“不痛不痒”的属性,删了它们对大局毫无影响。这就叫无关属性(或多余属性)。
我们分左右两边来看:
1. 左边混进了“累赘” (P47)
- 例子:公司规定
门禁卡 + 蓝衬衫 $\rightarrow$ 进大楼 (AB $\rightarrow$ C)。 - 分析:其实公司还有条隐藏规矩:
门禁卡 $\rightarrow$ 进大楼 (A $\rightarrow$ C)。这意味着“蓝衬衫 (B)”在这里完全是个累赘!有了门禁卡就能进,不管你穿什么。 - 结论:如果在
AB $\rightarrow$ C中,即使删掉 B(变成了更苛刻的A $\rightarrow$ C),整个规则系统依然逻辑自洽,那么 B 就是左边的无关属性。
2. 右边写了“废话” (P48)
- 例子:办卡送福利:
VIP卡 $\rightarrow$ 免费咖啡, 免费小饼干 (A $\rightarrow$ CD)。 - 分析:其实吧,店里本身就有个规矩:
免费咖啡 $\rightarrow$ 免费小饼干 (C $\rightarrow$ D)。 - 结论:既然你有了咖啡就自动有饼干,那我送你福利时,只需要说
VIP卡 $\rightarrow$ 免费咖啡 (A $\rightarrow$ C)就行了!这里右边的“免费小饼干 (D)”就是一句正确的废话。删掉 D,这就是右边的无关属性。
第二步:照妖镜算法 —— 怎么用代码测试无关属性?(P49 - P51)
大白话谁都会说,但在做题时,怎么用严格的数学证明某个属性是废话呢?P50 给出了终极测试算法(利用上一节的求闭包雪球算法):
👉 测试左边的累赘(查 $\alpha \rightarrow \beta$ 里的 $A$):
- 方法:把你怀疑是累赘的 $A$ 踢出左边。用剩下的左边去滚雪球(求闭包),用原版规则库 $F$。如果滚出来的雪球依然包含了右边的 $\beta$,那 $A$ 绝对是累赘,删!
👉 测试右边的废话(查 $\alpha \rightarrow \beta$ 里的 $A$):
- 方法:把你怀疑是废话的 $A$ 从右边踢走。此时规则库变成了一个缺胳膊少腿的新规则库 $F’$。然后,你拿着左边的 $\alpha$,在这个新规则库里滚雪球。如果雪球依然能把 $A$ 吞进来,说明不用你声明,大家也能推导出 $A$,那么 $A$ 就是右边的废话,删!
第三步:什么是“正则覆盖 (Canonical Cover)”?(P52 - P53)
当我们把所有的累赘和废话都删干净,并且把能合并的都合并了,留下的那个黄金集合,就叫正则覆盖,记作 $F_c$。
满足 $F_c$ 必须达到 3 个苛刻的条件 (P52):
- 等价性:$F_c$ 和原版 $F$ 逻辑上完全等价(闭包一样大,谁也没少谁)。
- 无冗余:里面没有任何一个无关属性(左边右边都被榨干了)。
- 不啰嗦:每条规则的左边必须是唯一的。(如果有
A $\rightarrow$ B和A $\rightarrow$ C,必须合并成A $\rightarrow$ BC)。
操作口诀 (P53):
- 合并:把左边一样的全捏到一起(用合并律)。
- 榨干:一个个去检查左边和右边有没有无关属性,有就删掉。
- 循环:重复 1 和 2,直到再也没有东西可以删为止。
第四步:期末终极 Boss 战 —— 完美实战演练 (P54 🚨🚨🚨 超级重点)
这页 PPT(P54)是全书最经典的例题,请一定跟着我的思路在脑子里过一遍,这 10 分你就稳拿了:
- 初始规则库 $F$:
A $\rightarrow$ BCB $\rightarrow$ CA $\rightarrow$ BAB $\rightarrow$ C
【第一轮:合并同类项】
- 我们看到
A $\rightarrow$ BC和A $\rightarrow$ B,左边都是A。合并!变成A $\rightarrow$ BC。 - 现在规则库精简为:
{A $\rightarrow$ BC, B $\rightarrow$ C, AB $\rightarrow$ C}。
【第二轮:对 AB $\rightarrow$ C 开刀(测试左边)】
- 怀疑:左边的
A是不是累赘?如果删掉A,变成B $\rightarrow$ C,这规则成立吗? - 验证:一看规则库,里面恰好已经有一条
B $\rightarrow$ C了!这说明就算没有 A,B 也完全能搞定 C。 - 动作:
A是累赘!直接从AB $\rightarrow$ C里删去 A,变成B $\rightarrow$ C。 - 由于库里已经有一个
B $\rightarrow$ C了,重复的直接删掉。 - 现在规则库精简为:
{A $\rightarrow$ BC, B $\rightarrow$ C}。
【第三轮:对 A $\rightarrow$ BC 开刀(测试右边)】
- 怀疑:右边的
C是不是废话? - 验证:我们假装把
C删掉,变成新库 $F’ = {A \rightarrow B, B \rightarrow C}$。在这个新库里,我们拿着A去滚雪球。- 由
A $\rightarrow$ B吞下 B,此时雪球是AB。 - 由
B $\rightarrow$ C吞下 C,此时雪球是ABC。
- 由
- 结论:你看!就算我把
A $\rightarrow$ BC里的C划掉,我依然能通过别的路径(借道 B)把 C 给找回来!这就证明在A $\rightarrow$ BC这句话里,说不说C都是废话。 - 动作:
C是废话!删去!A $\rightarrow$ BC变成了A $\rightarrow$ B。
【大结局】 再也没有能合并或能删减的了。 最终的正则覆盖 $F_c = $ {A $\rightarrow$ B, B $\rightarrow$ C}。
(你看看,原本啰里啰嗦 4 条规则,其实核心骨架只有这 2 条最基础的链条!这就是正则覆盖的魅力。)
小结:
通过 P46-P54,我们学会了数据库规范化中的终极“瘦身术”。 以后任何复杂的算法,我们都不要拿原始的 $F$ 去跑,而是先求出 $F_c$,拿着这个最干练的规则库去操作,运算速度会提升成百上千倍。
打通了这个数学难关,接下来,我们要开始用这套“干练的规则”,去执行真正的拆表大业了! 👉 我们将进入 P55 - P64:依赖保持算法 与 BCNF 分解算法。 我们要把之前在 P22-P27 用“肉眼”和“大白话”讲的 BCNF 拆表,变成机器能懂的无脑循环代码。
你准备好进入代码算法环节了吗?
太棒了!我们在前面 P20-P21 的时候,用“大白话”讲过什么是“依赖保持”(拆表后,如果验证某条规矩不需要跨表连表,就是依赖保持)。
但是,对于计算机来说,面对成百上千个属性和复杂的拆分,怎么用一套不用算死机的高效算法来验证它到底保没保持住呢?
这就是 P55 到 P58 要解决的终极问题。计算机科学家们发明了一个极为精妙的“跨表滚雪球”算法。我们分 3 步来破解它:
第一步:暴力的绝路与“局部规矩”的定义 (P55 - P56)
1. 暴力验证法为什么不行?(P55)
- 按照理论定义:假设大表拆成了两张小表 $R_1$ 和 $R_2$。你把 $R_1$ 里能保住的规矩(记作 $F_1$),和 $R_2$ 里能保住的规矩($F_2$)加在一起,求个大闭包。如果这个大闭包等于原来总规矩的闭包 $F^+$,那就是依赖保持。
- 死局:求 $F^+$ 的计算量是指数级 (Exponential time) 的!稍微大点的表,服务器就算冒烟了也算不完。
2. 什么叫“局部的规矩 (Restriction)”?(P56)
- 在拆完表后,我们要看看每张小表能“兜底”哪些规矩。
- 定义:对于小表 $R_i$,所谓的“局部规矩 $F_i$”,就是指那些左边和右边所有属性都完完全全只包含在 $R_i$ 内部的函数依赖。
- 大白话:只要这条规矩没跨界,在这一张小表里就能自给自足地验证,那它就被这张小表保下来了。
第二步:神级算法:多项式时间的“跨表滚雪球” (P57 🚨 核心算法)
既然不能暴力算全集,科学家想出了一个绝妙的办法:逐条验证! 对原规则库 $F$ 里的每一条规则 $\alpha \rightarrow \beta$,我都去测试一下:在不连表的情况下,我能不能靠各个小表的“局部规矩”接力,硬生生把 $\beta$ 给推导出来?
👉 【大白话模拟这个 P57 的牛逼算法】: 假设我们想验证规矩 A $\rightarrow$ C 能不能保住。但 A 在表 1,C 在表 2。
- 初始筹码:把你拥有的前提 $\alpha$ 捏在手里(目前
result = {A})。 - 去表 1 碰碰运气:
- 带着
A走进表 1 ($R_1$)。问:表 1 里有啥是我能用的?(计算result \cap R_1)。 - 在表 1 的地盘上开始滚雪球,利用表 1 内部的规矩,看看能额外赚到什么属性?假设表 1 里有规矩
A $\rightarrow$ B,那你赚到了B。 - 把在表 1 赚到的所有东西且属于表 1 的部分带走(加入到你的
result里)。现在你手里有result = {A, B}。
- 带着
- 去表 2 碰碰运气:
- 带着
{A, B}走进表 2 ($R_2$)。表 2 说:“我不认识 A,但我认识 B!”(你的筹码在表 2 里只剩下 B)。 - 没关系,拿着 B 在表 2 里滚雪球。假设表 2 里有规矩
B $\rightarrow$ C,恭喜你,赚到了C! - 把
C加进总资产。现在result = {A, B, C}。
- 带着
- 循环结束与判定:
- 各个表来回转,直到你再也赚不到新属性为止。
- 最后看看你手里的总资产
result。里面有没有包含我们要找的目标 $\beta$(这里是 C)? - 如果有,恭喜!哪怕不连表,靠着在各个小表之间“白嫖”并传递线索,这条规矩依然存活!这就是依赖保持!
(这个算法的伟大之处在于,它不需要求庞大的全集,只需要在几个小表里来回滚几次雪球,运算时间从指数级降维到了多项式时间 (Polynomial time),计算机一秒就能算完!)
第三步:完美的实战验证 (P58)
我们拿 P58 的那个经典例子,用刚才的逻辑跑一遍:
- 原始大表:$R = (A, B, C)$
- 规矩 $F$:
A $\rightarrow$ B,B $\rightarrow$ C - 拆分方案:拆成了两张小表:$R_1 = (A, B)$ 和 $R_2 = (B, C)$
1. 验证是否无损分解?
- 找交集:$R_1 \cap R_2 = {B}$
- 交集 B 能决定谁?根据规矩有
B $\rightarrow$ C,即B $\rightarrow$ BC。 - BC 就是 $R_2$。交集是 $R_2$ 的超键,完美,这是无损分解!
2. 验证是否依赖保持?
- 规矩 1:
A $\rightarrow$ B。A 和 B 都在 $R_1$ 里,没跨表,$R_1$ 轻松保住它。 - 规矩 2:
B $\rightarrow$ C。B 和 C 都在 $R_2$ 里,没跨表,$R_2$ 轻松保住它。 - 既然原规则库 $F$ 里的每一条规矩都被完好无损地分配到了各个小表里,没有任何一条被撕裂。
- 结论:毫无疑问,这是依赖保持!
小结:
到这里,我们不仅明白了什么是依赖保持,还掌握了机器是如何在不全表扫描的情况下,通过“跨表接力滚雪球”的方式来验证它的。
现在,我们手里有了:
- 测试无损的武器(交集决定某个拆分表)
- 测试依赖保持的武器(跨表接力推导算法)
- 精简规则的武器(正则覆盖)
万事俱备,只欠东风! 接下来,我们要正式去写自动拆分出 BCNF 和 3NF 的代码算法了!
👉 我们将进入 P59 - P64:BCNF 分解算法。看看计算机是如何一步步把带有冗余的表,机械化地劈成完全没有函数冗余的 BCNF 小表的。 准备好执行手术了吗?
太棒了!前面我们已经学完了所有的数学推导工具,现在我们要把这些工具组装成一台全自动的“外科手术机器人”。
从 P59 到 P64,我们正式讲解 BCNF 的分解算法。这台机器人的任务是:拿到一张臃肿的烂表,自动扫描出违反 BCNF 的“肿瘤”(非主键决定的函数依赖),然后咔嚓一刀切成两张表,直到所有表都绝对纯洁为止!
我们分 3 个步骤来拆解这台机器人的工作原理:
第一步:机器人怎么做“术前扫描”?(防坑指南,P60-P61)
怎么让电脑判断一张表是不是在 BCNF 里? 根据定义:只要有任何一条规矩 $\alpha \rightarrow \beta$ 成立,且 $\alpha$ 不是超键,就是违规。
- 小白程序员的“偷懒扫描法” (P60): 偷懒的做法是,我只看老板给的那几条原始规矩(集合 $F$),如果没有违规的,我就当它是 BCNF 了。
- 致命翻车现场: 大表 $R(A, B, C, D, E)$。规矩 $F = { A \rightarrow B, BC \rightarrow D }$。 拆成两张小表:$R_1(A,B)$ 和 $R_2(A,C,D,E)$。 如果用偷懒法查 $R_2$:原规矩里没有任何一条能完全放在 $R_2$ 里,所以 $R_2$ 是 BCNF?错! 隐藏的雷:别忘了我们学过闭包推导!因为 $A \rightarrow B$,且 $BC \rightarrow D$,根据伪传递律可以推导出隐藏规矩:$AC \rightarrow D$! 这条隐藏规矩完美落在了 $R_2$ 里,而且 $AC$ 根本不是 $R_2$ 的超键!$R_2$ 违规了!
- 正确的“全身体检法” (P61): 对于拆出来的小表 $R_i$,必须针对它里面的所有属性组合 $\alpha$,去算一下属性的闭包 $\alpha^+$。看看它能决定的东西里,有没有落在小表内、但它自己又不是超键的情况。必须把隐藏的“肿瘤”也扫出来!
第二步:机器人的“手术切割”代码(P62 核心算法)
扫出肿瘤(违规的函数依赖)后,怎么切?这段“伪代码”非常优美,它是一个 while 循环:
1
2
3
4
5
6
7
8
9
1. 把最开始的那张大表扔进结果集 result 里。
2. 当(result 里还有任何一张表不符合 BCNF)时,一直循环:
3. 找到那张烂表 Ri,以及在它身上作乱的违规规矩 α → β
(注:α 和 β 不能有交集)。
4. 【咔嚓一刀,切成两半】:
第一半:α ∪ β (带头大哥和它的跟班独立建表)
第二半:Ri - β (原表把跟班删掉,留下带头大哥当联络外键)
5. 把切坏的烂表扔掉,把切出来的新表放回 result 里。
6. 循环结束。
【算法的两大铁律保证】:
- 绝对无损!(因为两张新表的交集就是 $\alpha$,而 $\alpha$ 在第一张新表里绝对是超键,完美符合无损分解公式!)
- 不保证依赖保持!(切的时候根本不管别的规矩死活,切断了概不负责。)
第三步:教科书级的完美手术演示(P63-P64 🚨 期末必看大题)
我们来亲自上一台手术台,处理一个超级臃肿的大学教务大表:
【病人资料】
- 超级大表 Class:包含
(course_id, title, dept_name, credits, sec_id, semester, year, building, room_number, capacity, time_slot_id)。 - 主键(候选键)是:
{course_id, sec_id, semester, year}(只有拿着这四个,才能锁定唯一的一节课)。
【第一次扫描与切割】 (P63)
- 发现肿瘤:
course_id $\rightarrow$ title, dept_name, credits(课程号决定课名、开课系、学分)。 - 违规原因:
course_id只是个局部属性,不是整张大表的超键,凭什么带小弟? - 执行切除(根据公式):
- 新表 1 (带走跟班):
course (course_id, title, dept_name, credits)$\implies$ 这张表完美达到 BCNF。 - 新表 2 (留下大哥):原表剔除课名、开课系、学分。变成
class_1 (course_id, sec_id, semester, year, building, room_number, capacity, time_slot_id)。
- 新表 1 (带走跟班):
【第二次扫描与切割】 (P64)
- 刚刚切出来的
class_1康复了吗?再扫! - 发现新肿瘤:
building, room_number $\rightarrow$ capacity(大楼和房间号决定了教室的容量)。 - 违规原因:在这张包含排课时间的表里,房间号显然不是超键(同一个房间一天要排好几节课),但它却私自决定了容量!这会导致极大的数据冗余。
- 执行切除(再次套公式):
- 新表 1 (带走跟班):
classroom (building, room_number, capacity)$\implies$ 教室表独立,完美 BCNF。 - 新表 2 (留下大哥):把容量剔除,变成
section (course_id, sec_id, semester, year, building, room_number, time_slot_id)$\implies$ 班次排课表,完美 BCNF。
- 新表 1 (带走跟班):
【手术成功】 原本包含 11 个属性、处处是冗余和更新异常的巨大怪物,经过两次 while 循环切割,最终变成了三张干净、清爽、职责分明的表:course、classroom、section。 它们全部符合 BCNF 标准,且每次切割都是无损的!
小结:
这就是 BCNF 分解算法的暴力美学:只要你不按规矩出牌(非超键决定别人),我就把你和你决定的人一刀切出去单独建表。一直切,切到没人敢违规为止。
但是,这位医生是个“手术狂人”,只管切除肿瘤,不管病人切完之后某些特定的经脉(依赖保持)会不会断。
为了解决这个医学难题,我们需要另一套更温和、更精密的“微创手术方案”。 👉 这就是接下来 P65 - P73 压轴登场的:3NF 分解算法 (3NF Synthesis Algorithm) 以及两者的终极 PK。
准备好见证既能消除大部分冗余,又能 100% 保持业务依赖的“3NF 魔法算法”了吗?
太棒了!前面我们看到,BCNF 像一个拿着大砍刀的“疯狂外科医生”,为了切除冗余,根本不管病人的神经(业务依赖)会不会被切断。
而在现实的工程应用中,失去“依赖保持”是极其致命的。为了保住神经,数据库专家们发明了一套极其温和、犹如“3D打印”一般的微创手术方案 —— 3NF 综合算法 (3NF Synthesis Algorithm)。
从 P65 到 P73,我们将见证这个能在多项式时间内完美兼顾“无损”与“依赖保持”的神级算法,并迎来 BCNF 与 3NF 的终极对决。
第一部分:3NF 凭什么这么牛? (P65 - P67)
1. 妥协的艺术 (P65)
- 痛点:BCNF 无法永远保证“依赖保持”。在数据库每次做
INSERT或UPDATE校验时,如果跨表验证,性能会崩溃。 - 3NF 的承诺:允许表里有一点点微小的冗余,但绝对保证无损分解,且绝对 100% 保持所有依赖!
2. 检验 3NF 是个噩梦 (P67)
- 我们在前面 P28 学过 3NF 的“免死金牌”(如果被决定的属性是候选键的一部分,就算合法)。
- 但问题来了:为了发这块免死金牌,计算机必须先找出这张表所有的候选键。而在数学上,找全一张表的所有候选键是一个 NP-Hard 问题(极其极其耗时)!
- 反转:虽然“检验”一张表是不是 3NF 很难,但奇妙的是,“直接构造”出一套符合 3NF 的完美表结构,却非常简单快捷(只需要多项式时间)!
第二部分:神级方案 —— 3NF 综合算法 (P68 - P69 🚨 核心必考)
BCNF 的算法是“自顶向下”的砍大表;而 3NF 的算法是“自底向上”的拼乐高(所以也叫 Synthesis 综合算法)。
【3NF 魔法四步曲】(请务必牢记,这 4 步极其机械化、无脑且好用):
- 第 1 步:挤干水分(求正则覆盖 $F_c$) 拿着老板给的冗杂规则库 $F$,运行我们之前学的瘦身算法,得到最干练、无废话的 $F_c$。
- 第 2 步:按图索骥,直接建表! 针对 $F_c$ 里的每一条规矩 $\alpha \rightarrow \beta$,直接用它们单独建一张表。表里的列就是 $\alpha \cup \beta$。 (潜台词:你不是要保持依赖吗?那我直接为每一条规矩量身定制一张专属小表,规矩全包裹在表内,依赖绝对断不了!)
- 第 3 步:强行打补丁(保无损) 检查一下你刚才建的那一堆小表。它们之中,有没有任何一张表,包含了原大表的“候选键”?
- 如果有,很好,跳过这一步。
- 如果没有,强行加一张新表! 这张新表里什么都不放,只放原大表的一个候选键。(这一步是为了保证最后的分解是无损分解,必须有主心骨来串联数据)。
- 第 4 步:大扫除(删子集) 看看你建出来的这一堆表,有没有一张表“完全被另一张大表包裹了”(即它是别人的子集)?如果有,把它删掉,清理冗余。
第三部分:教科书级 3NF 算法实战 (P70 - P71 满分演示)
让我们直接上手术台,按照上面的 4 步切一个真实例子:
- 超级大表:$R = (C, E, B, T)$ (C:客户, E:员工, B:分行, T:类型)
- 原始规矩 $F$:
C, E $\rightarrow$ B, TE $\rightarrow$ BC, B $\rightarrow$ E
【第 1 步:求正则覆盖 $F_c$】 (P70)
- 这里直接给结论:我们发现规则1里的
B是右侧的废话(因为有了 E 就自动有 B 了,被规则2涵盖)。 - 瘦身后的 $F_c$ =
{ C, E $\rightarrow$ T ; E $\rightarrow$ B ; C, B $\rightarrow$ E }。
【第 2 步:按图索骥建表】 (P71) 针对 $F_c$ 里的 3 条规矩,直接无脑建 3 张表:
- 表 1:
(C, E, T) - 表 2:
(E, B) - 表 3:
(C, B, E)
【第 3 步:打补丁保无损】 (P71)
- 求一下原表 $R$ 的候选键,发现
{C, E}就能决定一切,所以{C, E}就是候选键。 - 看看刚才建的表里,表 1
(C, E, T)已经包含了{C, E}。 - 太好了!补丁不用打了!
【第 4 步:大扫除】 (P71)
- 审视这三张表:
(C, E, T),(E, B),(C, B, E)。 - 敏锐地发现:表 2
(E, B),完完全全是表 3(C, B, E)的子集! 留着纯属浪费空间。 - 删掉表 2!
【大结局】 最终的 3NF 分解方案出炉:表 1 (C, E, T) 和 表 3 (C, B, E)。 (完美保留了所有业务规则,而且连接无损!)
第四部分:终极 PK 与 现实世界的妥协 (P72 - P73)
学完了 BCNF 和 3NF 两个算法,P72 和 P73 进行了全章最核心的价值观总结。
在理论上,一个完美的数据库必须同时满足三大目标 (P73):
- 达到 BCNF 级别(一丝冗余都没有)。
- 无损连接(数据不失真)。
- 依赖保持(保证极速的业务校验)。
但是,现实是残酷的: 我们已经证明过,这三个目标有时候是“不可能三角”!你必须在“牺牲依赖保持”和“忍受轻微冗余 (3NF)”之间做抉择。
👉 现实工程界(如 MySQL, Oracle 等)做出了什么选择? (P73 🚨 极其贴近实战的结论) 工业界 99% 选择了 3NF!
为什么放弃 BCNF? 因为 SQL 语言的底层设计缺陷。在现有的 SQL 数据库中,只有一种方法能高效地验证函数依赖:设为主键 (Primary Key) 或 唯一约束 (Unique)。 如果某个依赖被 BCNF 切到了不同的表里(跨表),你想在 SQL 里验证它,就必须写极其复杂的 Assertion(断言)或 Trigger(触发器)。这不仅写起来恶心,而且每次存数据都会导致极其恐怖的性能开销,大部分现代数据库甚至直接不支持 Assertion!
所以,宁可表里留一点点冗余数据(3NF),也必须把依赖死死地锁在同一张表里!这就是数据库架构师在实战中的最终妥协。
小结:
到这里,第 7 章的绝对核心内容(函数依赖、求闭包、求正则覆盖、BCNF分解、3NF合成)已经全部彻底结束了! 这一套流程能拿满绝大多数大学数据库考试中 80% 的大题分数。
而在本章最后的 P74 - P85,是为了对付一种比“函数依赖”更刁钻、更奇葩的病症:“多值依赖 (MVD) 与 第四范式 (4NF)”。还记得那个因为有两个小孩、两个电话号码,导致数据发生恐怖交叉组合的例子吗?
你准备好进入这个被称为“全书最难懂”的进阶理论,看看如何用 4NF 斩妖除魔了吗?
太棒了!现在我们终于要面对数据库规范化理论里的“隐藏大 Boss”了。
在前面我们一直认为,只要用 BCNF 消灭了所有“函数依赖 (FD)”带来的冗余,数据库就完美了。但我们在 P33-P35 见过一个极度恐怖的反例:BCNF 查不出它有病,但它每一次新增数据,都会因为“交叉组合”而产生大量的冗余。
从 P74 到 P85,数据库专家们发明了新的检测仪器:多值依赖 (MVDs),以及专门治这个病的解药:第四范式 (4NF)。
因为这部分数学定义特别晦涩,我们完全抛开死板的公式,用最直白的大白话分 4 步把它打通关:
第一步:找出 BCNF 漏诊的“怪病”(多值依赖的直观体现,P75)
让我们再看一眼那个让 BCNF 颜面扫地的经典烂表(P75):
- 表名:
inst_info (ID, child_name, phone_number) - 数据场景:爱因斯坦老师(ID: 99999)有两个孩子:David, William;他也有两部手机:1234, 4321。
怪病发作: 孩子和手机号,这两个属性完全独立,八竿子打不着。但是!因为它们被强行揉在同一张表里,为了保证信息完整,数据库被逼着穷举了它们所有的排列组合(笛卡尔积):
- (99999, David, 1234)
- (99999, David, 4321)
- (99999, William, 1234)
- (99999, William, 4321)
明明只有 2 个孩子和 2 个手机号(总共 4 条信息),在表里却膨胀成了 4 行(12 个格子)。如果爱因斯坦生了 3 个孩子、办了 3 个手机号,这张表就会瞬间爆炸成 9 行!这就是比函数依赖更可怕的数据冗余。
第二步:给怪病命名 —— 什么是“多值依赖 (MVD)”?(P76 - P79)
既然它是病,我们就得给它下定义。
- 之前学的函数依赖(一夫一妻制):
X $\rightarrow$ Y(X 唯一决定一个 Y)。 - 现在的多值依赖(一夫多妻制,且互不干涉):
X $\rightarrow\rightarrow$ Y(注意是双箭头!) 读作“X 多值决定 Y”。
👉 【如何用大白话理解 P76-P77 那个极其复杂的数学定义?】 多值依赖的本质就是“完全独立、必须交叉”。 定义是这么说的:如果你在这张表里看到了两行数据,比如 (David, 1234) 和 (William, 4321)。只要存在 ID $\rightarrow\rightarrow$ 孩子,且 ID $\rightarrow\rightarrow$ 手机,那么这张表里就必须强制存在它们交叉交换后的数据,即必须也有 (David, 4321) 和 (William, 1234)!
如果一张表具有这种“只要有一组,就必须有全套交叉组合”的倒霉体质,我们就说这张表存在多值依赖 (MVD)。 (在 P79 的例子里,就是 ID $\rightarrow\rightarrow$ child_name 且 ID $\rightarrow\rightarrow$ phone_number )。
👉 【一个重要推论 (P81)】: 所有的单值依赖(函数依赖 FD $\alpha \rightarrow \beta$)本质上也是多值依赖。你可以把它理解为“退化版”的多值依赖(X 决定了一个集合,只不过这个集合里恰好只有一个元素罢了)。
第三步:治病解药 —— 第四范式 (4NF) 隆重登场(P82)
既然测出病因是由于非法的多值依赖导致的,那我们就定个规矩消灭它!这就是 第四范式 (4NF)。
4NF 的判定标准极其简单粗暴(和 BCNF 简直像亲兄弟): 对于表里任何一个非平凡的多值依赖 X $\rightarrow\rightarrow$ Y,X 必须是整张表的超键 (Superkey)!
(大白话:如果你想在表里搞一对多的集合映射,没问题,但你必须是这整张表唯一的大哥(超键)!如果你像前面的 ID 一样只是个局部属性(整张表的超键其实是 ID + 孩子 + 手机 的全集),那你绝对不允许带两个相互独立的多值小弟!)
结论:只要满足 4NF,就一定满足 BCNF。4NF 是对 BCNF 的进一步精神升华。
第四步:4NF 手术刀 —— 拆表算法与实战(P84 - P85)
如果一张表违反了 4NF 怎么办? P84 给出的拆表算法,竟然和 BCNF 的算法一模一样! 也是一个 while 循环。
【切割公式】: 发现违规的多值依赖:$\alpha \rightarrow\rightarrow \beta$。咔嚓一刀切成两张表:
- 新表 1(带走跟班):$\alpha \cup \beta$
- 新表 2(留下大哥):$R_i - \beta$ (原表把跟班删了,留下带头大哥当联络外键)
👉 【神级实战:拯救爱因斯坦的表】
- 违规点:
ID $\rightarrow\rightarrow$ child_name。(ID不是原表的超键)。 - 手起刀落:
- 表 1:
ID带着child_name走。建表inst_child (ID, child_name)。 - 表 2:原表删掉
child_name。只剩下inst_phone (ID, phone_number)。
- 表 1:
- 验收成果:
- 原来冗杂的 4 行数据,现在变成了:孩子表里 2 行,手机表里 2 行。
- 再加 100 个手机号,也绝不会影响孩子表。完美的 4NF,彻底杀死了交叉冗余,且 100% 无损连接!
总结:如何应对这部分的考试?
说实话,多值依赖 (MVD) 的数学推导(P85那种超级长的推导)由于太复杂且缺乏完备的公理,在现实的工业界极少直接用纯理论去算。 但在考试中,你只要把握住一个核心直觉: 如果在同一张表里,你发现了两个“一对多”的属性(比如一个人有多个特长、又有多个爱好),并且这两个属性之间毛关系都没有,那么这张表绝对违反了 4NF!唯一的解法就是:把它们分成两张表,分别和那个主体绑定(一张表存人与特长,一张表存人与爱好)。
到这里,规范化理论这座大山我们已经完全登顶了!我们见识了:1NF、3NF、BCNF、4NF。 (甚至 P87 还提了一嘴更高冷的 5NF(PJNF)和 终极范式(DKNF),但由于数学上太难推理,连课本都承认它们“rarely used 几乎没用过”)。
第 7 章只剩下最后一点收尾工作了:P86 - P93:其他设计问题(性能反规范化、交叉表、时间数据建模)。以及最后补充的 1NF 的概念 (P100)。
你想一股作气,把第 7 章彻底完结掉吗?我们马上就可以进入更接地气的“第 8 章:复杂数据与 NoSQL 世界”了!
太棒了!我们现在终于从极其烧脑的数学推导(范式理论)中解脱出来了。
从 P86 到 P93,课程的画风突然一转,回到了真实的工程世界。 在现实中,完美的数学理论往往会撞上性能瓶颈或者奇葩的业务需求。这部分内容,就像是一位资深数据库架构师在给你传授他多年踩坑总结出的“实战心法”。
这 8 页虽然标题叫“杂项”,但其实包含了 4 个极其重要的实战场景,我们一个个来看:
实战心法一:画图(E-R)与算题(规范化)到底是什么关系?(P88 - P89)
你可能会问:既然第7章的“规范化算法”这么牛,能自动拆表,那我们第6章辛辛苦苦学画 E-R 图还有什么用?干脆全扔进一个“万能大表 (Universal relation)”,让电脑自己去拆不就好了?
- 架构师的回答 (P89): 如果你在第6章把 E-R 图画得非常完美,那你生成的表天生就是 BCNF 或 3NF 的,根本不需要第7章的算法来擦屁股!
- 什么时候才需要算法救场? 当你的 E-R 图画得很“菜”的时候。比如,你把“系名 (dept_name)”和“系大楼 (building)”当成了员工 (employee) 的属性。结果转换成表后,出现了
dept_name $\rightarrow$ building的依赖。这时候,第7章的规范化算法就会跳出来说:“你这里冗余了!切掉,把部门独立成一个实体!”
实战心法二:反规范化 (Denormalization) —— 为了性能,向冗余妥协!(P90 🚨 工业界最常见)
前大半章我们一直像疯子一样消灭冗余,但到了 P90,作者告诉你:有时候,我们必须主动引入冗余!
- 痛点:高度规范化的数据库,表被拆得非常细(比如“课程表”和“先修课表”是分开的)。当用户点开一个页面想要查看完整信息时,数据库在底层必须做大量的
JOIN(多表连接) 操作。如果并发量极高(比如选课系统),JOIN会让服务器 CPU 直接冒烟宕机。 - 黑客客做法:反规范化 (Denormalization) 为了查询的极致速度,我们主动打破 3NF 或 BCNF,把“先修课”的信息强行塞回到“课程表”里。
- 代价:浪费硬盘空间;更新数据时很容易出错(因为存在冗余)。程序员必须在写代码时格外小心,手动维护数据的一致性。
- 高级替代方案:物化视图 (Materialized View) 现代数据库给了一个两全其美的折中方案:底层依然用完美切分的 3NF 表来存,但利用数据库自带的视图功能,预先算好一个拼合后的“影子表(物化视图)”。查询快,且出错风险低。
实战心法三:Excel 思维的毒瘤 —— 交叉表 (Crosstabs, P91)
在实际开发中,有些表虽然能通过 BCNF 的严苛数学审查,但却会让写 SQL 的程序员想打人。
- 奇葩设计展示: 假设你要存公司每年的利润。小白程序员把表设计成了这样:
公司利润表 (公司ID, 2004年利润, 2005年利润, 2006年利润) - 它符合 BCNF 吗? 符合!公司 ID 作为主键,完美决定后面所有的列。
- 这设计烂在哪? 如果到了 2027 年怎么办?你必须去修改数据库的表结构 (ALTER TABLE),给它新增一列
2027年利润!这在生产环境是极度危险的。而且你想用 SQL 写一句SUM()算出总利润,根本写不出来! - 警示:这就是典型的“用做 Excel 表格的思维(交叉表 Crosstab)来做关系型数据库”。
- 正确做法:必须设计成竖表:
公司利润表 (公司ID, 年份, 利润金额)。一行只存一年的数据,增加年份只需要新增一行 (INSERT),永远不需要改表结构。
实战心法四:时间旅行的陷阱 —— 时态数据建模 (Temporal Data, P92 - P93)
现实世界是随着时间流逝而变化的,这就给我们的“函数依赖”理论带来了巨大的麻烦。
- 时间摧毁了规则 (P92): 我们之前笃信:
老师 ID $\rightarrow$ 家庭住址(知道老师 ID 就能确定他住哪)。 但是!如果这个老师在 2024 年搬家了呢?在含有历史数据的表里,同一个ID就会对应两个不同的地址。函数依赖瞬间崩溃! - 如何抢救?(P93 工业界解法): 现在的通用做法是,给这种历史表加上两个雷打不动的列:
start_time(生效时间) 和end_time(失效时间)。- 约束条件:对于同一个老师,他的任意两条住址记录,时间区间绝对不能重叠 (Overlap)!
- 外键的麻烦: 比如学生的“成绩单”记录了选修的课。如果课程后来改名了、或者学分变了,成绩单上的外键还能指望吗?所以,涉及历史版本的引用,必须带上“时间戳”,指定引用的是当时那个时间点的数据快照 (Snapshot)。
🎉 第 7 章终局总结
至此,第 7 章《关系数据库设计与规范化》的正文内容我们已经彻底打通关了!
回顾你拿下的成就:
- 数学底座:掌握了函数依赖 ($X \rightarrow Y$)、阿姆斯特朗公理、求闭包滚雪球算法。
- 手术工具:学会了如何给臃肿的规则库挤水分(求正则覆盖 $F_c$)。
- 两大核心范式:
- BCNF:眼里不揉沙子,手起刀落切除所有函数冗余(但可能切断依赖)。
- 3NF:现实工程的妥协,允许极其微小的冗余,通过 4 步综合算法,保住所有业务依赖。
- 进阶恶魔:用 4NF 处理毫无关联的多值属性引发的恐怖笛卡尔积冗余。
- 工程智慧:懂得了何时该向性能妥协(反规范化),懂得了避开跨表和时间设计的坑。
(注:P100-101 提到了第一范式 1NF,其实就是规定表里的任何一个格子只能存不可再分的单一数据,这是所有关系型数据库的最基本要求,我们在拆多值属性时其实就已经默认遵循了。)
接下来是一片完全不同的新天地! 世界在发展,互联网产生的数据不再是死板的二维表。当我们遇到淘宝的商品属性、朋友圈的复杂社交网络、Google 的海量网页时,二维表就不管用了。
我们要开启 第 8 章:复杂数据类型 (Complex Data Types) 了。我们将抛开死板的 SQL 表,去拥抱 JSON、XML、RDF知识图谱、多维数组 以及 空间/地理信息数据。
你准备好进入这个自由、灵活的 NoSQL 与复杂结构的世界了吗?我们需要先从哪个模块开始?是 JSON/XML 半结构化数据,还是让人兴奋的 PageRank 算法?
太明智了!第7章那些繁琐的数学证明在实际开发中确实很少手算。现在,让我们深呼吸,彻底抛开之前那些死板、严苛的二维表和范式规则,迎接自由的现代数据世界!
欢迎来到 第8章:复杂数据类型 (Complex Data Types)。 前几章我们学的关系型数据库(如 MySQL)就像是定制的“格子收纳盒”,每个格子只能放一种规定的东西(第一范式 1NF)。但现实世界的数据往往是极其奔放的,比如发朋友圈、物联网传感器数据、气象卫星图。
从 P1 到 P5,我们先来探讨:当我们嫌弃“二维表”太死板时,我们该用什么来存数据?—— 半结构化数据与数组。
1. 为什么我们需要“半结构化数据”? (P3 痛点与动机)
在前面我们学过,传统关系模型的硬性规定是:属性必须是原子性的(第一范式,不可再分)。
- 痛点 1:表结构 (Schema) 频繁变动。假设你要存用户的“兴趣爱好”。如果用二维表,你得专门建一张爱好表,再用外键连起来。如果爱好总是变,表结构修改极其痛苦,这叫“杀鸡用牛刀 (Overkill)”。
- 痛点 2:前后端数据交互的阻碍。现在是移动互联网时代,你的手机 App 或网页前端 (JavaScript) 经常需要向后端一次性拉取一个复杂的嵌套数据包。如果后端只吐出死板的二维表,前端解析起来极其费劲。
解决方案:半结构化数据 (Semi-Structured Data) 它允许数据有结构,但不强制所有人长得一模一样。目前统治 Web 界的两大标准就是 JSON 和 XML(后面几页会细讲)。
2. 半结构化数据的“两大魔法特性” (P4 核心概念)
为什么它这么灵活?因为它打破了传统数据库的两大禁忌:
特性一:极度灵活的表结构 (Flexible Schema)
在传统数据库里,一行数据有几列是建表时写死的。但在半结构化模型里:
- 宽列表示法 (Wide column):允许每一行(每个 Tuple)拥有完全不同的属性!比如张三的记录有“微信号”,李四的记录有“抖音号”,随时随地可以增加新列,不需要
ALTER TABLE。 - 稀疏列表示法 (Sparse column):假设系统里总共有 1000 种可能的属性,但每个具体的记录可能只用到其中 5 个。传统数据库会产生 995 个 Null(极度浪费),而稀疏列表只会存储那 5 个真实存在的值。
特性二:允许“套娃”的多值数据类型 (Multivalued data types)
打破第一范式,允许一个格子里塞复杂的东西:
- 集合/多重集 (Sets/Multisets):比如直接把兴趣存成一个集合
{篮球, 烹饪, 动漫},放在一个格子里。 - 键值映射 (Key-value map):这是 NoSQL 数据库的核心。一个属性里面还可以是一个字典。比如:
{(品牌, 苹果), (型号, MacBook), (内存, 16G)}。你可以随时通过put添加、get获取、delete删除键值对。
3. 专为科学与监控而生的“数组” (Arrays, P4 - P5)
在物联网 (IoT) 和科学计算领域,有一种极度重要的数据类型:数组 (Arrays)。
- 传统数据库的灾难: 假设你是一个气象站,每秒记录一次温度。如果存在 MySQL 里,你得存成:
(1秒, 5度), (2秒, 8度), (3秒, 9度), (4秒, 11度)。 这不仅浪费了大量空间存“时间键”,而且查询极其低效。 - 数组的降维打击: 因为采集是定时的(自带顺序),我们完全可以丢掉时间键,直接存一个纯粹的数组:
[5, 8, 9, 11]! - 非第一范式 (NFNF - Non First-Normal-Form): 现在的数据库(如 PostgreSQL, Oracle)已经完全支持把这种“多值数组”直接作为一个字段存进去,这叫 NFNF 数据模型。
- 专业的数组数据库 (Array database): 为了应对每天产生 TB 级别的卫星遥感或传感器数组数据,业界开发了专门的数据库(如
SciDB,Oracle GeoRaster,PostGIS)。它们提供极致的数据压缩和专门针对数组计算的查询语言扩展。
小结:
前 5 页给我们打开了新世界的大门:
- 因为 Web 和科学计算的发展,我们需要脱离死板的二维表。
- 我们引入了允许随便加列、允许格子里存“集合”和“字典 (Map)”的半结构化数据。
- 针对时序和科学数据,我们直接把数组 (Arrays) 当作一等公民存进数据库。
既然提到了 Web 时代最主流的半结构化格式,那就不得不提 JSON 和 XML 了。 👉 准备好进入 P6 - P11:JSON 与 XML 的嵌套数据世界 了吗?我们来看看平时手机 App 是如何和服务器交换数据的。
太棒了!前面我们提到,为了适应 Web 时代,我们需要打破二维表的束缚,拥抱可以“套娃”的数据格式。
从 P6 到 P11,课程正式介绍了统治当今互联网数据交换的两大霸主:JSON (当红炸子鸡) 和 XML (老牌老将)。它们都是用来处理嵌套/层次化数据 (Nested / Hierarchical Data) 的绝对主力。
我们把这部分拆成两大块来对比学习:
第一块:当今 Web 世界的绝对统治者 —— JSON (P7 - P8)
1. JSON 长什么样?(P7 语法解剖)
- 全称:JavaScript Object Notation(起初是给前端 JS 用的,后来因为太好用,统一了全网)。
- 特点:纯文本,轻量级,极度适合手机 App 和服务器之间交换数据。
- 核心语法(看 P7 的代码例子):
- 大括号
{ }代表“对象 (Object)”:本质上就是一个键值对字典 (Key-value map)。比如"ID": "22222"。 - 方括号
[ ]代表“数组 (Array)”:也是键值对的一种,只不过键是索引序号(0, 1, 2…)。 - 无限套娃:你看 P7 的例子,
"name"的值竟然又是一个大括号(包含了名和姓);"children"的值是一个方括号(数组),数组里面又装着两个大括号(具体的孩子信息)。这种嵌套能力,是传统关系型表根本做不到的!
- 大括号
2. 传统数据库如何“收编” JSON?(P8 🚨 实战考点) 现在的关系型数据库(如 PostgreSQL, MySQL)早已向 JSON 低头,提供了极其强大的 SQL 扩展支持:
- 专属数据类型:你可以直接建一列,类型就叫
JSON,把那一长串套娃文本直接塞进去。 - 路径表达式 (Path Expressions):存进去怎么查?数据库发明了箭头语法。比如
V -> ID或者v.ID,SQL 会像剥洋葱一样,直接钻进 JSON 内部,把那个特定的值揪出来! - 数据互相转换:
- 把关系表变成 JSON:用
json.build_object(...)函数。 - 把多行数据聚合成一个 JSON 数组:用
json_agg聚合函数(PostgreSQL特有)。
- 把关系表变成 JSON:用
3. JSON 的致命弱点与解药 (P8)
- 弱点:JSON 太“啰嗦 (Verbose)”了!那些大量的双引号、大括号、字段名在每次传输和存储时都占据了巨大的空间。
- 解药:BSON (Binary JSON)。在底层存储时(比如大名鼎鼎的 MongoDB),会把 JSON 压缩成二进制格式,大大提高存储和读取效率。
第二块:自带说明书的老将 —— XML (P9 - P11)
1. XML 长什么样?(P9 - P10)
- 全称:Extensible Markup Language(可扩展标记语言)。在 JSON 火起来之前,全网的数据交换全靠它。
- 特点:用标签 (Tags) 来包裹数据。长得非常像网页的 HTML 代码。
- 核心语法(看 P9、P10 例子):
- 由一对对标签组成:
<标签名> 内容 </标签名>。比如<course id> CS-101 </course id>。 - 自我描述性 (Self-documenting):XML 最大的优点是“见名知意”。你只要看标签名,连表结构说明书都不用看,就能猜出里面的数据是什么意思。
- 同样支持完美的层次嵌套 (Hierarchical):比如
<purchase order>里面包裹着<purchaser>(买家)和<supplier>(卖家),买家里面又包裹着<name>和<address>。
- 由一对对标签组成:
2. 传统数据库如何操作 XML?(P11)
- 查询语言 XQuery:当年为了专门查询嵌套的 XML 树,专家们发明了一门叫 XQuery 的语言。但 PPT 无情地指出现状:“目前已不太常用了 (Not widely used currently)”,因为都被 JSON 抢走风头了。
- SQL 支持:和 JSON 一样,现代 SQL 也支持把 XML 存进特殊的列中,并且支持用“路径表达式 (Path expressions)”(比如 XPath)去一层层钻取提取数据。
小结与对比:
- JSON 就像是极简主义者,用最少的符号(
{}和[])完成了最复杂的嵌套,深得现代程序员的喜爱。 - XML 就像是严谨的老学究,必须有头有尾地写一堆繁琐的标签标签。虽然冗长,但在某些传统的大型企业金融级系统里,依然在稳如泰山地运行着。
- 现代数据库的胸怀:无论是 MySQL 还是 PostgreSQL,都不再死守“关系表”,它们已经完美集成了 JSON 和 XML 类型,并且允许你用 SQL 配合“路径箭头”去查询里面的套娃数据。
处理完了互联网应用层面的格式(JSON/XML),接下来我们要进入一个极具“人工智能 (AI)”色彩的领域: 👉 P12 - P16:知识表示 (Knowledge Representation) 与 知识图谱。
当我们要把全人类百科全书的知识塞进数据库时,既不用表,也不用 JSON,而是用一种叫三元组 (RDF) 的神奇格式。准备好看看怎么用数据库来表示“华盛顿是美国的首都”了吗?
太棒了!我们现在画风突变,从普通程序员的视角,跨入到了人工智能 (AI) 的初级领域。
当我们要把全人类的百科知识(比如“华盛顿是美国的首都”、“张三教了数据库这门课”)塞进电脑时,关系型二维表和嵌套的 JSON 都显得极其笨重。
从 P12 到 P16,专家们引入了一种终极灵活的格式:RDF(资源描述框架)与知识图谱 (Knowledge Graph)。
我们分 4 步来解密这个能存储人类知识的“超级大脑”:
第一步:万物皆可“三元组” (Triples, P12 & P14)
表示人类知识有一个长期的 AI 目标。怎么存最合理? 科学家发现,世界上绝大多数的事实,都可以用一句最简单的语法来概括:主语 + 谓语 + 宾语 (Subject, Predicate, Object)。 在数据库里,这被称为三元组 (Triples)。这种格式的全称叫做 RDF (Resource Description Format)。
👉 【大白话例子】:
- 事实 1:猛龙队赢了2019年NBA。(
NBA-2019,winner,Raptors) - 事实 2:华盛顿是美国的首都。(
Washington-DC,capital-of,USA) - 事实 3:华盛顿的人口是 620万。(
Washington-DC,population,6200000)
RDF 的降维打击:没有 Schema! 在传统数据库里,你需要先建表、定列名。但在 RDF 里,根本不需要固定的表结构 (Flexible schema)! 不管是存属性(华盛顿,人口,620万),还是存关系(华盛顿,首都,美国),全部用这同一个“三元组”格式一股脑儿地砸进数据库里即可。(看看 P14 满屏的三元组列表,就像是人类在陈述一个又一个的事实)。
第二步:将三元组连成“知识图谱” (Knowledge Graph, P13)
三元组如果只是一行行枯燥的文字,看着太难受了。但如果你把它画出来呢?
- 主语和宾语 变成图上的 节点 (Nodes)。
- 谓语 变成节点之间的 带箭头连线 (Edges)。
👉 【看 P13 极其直观的网络图】: 无数个散落的三元组互相交织,最终形成了一张庞大的蜘蛛网! 比如图上的:节点 10101 伸出一条线叫 name 指向 Srinivasan,又伸出一条线叫 teaches 指向节点 sec1,sec1 又连着 CS-101。 这张能让机器沿着线到处爬行、做逻辑推理的庞大网络,就是大名鼎鼎的“知识图谱 (Knowledge graph)”!
第三步:怎么查询知识图谱?遇见 SPARQL (P15 🚨 重点语法)
用 SQL 是查不了这种网络的。为了专门对付 RDF,诞生了一门超强查询语言:SPARQL。
它的核心思想是:玩拼图匹配 (Pattern Matching)! 只要你在词的前面加上一个问号 ?,它就是一个未知数(变量)。
👉 【看 P15 精彩的查询代码】: 假设你想问:“教《计算机科学导论》这门课的老师,叫什么名字?” 这在 SQL 里要写好几个 Join,而在 SPARQL 里,你只需要描述这几条线连在一起的形状:
1
2
3
4
5
6
7
select ?name
where {
?cid title "Intro. to Computer Science" . # 找一门课(代号?cid),它的标题叫"计算机导论"
?sid course ?cid . # 找一个班次(?sid),它属于上面那门课(?cid)
?id takes ?sid . # 找一个人(?id),他参加了上面那个班次(?sid)
?id name ?name . # 查出这个人(?id)的名字,命名为 ?name 输出!
}
你看!数据库会拿着这个“形状”去知识图谱里“套”,套中了,就把打问号 ? 的值返回给你。它还支持聚合、子查询,甚至能沿着图的路径无限往下挖(Transitive closure,比如查某人的祖宗十八代)。
第四步:三元组的阿喀琉斯之踵,多元关系怎么存?(P16 梦幻联动)
三元组非常优美,但它有一个致命缺陷:它只能表达二元关系(两两对应)! 如果遇到多元关系 (n-ary relationships) 怎么办?
- 痛点:比如事实“奥巴马从2008年到2016年担任美国总统”。这牵扯到“奥巴马、总统、美国、2008、2016”足足 5 个元素,三元组塞不进去啊!
- 解法 1:造个假人(梦回第6章 P70!) 这和我们第6章学过的“把三元联系强行拆成二元联系”的方法一模一样! 创建一个名叫
e1的虚拟节点(代表“这次总统任期事件”)。 然后拆成一堆三元组:(e1, 是什么, 奥巴马), (e1, 哪个国家, 美国), (e1, 开始年, 2008), (e1, 结束年, 2016)。完美化解! - 解法 2:升维打击(用四元组 Quads) 别用三元组了,加一个维度“语境 (Context)”,变成四元组。
👉 【现实世界的影响力】: PPT 结尾提到了几个如雷贯耳的名字:DBPedia, WikiData, Yago, Freebase。 这些全都是全球最大的开源知识图谱项目。谷歌的搜索引擎之所以这么聪明(比如你搜奥巴马,右边会直接弹出一个人物关系卡片),背后就是一堆巨大的 RDF 图谱和“链接开放数据 (Linked open data)”在支撑。
小结:
这几页给我们展示了传统二维表之外的星辰大海。 如果你的业务是“高度网状化、实体之间关系极其复杂且未知”(比如刑侦图谱、企业股权穿透、搜索引擎知识库),千万别用死板的关系型数据库,RDF + 知识图谱 才是降维打击的武器!
处理完了代表 AI 和大数据的图谱数据,接下来我们要回归到最传统的软件工程领域了。 👉 我们将进入 P17 - P21:面向对象数据库 (Object Orientation)。
在 Java/C++ 里,一切皆“对象 (Object)”。如何让数据库也能拥有面向对象的特性(比如自定义类型、继承)呢?我们要一鼓作气拿下这个模块吗?
太棒了!我们现在进入一个对软件开发工程师(程序员)极其友好的领域。
从 P17 到 P21,课程聚焦于 面向对象数据库 (Object Orientation)。 如果说前面的 JSON 和 RDF 是为了应对 Web 和 AI 数据,那么这部分内容则是为了解决程序员长久以来的一个“世纪大痛点”。
我们分 3 步来解开这段数据库与编程语言的爱恨情仇:
第一步:世纪大痛点 —— “阻抗不匹配” (P17)
痛点是什么?
- 程序员的世界 (Java/C++/Python):万物皆对象 (Object)。数据都是有层级的、可以互相嵌套的、而且可以搞“继承”(比如子类继承父类)。这就像是立体的 3D 乐高模型。
- 数据库的世界 (关系型 SQL):万物皆二维表 (Table)。数据必须被拍扁成一个个格子,不允许嵌套,也没有继承的概念。这就像是平面的 2D 图纸。
每次程序员要把内存里的“对象”存进数据库,或者从数据库里读数据还原成“对象”,都必须写极其恶心的转化代码。这在软件工程里被称为“阻抗不匹配 (Impedance Mismatch)”。
怎么解决?P17 给出了 3 条历史发展路线:
- 造新轮子:直接发明一种原生的面向对象数据库 (Object-oriented DB)。(注:历史上搞过,但因为查数据太慢,基本死绝了)。
- 给 SQL 升级(折中派):在传统的二维表上,强行加入面向对象的特性!这就是 对象-关系数据库 (Object-Relational DB)。
- 请个翻译官(现实派):数据库还是老老实实当二维表,但在程序和数据库之间加一层“翻译软件”,这就是大名鼎鼎的 ORM 框架。
第二步:SQL 的终极进化 —— 对象-关系数据库 (P18 - P20)
这条路线是让传统数据库(如 PostgreSQL, Oracle)直接拥有 Java 那样的面向对象能力。看看 SQL 语法被魔改成了什么神仙样子:
1. 自定义类型与表类型 (P18) 以前建表只能用 int, varchar。现在你可以像写 Java class 一样自己造类型了!
- 先造一个“人”的类型:
create type Person (ID varchar, name varchar...); - 然后再用这个类型去建表:
create table people of Person;
2. 支持“继承” (Inheritance, P19 🚨 酷炫特性) 在第6章我们学了 E-R 图里的特化/泛化(父子类),现在可以用 SQL 原生实现了!
- 类型的继承:你可以写
create type Student under Person。学生不仅有学号,还无条件继承了Person里的所有属性! - 表的继承:PostgreSQL 里可以直接写
create table students (...) inherits people;。查询父表people的时候,会自动把子表students里的数据也一并查出来,完美复刻多态!
3. 用“指针 (Reference)”代替“外键” (P20) 在传统数据库里,表和表之间通过“外键”关联,查询必须写又臭又长的 JOIN。 但在对象数据库里,你可以直接存一个对象的指针(引用 Ref)!
- 建表时使用:
head ref(Person) scope people。部门表里的“系主任”这一列,直接存储了一个指向Person表里某个具体人的“内存地址/引用”。 - 查询时的降维打击:不用写
JOIN了!你可以像写 C++ 代码一样,直接用箭头语法->拿数据:select head->name, head->address from departments;数据库会自动顺着指针帮你把系主任的名字拿出来!
第三步:工业界的最终赢家 —— ORM 框架 (P21 🚨 真实开发必备)
虽然上面给 SQL 加“继承”和“指针”看起来很酷,但在目前的真实工业界中,绝大多数企业(如阿里、腾讯)根本不用上面那套复杂的 SQL 语法。
为什么?因为数据库就该干好存储的本职工作,把复杂的逻辑留给代码! 于是,对象-关系映射 (Object-Relational Mapping, 简称 ORM) 成了当今软件开发的绝对霸主。
- ORM 是什么? 它是一个超级“翻译官”中间件。程序员在 Java 或 Python 里只管定义类(Class)和对象(Object)。
- ORM 的魔法:
- 你一运行程序,ORM 会自动帮你生成建表语句发给数据库。
- 你在代码里
student.save(),ORM 会自动翻译成INSERT INTO...发给数据库。 - 你在代码里删对象,它自动翻译成
DELETE。 - 你用代码语法查询,它帮你翻译出多表
JOIN的复杂 SQL,并把查回来的结果直接打包成对象扔给你!
- 业界鼎鼎大名的 ORM 框架(P21 底部点名):
- Java 界的王牌:Hibernate
- Python 界的霸主:Django ORM
小结:
P17-P21 给我们展示了数据库向程序员妥协的过程。
- 学术界和数据库厂商试图通过魔改 SQL 语法(
Type,Inherits,->)来迎合面向对象。 - 但工程界最终用脚投票,选择了 ORM 框架,在不改变底层二维表的前提下,用“翻译官”完美化解了阻抗不匹配。
到这里,面向对象数据处理我们也拿下了! 第 8 章只剩下最后两个特别有趣的实战应用领域:
- P22 - P26:文本数据与搜索引擎 (TF-IDF 与 PageRank) —— 带你揭秘 Google 和百度的核心算法。
- P27 - P37:空间与地理数据 (GIS) —— 看看高德地图、滴滴打车是怎么在数据库里存地理位置的。
你想先去探索搜索引擎的秘密,还是看看地图数据是怎么玩的?我们可以一口气把这章收尾!
太棒了!我们现在要跨入一个大家每天都在用,但极其神秘的领域:搜索引擎背后的底层算法。
从 P22 到 P26,课程探讨的是如何处理非结构化数据(纯文本 Textual Data)。 在关系型二维表里,我们用 WHERE ID = 1 就能精准找到数据;但在汪洋大海般的纯文本(如全网网页、新闻文章)中,当你搜一个关键词时,动辄搜出几百万个结果。
系统到底该把哪篇文章排在第一名? 这几页直接为你揭秘了 TF-IDF 算法 和 谷歌发家的绝对核心机密 —— PageRank 算法。
我们分 3 个核心关卡来解密搜索技术:
第一关:基于文章内容的算分神器 —— TF-IDF (P22 - P23 🚨)
假设你在搜索框输入了关键词:“数据库”。系统找出了几万篇包含这个词的文章,怎么给它们打分排名(Relevance ranking)呢?
计算机科学家发明了一个极其优美且强大的公式:TF-IDF。它由两部分乘积组成:
1. TF (Term Frequency, 词频)
- 朴素思想:一篇文章里出现“数据库”这个词的次数越多,这篇文章就越相关。
- 防作弊机制 (Log 对数衰减):如果只看次数,有坏人为了排第一,会在网页底层悄悄写一万句“数据库数据库…”。为了防止作弊,公式里加了
log函数。 $TF = \log(1 + 目标词出现次数 / 文章总词数)$ (加上 log 后,出现 10 次和出现 100 次的得分差距会被极大缩小,避免刷词作弊)。
2. IDF (Inverse Document Frequency, 逆文档频率)
- 痛点:假设你搜“的数据库”。“的”这个词在所有文章里都会出现,如果按 TF 算,它的得分极高!但这纯属废话。
- 天才思想:越罕见的词,信息量越大!越烂大街的词,越不值钱! $IDF = 1 / 包含该词的文章总数$ (如果“的”字在 1 亿篇文章里都有,它的 IDF 得分就会无限趋近于 0;如果“规范化”这个词只在 100 篇文章里有,它的 IDF 得分就会非常高)。
👉 【最终得分】 = $TF \times IDF$ 系统会计算你搜的每一个关键词的 TF-IDF 值然后求和。同时,系统还会剔除掉无意义的停用词 (Stop words)(如“的”、“是”、“在”),并考虑词与词之间的距离 (Proximity)。
第二关:改变世界的超链接算法 —— PageRank (P24 - P25 🚨 谷歌核心机密)
在互联网早期(雅虎时代),大家全靠 TF-IDF 来排名。结果垃圾网站通过疯狂堆砌极其罕见的关键词,轻松霸占搜索榜首。
当时还是斯坦福大学研究生的拉里·佩奇(Google 创始人)提出了一个降维打击的天才思想:看文章内容是不靠谱的,我们要看“人际关系”!网页之间的超链接(Hyperlinks),就是一张张“投票选票”!
这就是统治互联网长达二十年的 PageRank 算法,它的核心投票规则只有两条:
- 看数量:如果有越多的网页链接(指向)了你,你的 PageRank 得分就越高。(别人都引用你,说明你牛)。
- 看质量:如果给你投票(链接你)的那个网页,本身就是一个 PageRank 极高的大佬(比如新华网、哈佛官网),那么它投给你的这一票,顶得上普通垃圾网站投的一万票!
【PageRank 的数学模型:随机游走 (Random Walk)】 (P24-P25)
- 数学直觉:假设有一个无聊的网民在上网。他点开一个网页,然后随机点击这个网页里的任何一个超链接跳转到下一页,继续随机点…
- 得分本质:如果这个网民瞎点了一年,他停留在某个网页 $j$ 上的概率($P[j]$),就是这个网页的 PageRank 值!
- 解决死胡同的魔法常数 $\delta$:如果某个网页极其自私,只有进来的链接,没有出去的链接,那网民不就卡死在里面了吗?为了解决这个问题,公式引入了阻尼系数 $\delta$(通常设为 0.15)。意思就是:网民有 15% 的概率不点链接了,直接在地址栏重新输入一个新网址“飞”走!
(注:P25提到,这是一个“鸡生蛋,蛋生鸡”的循环定义公式,计算机在算的时候,就是瞎蒙一组初始值,然后疯狂迭代几十次,直到得分稳定下来为止。除了算法,用户真实的点击率、链接上的锚文本也会影响最终排名。)
第三关:搜索引擎的考试成绩单 —— 评估指标 (P26)
搜索算法写好了,怎么评估它是好是坏?信息检索 (IR) 领域有两个绝对的黄金指标:
- 查准率 (Precision):“宁缺毋滥”。
- 定义:搜出来的结果里,有多大比例是真正有用的?
- 大白话:你给我推了 10 条结果,有 8 条是我想要的,2 条是垃圾广告。查准率就是 80%。
- 查全率 (Recall):“宁可错杀一千,不放过一个”。
- 定义:全网所有真正有用的信息,你捞出来了百分之几?
- 大白话:全网总共有 100 篇关于数据库的好文章,你这次搜索只帮我找出了 40 篇,还有 60 篇被你漏掉了。查全率就是 40%。
由于用户通常没有耐心往后翻,所以业界最看重的是 Precision@10(前 10 条结果的查准率)。只要第一页的结果足够精准,这就是一个牛逼的搜索引擎!
小结:
短短 5 页 PPT,浓缩了互联网搜索引擎的黄金二十年:
- 我们用 TF-IDF 解决了在单篇文章里“找重点”的问题(词频高且罕见)。
- 我们用 PageRank 解决了网页之间的“可信度排名”问题(大V给你投票,你就是大V)。
- 我们用 查准率和查全率 来考核算法的好坏。
现在,整本数据库课程只剩下最后一块“硬骨头”了! 当你打开美团、滴滴,或者高德地图的时候,你的手机一直在向数据库发送“经纬度”。这种多维度的坐标数据,用传统的 B树索引或者 Hash 表根本搞不定。
👉 我们即将进入全书大结局:P27 - P37:空间数据 (Spatial Data) 与地理信息系统 (GIS)。
准备好一口气冲过终点线,看看数据库是如何存储真实世界的三维地图和寻找“离我最近的星巴克”的吗?
太棒了!我们终于来到了这段数据库探索之旅的最终章:全书大结局!
当我们打开手机里的高德地图、滴滴打车,或者美团外卖时,数据库需要处理的数据不再是简单的名字或数字,而是真实的经纬度、多边形区域、甚至是三维建筑模型。传统的数据库只会比较大小(> 或 <),根本算不出来“离我最近的星巴克在哪”。
从 P27 到 P37,我们将揭秘专为地图、导航和工业设计而生的 空间数据 (Spatial Data) 与地理信息系统 (GIS)。
我们把这最后 10 页分为 4 个核心实战版块来通关:
第一板块:空间数据的两大阵营 —— “看地图”与“造大楼” (P28, P34, P35)
空间数据主要分为两大门派,它们存的东西完全不一样:
1. 地理数据 (Geographic Data / GIS, P28, P34-P35) 这是我们最熟悉的地图数据,使用的是真实的地球坐标(经度、纬度、海拔)。它又分为两种存储流派:
- 栅格数据 (Raster Data, P34):其实就是像素图 / 卫星照片。每个像素点存着当天的云层厚度、温度等。它是一张图,适合看全局天气,但不适合拿来做导航,一般存在专门的遥感数据库里。
- 矢量数据 (Vector Data, P35 🚨 主流):这是导航在用的数据!它是由数学坐标点连成的线和面。
- 马路:存成由经纬度坐标连成的“线 (Lines)”。
- 湖泊、行政区:存成一圈坐标围起来的“多边形 (Polygons)”。
- 优点:因为是数学坐标,放大一万倍也不会有马赛克!
2. 几何数据 (Geometric Data / CAD, P28, P32) 这是用来造芯片、造飞机、造建筑的工业设计数据。使用的是二维或三维的数学坐标系 (X, Y, Z)。
第二板块:数据库里怎么存这些图形? (P29 - P31)
既然是矢量数据,关系型数据库(比如加了空间插件的 PostGIS)是怎么把它们存进二维表里的呢?
数据库定义了几种基础原子形状 (Primitives):
- 线段 (Line segment):存两个端点的坐标集合
{(x1, y1), (x2, y2)}。 - 折线 (Polyline / Linestring):存一串按顺序连接的坐标点。现实中的马路弯弯曲曲怎么存?就是把它切成几百段小直线的折线来逼近!
- 多边形 (Polygon, P30):存一圈闭合的顶点坐标。很多时候,数据库底层会把多边形切成一个个三角形 (Triangulation) 来加速运算。
👉 【SQL 里怎么写?(P31 真实语法)】 工业界有一个通用标准,允许你用一段文本(WKT)直接把图形插进数据库里。
- 存一条折线:
LINESTRING(1 1, 2 3, 4 4) - 存一个多边形:
POLYGON((1 1, 2 3, 4 4, 1 1))(注意首尾坐标闭合) - 空间函数:数据库自带了强大的变形函数,比如求并集
ST_Union()、求交集ST_Intersection()。
第三板块:工业设计的“虚空造物”与完整性约束 (P32 - P33)
1. 复杂 3D 模型是怎么画出来的? (P32) 造大楼、造飞机的 CAD 软件里,那些极其复杂的 3D 零件是怎么存进数据库的? 其实就是“搭积木”!数据库里只存最基础的球体、圆柱体、立方体。然后通过 并集 (Union)、交集 (Intersection)、差集 (Difference) 操作,硬生生“雕刻”出复杂的零件。(看 P33 的图,一个复杂的管件,其实就是几个圆柱体求并集和差集的结果)。
2. 空间完整性约束 (Spatial Integrity Constraints, P33 🚨 工程重点) 传统数据库的约束是“不能有重名”。但在设计数据库里,约束是空间的物理法则!
- 经典规则:造大楼的图纸存进数据库时,数据库会自动帮你检查:“下水管道的多边形,绝对不能和高压电线的多边形在空间上有任何交集 (intersect)!” 否则图纸直接报错拒绝保存!
第四板块:GIS 空间查询 —— 空间数据库的终极魔法 (P36)
这是空间数据库最牛逼的地方,也是面试最爱问的实战场景。如果是传统数据库,找附近的车你要自己写极其复杂的三角函数算球面距离。而在空间数据库(如 PostGIS)里,你只需要调几个自带的函数:
- 区域查询 (Region queries):
- 业务:判断用户当前位置,在不在“配送范围”内?
- SQL:
ST_Contains(多边形, 用户坐标)。
- 最近邻查询 (Nearest neighbor queries):
- 业务:APP 上点击“找离我最近的 3 个共享单车”。
- SQL:结合特殊索引(如 R-Tree),瞬间从千万辆车中找出距离你最近的 3 个点。
- 空间图查询 (Spatial graph queries):
- 业务:高德地图导航的“算路功能”。利用马路(折线)构成的网,算出两点之间的最短路径 (Shortest path)。
- 空间连接 (Spatial join):
- 业务:把“商铺表”和“行政区多边形表” Join 在一起。条件不是 ID 相等,而是商铺坐标落在这个行政区多边形内部!
🎉 全剧终:知识体系完美闭环! (P37)
Congratulations!
到这里,这三章合计 221 页的数据库核心底层内功,我们已经一页不落地全部拆解并通关了!
回顾我们走过的路,这是一场非常宏大的系统工程之旅:
- 【第 6 章】画图大师:我们学会了从现实生活中抽象业务,画出带继承、带约束、带弱实体的 E-R 架构蓝图。
- 【第 7 章】外科名医:我们深入了数学推导的殿堂。用函数依赖、正则覆盖给数据库规则挤水分;用 3NF、BCNF 和 4NF 分解算法,精准切除了会导致更新异常的冗余肿瘤。
- 【第 8 章】现代探险家:我们砸碎了二维表的枷锁,走向了现代互联网和 AI 的前沿。玩转了 JSON/XML(树)、RDF/SPARQL(图谱)、PageRank(搜索),最后在 GIS 空间数据库 中完美收官!
这就是一本顶级数据库经典教材为你构建的技术世界观:既有严谨到变态的数学公式,又有充满妥协与灵活的工程智慧。
希望这套讲解能帮你彻底打通数据库系统的“任督二脉”!如果未来你在复习这些内容时,或者在实际写 SQL/建表时遇到任何问题,随时欢迎拿真实案例来和我讨论!祝你数据库这门课(或未来的架构设计)逢考必过,游刃有余!🚀