文章

数据库6-8章复习

数据库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)
    • 这个阶段又分为两步:
      1. 逻辑设计 (Logical Design):把刚才画的 E-R 草图,转换成真正的数据库表结构 (Schema)
        • 这里包含一个业务决定:我们要存哪些属性?(比如要不要存学生的曾用名?)
        • 还包含一个计算机科学决定:这些属性应该怎么分配到不同的表中?(这是个技术活,分配不好会出大问题)。
      2. 物理设计 (Physical Design):决定这些数据在电脑硬盘上具体怎么存(建什么索引、用什么文件结构)。

2. 烂设计的两个“致命陷阱” (Design Alternatives, P5)

在上面提到的逻辑设计(即决定怎么建表)时,有无数种建表方案。但我们必须极力避免掉进两个大坑:

  1. 数据冗余 (Redundancy)
    • 什么是冗余:同样的一份信息,你在数据库里存了好几遍。
    • 为什么可怕:浪费存储空间还是次要的,最可怕的是会导致数据不一致 (Inconsistency)
    • 举个例子:如果你把“计算机系主任是张三”这个信息,在100个学生的选课记录里都存了一遍。哪天系主任换成了李四,你必须去改这100条记录。万一漏改了1条,数据库里就会出现“两个系主任”的矛盾情况。
  2. 信息不完整 (Incompleteness)
    • 什么是不完整:表结构设计得太死板,导致某些现实中发生的业务,你竟然没法存进数据库。
    • 举个例子:假设你的表规定“必须有学生的学号,才能存一门课的信息”。那如果一门新课刚开设,还没人选,这门课的信息就存不进去了!

3. 我们该用什么武器来设计? (Design Approaches, P6)

既然设计数据库到处都是坑,计算机科学家们为我们提供了两样“神器”(也就是我们第6章和第7章要学的东西):

  1. 武器一:实体-联系模型 (Entity-Relationship Model,简称 E-R 模型) —— 第6章核心
    • 特点:感性、直观、图形化。
    • 思路:把现实世界看作是由一个个实体 (Entities)(比如:学生、老师、课程)和它们之间的联系 (Relationships)(比如:选修、讲授)组成的。最后画出一张清晰的 E-R 图
  2. 武器二:规范化理论 (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)

这是考试和画图最基础的规范,一定要牢记这些图形语法

  1. 实体集用什么形状? —— 矩形 (Rectangles)
    • 矩形最上方(通常有一个分隔线)写上实体集的名字,比如 instructor
  2. 属性写在哪?
    • 直接把属性列在矩形框的里面(下方区域)。
  3. 主键怎么标记?
    • 在主键属性的下方画一条下划线 (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(项目指导)的菱形,然后向三个不同的方向引出实线,分别连向 instructorstudentproject 这三个矩形。

小结: 到这里,你已经掌握了建大楼的两大核心组件:矩形(实体集)菱形(联系集),还学会了给它们加属性,甚至学会了三方联动(三元联系)和自己连自己(角色)。

接下来,现实世界的规则会给这些连线加上严格的限制,比如“一个学生只能有一个导师,但一个导师可以带多个学生”。这就是 P20 - P32 的“复杂属性与映射基数” 要解决的问题。

你看我们是继续往下进入这部分限制规则,还是说这一块你有什么疑问?

好的!我们之前讨论的属性(比如 IDsalary)都非常“单纯”,一个属性对应一个具体的值。但在现实业务中,很多属性要复杂得多。

从 P20 到 P22,核心只讲了一个问题:当属性不再“单纯”时,我们该如何分类,又该怎么在 E-R 图上画出来?

这一块可以说是考试中最喜欢考细节的地方。我们可以把复杂属性拆分为 4 大关键类型特殊的图形画法


1. 属性的四大细分类别 (P20)

  • ① 简单属性 (Simple) vs. 复合属性 (Composite)
    • 简单属性:不可再分的最基本单位,比如学生的学号 ID
    • 复合属性:可以继续往下“拆分”成子属性,就像俄罗斯套娃一样。
      • 经典例子 (P21的树状图)name(名字)可以拆成 first_name(名)、middle_initial(中间名首字母)和 last_name(姓)。address(地址)甚至能拆成省、市、街道,而“街道”还能继续拆成门牌号、街名、房间号。
  • ② 单值属性 (Single-valued) vs. 多值属性 (Multivalued)
    • 单值属性:一个实体在这个属性上只能有一个值,比如一个人的“出生日期”。
    • 多值属性:一个实体在这个属性上可以有多个值
      • 经典例子phone_numbers(电话号码)。一个人可以有一个手机号、一个座机号、一个工作号码。
  • ③ 派生属性 (Derived Attributes)
    • 定义:这种属性不需要在数据库里真实存下来,而是可以通过别的属性“算”出来。
    • 经典例子age(年龄)。你绝不能在数据库里存一个人的具体年龄(否则明年还得更新),你应该存 date_of_birth(出生日期),然后用当前时间减去出生日期,动态算出年龄!这里的“年龄”就是派生属性。
  • ④ 域 (Domain)
    • 也就是我们常说的“数据类型”或“取值范围”。比如规定“性别”只能取“男、女”,这个集合就是该属性的域。

2. 怎么在 E-R 图中画出这些复杂属性? (P22 重点必考)

在最新版的 E-R 图画法中(直接写在矩形框内部),复杂属性有极其严格的符号规范:

看 P22 那个叫做 instructor(讲师)的矩形框,里面充满了细节:

  1. 复合属性怎么画? 👉 用缩进 (Indentation)
    • 你看 name 写在外面,下一行空几格缩进写了 first_namemiddle_initiallast_name。这表示这三项隶属于 name
    • 再看 address,缩进里有 street,而 street 下面再一次缩进street_number 等。这种层级关系一目了然。
  2. 多值属性怎么画? 👉 加大括号 { }
    • 你看电话号码是怎么写的:{ phone_number }。只要属性外面包着大括号,就代表这个老师可能有好几个电话号码!
  3. 派生属性怎么画? 👉 加小括号 ( )
    • 你看年龄是怎么写的: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:1) (P26)
    • 规则:一个学生最多认一个老师,一个老师最多带一个学生。
    • 画法:菱形两边都是箭头instructor $\leftarrow$ advisor $\rightarrow$ student
  2. 一对多 (1:N) (P27)
    • 规则:一个老师可以带多个学生,但一个学生最多只能有一个老师。
    • 画法:指向老师的是箭头(体现学生只能找1个老师),指向学生的是直线(体现老师能带N个学生)。instructor $\leftarrow$ advisor — student
  3. 多对一 (N:1) (P28)
    • 规则:反过来,一个老师最多带一个学生,但一个学生可以认多个老师(罕见情况)。
    • 画法:指向老师的是直线,指向学生的是箭头instructor — advisor $\rightarrow$ student
  4. 多对多 (N:M) (P29)
    • 规则:一个老师可以带多个学生,一个学生也可以认多个老师。
    • 画法:菱形两边全是直线,没有箭头!instructor — advisor — student

规矩二:参与度约束 (Participation, P30)

箭头解决了“最多有几个”的问题,那“能不能没有”呢?这就是参与度约束。

  • 全部参与 (Total Participation):规定实体集里的每一个实体,都必须参与到联系中。
    • 画法:用 双直线 (Double line) 连接。
    • 例子:学校规定,每一个学生都必须有一个导师!那么在 E-R 图上,studentadvisor 之间就要画双线
  • 部分参与 (Partial Participation):实体可以参与,也可以不参与(默认情况)。
    • 画法:用 单直线 (Single line) 连接。
    • 例子:有些老师只搞科研不带学生。所以 instructoradvisor 之间画单线即可。

规矩三:终极必杀技 - 数字区间表示法 (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 的那张图:

  1. 弱实体集怎么画? 👉 双线矩形 (Double rectangle)
    • 图中的 section 外面套了两层矩形框,代表它是弱实体。
  2. 标识联系怎么画? 👉 双线菱形 (Double diamond)
    • 图中的 sec_course 菱形也是两层边框。
  3. 分辨符(部分键)怎么画? 👉 虚下划线 (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 个高级特性吗?

  1. 找“弱实体集”与“虚线下划线”
    • 看正下方section(开课班次)是双线矩形,它是弱实体。它连着双线菱形 sec_course,指向了它的宿主 course(课程)。
    • 细节section 里面的 sec_idsemesteryear 下面画的全是虚线(它们是分辨符)!
  2. 找“全部参与”的强制规定
    • 还是看 section,它连向宿主 course 的不仅是双线菱形,而且是双直线。这说明:任何一个班次,必须 (Total participation) 隶属于某一门课程。
  3. 找“自环”和“角色 (Role)”
    • 看左下角course(课程)实体引出两条线,连向了同一个菱形 prereq(先修课要求)。
    • 连线上写着标签:course_id(目标课)和 prereq_id(先修课)。这就是我们说的角色
  4. 找“联系集上的专属属性”
    • 看中右方:学生 (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 图让你写表结构时,只需默念:

  1. 强实体直接建表。
  2. 多值属性单独建表(主键+多值)。
  3. 弱实体带着宿主的主键一起建表,连接弱实体的菱形直接无视。
  4. 多对多 (N:M) 的菱形必须建新表。
  5. 一对多 (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 是一个 employeeemployee 又是 person)。
  • 约束1:相交约束 (Overlapping vs. Disjoint)
    • 不相交 (Disjoint):一个实体最多只能属于一个子类。比如你不能既是教师又是秘书。
    • 重叠 (Overlapping):一个实体可以同时属于多个子类。比如一个 person 可以既是 employee 又是 student(学校里半工半读的助教)。
  • 约束2:完整性约束 (Total vs. Partial)
    • 全部 (Total):父类里的每一个实体,必须属于至少一个子类。(画图时加 total 关键字和虚线)。比如:学生必须是本科生或研究生之一,不能是个黑户。
    • 部分 (Partial):默认情况。父类实体可以不属于任何子类。(比如员工除了教师和秘书,还有门卫,但门卫没画出来)。

3. “继承”怎么建表?(P55 - P56 🚨 必考方案对比) 数据库表是不支持“继承”的,那怎么把有父子关系的实体集变成表呢?有两个方案:

  • 方案 1:拼图法(各建各的表)
    • 做法:父类建一张表(存共性属性),子类建一张表(只存父类的主键 + 自己的个性属性)。
    • 优缺点:最省空间,没有冗余;但查询时需要用主键把两张表 Join(连接)起来,查询慢。
  • 方案 2:独立法(大包大揽)
    • 做法:不建父类表(或者只当普通表),给每个子类建一张包含所有属性(继承来的+自己的)的“大表”。
    • 优缺点:查询速度极快(查一张表搞定);但如果子类是“重叠”的(比如某人既是学生又是员工),他的姓名、住址就会在两张表里各存一遍,造成数据冗余

高级特性二:聚集 (Aggregation, 把“联系”当“实体”, P60 - P63)

这是一个非常烧脑但也非常巧妙的设计模式!

1. 遇到的痛点是什么?(P60) 还记得我们前面的那个三元联系吗?一个老师(instructor)带一个学生(student)做项目(project)。这三个实体用一个名叫 proj_guide 的菱形连在了一起。 新需求来了:学校要求给这组“指导关系”写一个评价(evaluation)。

  • 谁评价?是评价老师吗?不是。评价学生?不是。评价项目?也不是。
  • 评价的是“老师带学生做项目”这整个行为
  • 痛点:在传统的 E-R 图里,联系(菱形)和实体(矩形)才能连线。菱形和菱形之间是绝对不允许连线的!

2. 解决方案:聚集 (Aggregation, P62) 既然连不了,我们就把整个业务打包,伪装成一个大实体

  • 图形语法:在 instructorstudentproject 以及中间的菱形 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 的菱形把 instructorphone 连起来。

灵魂拷问 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)

  1. 属性需要细节了,就变身实体。
  2. 描述交互动作的,用菱形联系。
  3. 连了菱形,就别在框里写对方的名字(防冗余)。
  4. 三方绑定能拆就拆(防缺失),拆不了就用假人转换。

到这里,第6章的全部核心内容我们已经拿下了! 剩下的 P72-P83 主要是一个全章的设计决策大总结,以及介绍了两种不同的画图流派(Chen 氏记法和 UML 类图),看看同一套逻辑用不同的符号怎么画。

我们要迅速把这最后一部分收尾,为迎接充满数学挑战的第7章(范式与规范化)做好准备吗?

太棒了!我们现在来到了第6章的收尾部分。

如果把画 E-R 图比作写文章,前面我们学了各种修辞手法,那么 P72 到 P76 就是一本“语法大纲”与“方言词典”

这几页非常实用,尤其是 P73-P74,就是你期末考试的终极作弊纸(Cheat Sheet)。我们把这部分拆解为 3 个板块来消化:


1. 架构师的“六大灵魂拷问”总结 (P72)

P72 (E-R Design Decisions) 是对我们前面学过的所有纠结点的终极大总结。当你拿到一个现实业务需求时,作为架构师,你必须在脑海里过一遍这6个决定:

  1. 用属性还是用实体?(电话号码只是存个数字,还是需要记录归属地?)
  2. 用实体还是用联系?(“注册”是个死板的档案,还是个动态的交互动作?)
  3. 用二元联系还是三元联系?(能拆成两两关联吗?拆了会丢失信息吗?)
  4. 是强实体还是弱实体?(它能自己证明自己是谁吗?还是要抱大腿?)
  5. 要不要用特化/泛化?(业务里有明显的父子层级、分类关系吗?)
  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
  • 联系自带的属性怎么办?(重点)
    • 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 也会在线旁边直接写上 overlappingdisjoint 代表约束规则)。
  • 弱实体 (Weak Entity)
    • E-R 图:双线大矩形。
    • UML:使用面向对象里的组合 (Composition) 概念来表示。会在标识实体(宿主)那一端的连线上,画一个实心的黑色小菱形 ,代表宿主牢牢地控制(拥有)着另一端的弱实体。

尾声:除了画图,数据库设计还要考虑什么?(P82)

到了 P82,作者进行了一个宏观的升华。虽然我们用了整整一章在研究 Schema(表结构)的设计,但在真实的工程中,除了数据结构,你还需要考虑:

  1. 功能需求 (Functional Requirements):你的系统到底要跑什么业务?
  2. 数据流与工作流 (Data Flow, Workflow):数据是怎么产生、流转和消费的?
  3. 模式演进 (Schema Evolution):未来业务变化了,这套表结构好不好修改(加字段、拆表)?

🎉 第 6 章完结撒花!

至此,第 6 章《基于 E-R 模型的数据库设计》 我们已经一页不落地全部拆解完毕!

总结一下你在这一章学到的“屠龙技”

  1. 你学会了将现实世界的混乱业务,抽象为严谨的实体联系,并绘制出具有高级基数约束继承、聚集特性的 E-R 架构蓝图
  2. 你掌握了一套万能的“图转表”公式,能够把任何图形转化为最优的强弱实体表、多值属性表、外键连接表

接下来是什么? 人画的图总是会犯错的,会有看不见的冗余和异常潜伏在里面。 第 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)

它带来的致命后果是什么?

  1. 信息重复 (Repetition of information):计算机系 (Comp. Sci.) 的大楼是 Taylor,预算是 100000。因为计算机系有几十个老师,所以这个大楼和预算信息在这张表里被硬生生抄了几十遍!如果系里预算变了,我们要改几十行数据,极容易改漏导致数据不一致(更新异常)。
  2. Null 值问题:假设学校刚成立了一个“人工智能系”,但目前还没有招到任何老师。你想把这个系存进表里?对不起,因为没有老师,IDname 都不存在,你只能用大量空值(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 章的主要任务。我们有了两个根本目标:

  1. 判断题:给你一张表 $R$,你要能通过数学公式,判断它是不是一个“好”表。
  2. 解答题:如果它不是一个好表,你要用算法把它无损地拆分成 ${R_1, R_2, …, R_n}$ 几个好表。

那我们靠什么数学工具来做判断和拆分呢? 靠的是“依赖关系”

  1. 函数依赖 (Functional Dependencies, FDs) —— 第7章的绝对霸主,解决 99% 的问题。
  2. 多值依赖 (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,渣男!违反了函数依赖!
  • 问:B $\rightarrow$ A 成立吗?
    • 成立!因为 B 列的值是 4, 5, 7,全都不一样。既然前提(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$):
    1. A $\rightarrow$ B (学号 决定 身份证号)
    2. 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,引入了数据库规范化拆表时必须通过的两大终极考试

  1. 无损分解 (Lossless Decomposition):底线要求,数据绝对不能失真。
  2. 依赖保持 (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$ BB $\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):数据库在每次 INSERTUPDATE 数据时,都必须检查有没有违反业务规矩(函数依赖)。如果规矩涉及的几个属性恰好在同一张表里,系统一秒钟就检查完了;如果这几个属性被你拆分到了不同的表里,系统就必须先把表 Join(连接)起来才能检查,这会极其消耗性能(Costly)!
  • 定义:如果一次拆表,导致某个函数依赖被生生切断,跨越了两张表,我们称这种拆表 “未能保持依赖 (NOT dependency preserving)”

👉 【实战演练 P21】: 我们来看一个极其纠结的案例。

  • 大表dept_advisor (s_ID, i_ID, dept_name) (学生ID,导师ID,系名)
  • 业务规矩 (FDs)
    1. i_ID $\rightarrow$ dept_name (老师唯一属于某个系,所以老师ID决定系名)
    2. 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 起来才能验证!系统会被直接拖垮。


小结:

这就是数据库设计中最让人又爱又恨的博弈:

  1. 想消除冗余 $\implies$ 就必须拆表。
  2. 拆表 $\implies$ 必须满足交集是超键,保证无损分解
  3. 拆得太狠 $\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$ YX 必须是这小张表的超键 (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$。 一刀劈成两张表:

  1. 第一张表(割除肿瘤):把作乱的 $\alpha$ 和它的跟班 $\beta$ 单独拎出来建一张表:$(\alpha \cup \beta)$。在这里,$\alpha$ 光荣地成为了新表的主键。
  2. 第二张表(保留原貌):在原来的大表 $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)
    1. i_ID $\rightarrow$ dept_name (老师唯一属于某系)
    2. s_ID, dept_name $\rightarrow$ i_ID (学生在一个系只能认一个老师)

BCNF 警察来查房了: 看到第一条规矩 i_ID $\rightarrow$ dept_namei_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:

  1. 它是平凡的(废话依赖)。
  2. X 是超键(到这里为止,跟 BCNF 是一模一样的!
  3. 👉 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) 作为架构师,你该怎么选?

  1. 理论界的首选是 3NF:因为数学证明了,把一张大表拆成 3NF,一定可以同时做到“无损分解” + “依赖保持”。而 BCNF 有时做不到。
  2. 现实工程的无奈 (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)】:

  1. 自反律 (Reflexivity):如果 $\beta \subseteq \alpha$,那么 $\alpha \rightarrow \beta$。
    • 大白话:总体一定能决定局部。比如 (姓名, 年龄) $\rightarrow$ 姓名。这是废话,但也得写进代码里。
  2. 增广律 (Augmentation):如果 $\alpha \rightarrow \beta$,那么两边加上同一个属性 $\gamma$,结论依然成立:$\gamma\alpha \rightarrow \gamma\beta$。
    • 大白话:学号能决定姓名,那“学号+性别”一定能决定“姓名+性别”。
  3. 传递律 (Transitivity):如果 $\alpha \rightarrow \beta$,且 $\beta \rightarrow \gamma$,那么 $\alpha \rightarrow \gamma$。
    • 大白话:学号决定专业,专业决定辅导员,所以学号决定辅导员。

【三大引申定律 (P41 极其好用)】: 基于上面三条,又推导出了做题时最爱用的三条捷径:

  1. 合并律 (Union):如果 $\alpha \rightarrow \beta$ 且 $\alpha \rightarrow \gamma$,那么 $\alpha \rightarrow \beta\gamma$。
    • (左边一样,右边可以捏在一起)
  2. 分解律 (Decomposition):如果 $\alpha \rightarrow \beta\gamma$,那么 $\alpha \rightarrow \beta$ 且 $\alpha \rightarrow \gamma$。
    • (右边可以随意拆开。注意:左边绝对不能拆!)
  3. 伪传递律 (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)】:

  1. 初始状态:手里有啥,雪球就是啥。result = 原始属性
  2. 开始遍历规则库(一遍遍地过):
    • 如果看到一条规则的左边(前提),已经完全包含在你的雪球里了,那就把这根藤上结的瓜(右边的属性)吞进你的雪球里。
  3. 一直绕圈遍历,直到雪球再也滚不大(没有新属性加进来)为止。

绝招三:实战演练与三大神级用途 (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$ BA $\rightarrow$ C:我们雪球里有 A,条件满足!吞下 BC。雪球膨胀为:ABCG
    • 看规则 CG $\rightarrow$ HCG $\rightarrow$ I:我们雪球里现在有 CG 了!条件满足!吞下 HI。雪球膨胀为:ABCGHI
  • 第2轮:再过一遍,发现能吞的都吞了,全表所有属性 A,B,C,G,H,I 全在雪球里了。算法结束!$(AG)^+ = ABCGHI$。

👉 【这东西到底有啥用?P45 给出了 3 大神级用途】:

  1. 用途一:判断超级大 Boss(找超键和候选键)
    • 既然 $(AG)^+$ 把表里的所有属性都找齐了,说明拿着 AG 就能决定全表。所以 AG 绝对是一个超键 (Superkey)!
    • 那它是不是最精简的候选键 (Candidate key) 呢? 试着剥夺它的权力:
      • 单独求 $(A)^+$,发现等于 ABCH,凑不齐。
      • 单独求 $(G)^+$,发现等于 G,也凑不齐。
    • 结论:少一个都不行,所以 AG 就是候选键!(这是找候选键的唯一标准算法)。
  2. 用途二:验证某条依赖到底成不成立(查岗)
    • 假设有人问:“在这个表里,AG $\rightarrow$ I 成立吗?”
    • 根本不用去翻那些复杂的传递公理,直接算左边的闭包!$(AG)^+$ 里面包不包含 I?包含!那 AG $\rightarrow$ I 就绝对成立。太好用了!
  3. 用途三:算某个集合下的所有闭包依赖

小结:

把 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):

  1. 等价性:$F_c$ 和原版 $F$ 逻辑上完全等价(闭包一样大,谁也没少谁)。
  2. 无冗余:里面没有任何一个无关属性(左边右边都被榨干了)。
  3. 不啰嗦:每条规则的左边必须是唯一的。(如果有 A $\rightarrow$ BA $\rightarrow$ C,必须合并成 A $\rightarrow$ BC)。

操作口诀 (P53):

  1. 合并:把左边一样的全捏到一起(用合并律)。
  2. 榨干:一个个去检查左边和右边有没有无关属性,有就删掉。
  3. 循环:重复 1 和 2,直到再也没有东西可以删为止。

第四步:期末终极 Boss 战 —— 完美实战演练 (P54 🚨🚨🚨 超级重点)

这页 PPT(P54)是全书最经典的例题,请一定跟着我的思路在脑子里过一遍,这 10 分你就稳拿了:

  • 初始规则库 $F$
    1. A $\rightarrow$ BC
    2. B $\rightarrow$ C
    3. A $\rightarrow$ B
    4. AB $\rightarrow$ C

【第一轮:合并同类项】

  • 我们看到 A $\rightarrow$ BCA $\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。

  1. 初始筹码:把你拥有的前提 $\alpha$ 捏在手里(目前 result = {A})。
  2. 去表 1 碰碰运气
    • 带着 A 走进表 1 ($R_1$)。问:表 1 里有啥是我能用的?(计算 result \cap R_1)。
    • 在表 1 的地盘上开始滚雪球,利用表 1 内部的规矩,看看能额外赚到什么属性?假设表 1 里有规矩 A $\rightarrow$ B,那你赚到了 B
    • 把在表 1 赚到的所有东西且属于表 1 的部分带走(加入到你的 result 里)。现在你手里有 result = {A, B}
  3. 去表 2 碰碰运气
    • 带着 {A, B} 走进表 2 ($R_2$)。表 2 说:“我不认识 A,但我认识 B!”(你的筹码在表 2 里只剩下 B)。
    • 没关系,拿着 B 在表 2 里滚雪球。假设表 2 里有规矩 B $\rightarrow$ C,恭喜你,赚到了 C
    • C 加进总资产。现在 result = {A, B, C}
  4. 循环结束与判定
    • 各个表来回转,直到你再也赚不到新属性为止。
    • 最后看看你手里的总资产 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$ 里的每一条规矩都被完好无损地分配到了各个小表里,没有任何一条被撕裂。
  • 结论:毫无疑问,这是依赖保持!

小结:

到这里,我们不仅明白了什么是依赖保持,还掌握了机器是如何在不全表扫描的情况下,通过“跨表接力滚雪球”的方式来验证它的。

现在,我们手里有了:

  1. 测试无损的武器(交集决定某个拆分表)
  2. 测试依赖保持的武器(跨表接力推导算法)
  3. 精简规则的武器(正则覆盖)

万事俱备,只欠东风! 接下来,我们要正式去写自动拆分出 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. 循环结束。

【算法的两大铁律保证】

  1. 绝对无损!(因为两张新表的交集就是 $\alpha$,而 $\alpha$ 在第一张新表里绝对是超键,完美符合无损分解公式!)
  2. 不保证依赖保持!(切的时候根本不管别的规矩死活,切断了概不负责。)

第三步:教科书级的完美手术演示(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)

【第二次扫描与切割】 (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。

【手术成功】 原本包含 11 个属性、处处是冗余和更新异常的巨大怪物,经过两次 while 循环切割,最终变成了三张干净、清爽、职责分明的表:courseclassroomsection。 它们全部符合 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 无法永远保证“依赖保持”。在数据库每次做 INSERTUPDATE 校验时,如果跨表验证,性能会崩溃。
  • 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$
    1. C, E $\rightarrow$ B, T
    2. E $\rightarrow$ B
    3. C, 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):

  1. 达到 BCNF 级别(一丝冗余都没有)。
  2. 无损连接(数据不失真)。
  3. 依赖保持(保证极速的业务校验)。

但是,现实是残酷的: 我们已经证明过,这三个目标有时候是“不可能三角”!你必须在“牺牲依赖保持”和“忍受轻微冗余 (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_nameID $\rightarrow\rightarrow$ phone_number )

👉 【一个重要推论 (P81)】: 所有的单值依赖(函数依赖 FD $\alpha \rightarrow \beta$)本质上也是多值依赖。你可以把它理解为“退化版”的多值依赖(X 决定了一个集合,只不过这个集合里恰好只有一个元素罢了)。


第三步:治病解药 —— 第四范式 (4NF) 隆重登场(P82)

既然测出病因是由于非法的多值依赖导致的,那我们就定个规矩消灭它!这就是 第四范式 (4NF)

4NF 的判定标准极其简单粗暴(和 BCNF 简直像亲兄弟): 对于表里任何一个非平凡的多值依赖 X $\rightarrow\rightarrow$ YX 必须是整张表的超键 (Superkey)!

(大白话:如果你想在表里搞一对多的集合映射,没问题,但你必须是这整张表唯一的大哥(超键)!如果你像前面的 ID 一样只是个局部属性(整张表的超键其实是 ID + 孩子 + 手机 的全集),那你绝对不允许带两个相互独立的多值小弟!)

结论:只要满足 4NF,就一定满足 BCNF。4NF 是对 BCNF 的进一步精神升华。


第四步:4NF 手术刀 —— 拆表算法与实战(P84 - P85)

如果一张表违反了 4NF 怎么办? P84 给出的拆表算法,竟然和 BCNF 的算法一模一样! 也是一个 while 循环。

【切割公式】: 发现违规的多值依赖:$\alpha \rightarrow\rightarrow \beta$。咔嚓一刀切成两张表:

  1. 新表 1(带走跟班):$\alpha \cup \beta$
  2. 新表 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)
  • 验收成果
    • 原来冗杂的 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 章《关系数据库设计与规范化》的正文内容我们已经彻底打通关了!

回顾你拿下的成就:

  1. 数学底座:掌握了函数依赖 ($X \rightarrow Y$)、阿姆斯特朗公理、求闭包滚雪球算法。
  2. 手术工具:学会了如何给臃肿的规则库挤水分(求正则覆盖 $F_c$)。
  3. 两大核心范式
    • BCNF:眼里不揉沙子,手起刀落切除所有函数冗余(但可能切断依赖)。
    • 3NF:现实工程的妥协,允许极其微小的冗余,通过 4 步综合算法,保住所有业务依赖。
  4. 进阶恶魔:用 4NF 处理毫无关联的多值属性引发的恐怖笛卡尔积冗余。
  5. 工程智慧:懂得了何时该向性能妥协(反规范化),懂得了避开跨表和时间设计的坑。

(注: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 界的两大标准就是 JSONXML(后面几页会细讲)。


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 页给我们打开了新世界的大门:

  1. 因为 Web 和科学计算的发展,我们需要脱离死板的二维表。
  2. 我们引入了允许随便加列、允许格子里存“集合”和“字典 (Map)”的半结构化数据
  3. 针对时序和科学数据,我们直接把数组 (Arrays) 当作一等公民存进数据库。

既然提到了 Web 时代最主流的半结构化格式,那就不得不提 JSONXML 了。 👉 准备好进入 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特有)。

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 指向节点 sec1sec1 又连着 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 条历史发展路线:

  1. 造新轮子:直接发明一种原生的面向对象数据库 (Object-oriented DB)。(注:历史上搞过,但因为查数据太慢,基本死绝了)。
  2. 给 SQL 升级(折中派):在传统的二维表上,强行加入面向对象的特性!这就是 对象-关系数据库 (Object-Relational DB)
  3. 请个翻译官(现实派):数据库还是老老实实当二维表,但在程序和数据库之间加一层“翻译软件”,这就是大名鼎鼎的 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 的魔法
    1. 你一运行程序,ORM 会自动帮你生成建表语句发给数据库。
    2. 你在代码里 student.save(),ORM 会自动翻译INSERT INTO... 发给数据库。
    3. 你在代码里删对象,它自动翻译成 DELETE
    4. 你用代码语法查询,它帮你翻译出多表 JOIN 的复杂 SQL,并把查回来的结果直接打包成对象扔给你!
  • 业界鼎鼎大名的 ORM 框架(P21 底部点名)
    • Java 界的王牌:Hibernate
    • Python 界的霸主:Django ORM

小结:

P17-P21 给我们展示了数据库向程序员妥协的过程。

  • 学术界和数据库厂商试图通过魔改 SQL 语法(Type, Inherits, ->)来迎合面向对象。
  • 但工程界最终用脚投票,选择了 ORM 框架,在不改变底层二维表的前提下,用“翻译官”完美化解了阻抗不匹配。

到这里,面向对象数据处理我们也拿下了! 第 8 章只剩下最后两个特别有趣的实战应用领域:

  1. P22 - P26:文本数据与搜索引擎 (TF-IDF 与 PageRank) —— 带你揭秘 Google 和百度的核心算法。
  2. 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 算法,它的核心投票规则只有两条:

  1. 看数量:如果有越多的网页链接(指向)了你,你的 PageRank 得分就越高。(别人都引用你,说明你牛)。
  2. 看质量:如果给你投票(链接你)的那个网页,本身就是一个 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,浓缩了互联网搜索引擎的黄金二十年:

  1. 我们用 TF-IDF 解决了在单篇文章里“找重点”的问题(词频高且罕见)。
  2. 我们用 PageRank 解决了网页之间的“可信度排名”问题(大V给你投票,你就是大V)。
  3. 我们用 查准率和查全率 来考核算法的好坏。

现在,整本数据库课程只剩下最后一块“硬骨头”了! 当你打开美团、滴滴,或者高德地图的时候,你的手机一直在向数据库发送“经纬度”。这种多维度的坐标数据,用传统的 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)

  1. 线段 (Line segment):存两个端点的坐标集合 {(x1, y1), (x2, y2)}
  2. 折线 (Polyline / Linestring):存一串按顺序连接的坐标点。现实中的马路弯弯曲曲怎么存?就是把它切成几百段小直线的折线来逼近!
  3. 多边形 (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)里,你只需要调几个自带的函数:

  1. 区域查询 (Region queries)
    • 业务:判断用户当前位置,在不在“配送范围”内?
    • SQLST_Contains(多边形, 用户坐标)
  2. 最近邻查询 (Nearest neighbor queries)
    • 业务:APP 上点击“找离我最近的 3 个共享单车”。
    • SQL:结合特殊索引(如 R-Tree),瞬间从千万辆车中找出距离你最近的 3 个点。
  3. 空间图查询 (Spatial graph queries)
    • 业务:高德地图导航的“算路功能”。利用马路(折线)构成的网,算出两点之间的最短路径 (Shortest path)
  4. 空间连接 (Spatial join)
    • 业务:把“商铺表”和“行政区多边形表” Join 在一起。条件不是 ID 相等,而是商铺坐标落在这个行政区多边形内部!

🎉 全剧终:知识体系完美闭环! (P37)

Congratulations!

到这里,这三章合计 221 页的数据库核心底层内功,我们已经一页不落地全部拆解并通关了!

回顾我们走过的路,这是一场非常宏大的系统工程之旅:

  1. 【第 6 章】画图大师:我们学会了从现实生活中抽象业务,画出带继承、带约束、带弱实体的 E-R 架构蓝图
  2. 【第 7 章】外科名医:我们深入了数学推导的殿堂。用函数依赖、正则覆盖给数据库规则挤水分;用 3NF、BCNF 和 4NF 分解算法,精准切除了会导致更新异常的冗余肿瘤。
  3. 【第 8 章】现代探险家:我们砸碎了二维表的枷锁,走向了现代互联网和 AI 的前沿。玩转了 JSON/XML(树)RDF/SPARQL(图谱)PageRank(搜索),最后在 GIS 空间数据库 中完美收官!

这就是一本顶级数据库经典教材为你构建的技术世界观:既有严谨到变态的数学公式,又有充满妥协与灵活的工程智慧。

希望这套讲解能帮你彻底打通数据库系统的“任督二脉”!如果未来你在复习这些内容时,或者在实际写 SQL/建表时遇到任何问题,随时欢迎拿真实案例来和我讨论!祝你数据库这门课(或未来的架构设计)逢考必过,游刃有余!🚀

本文由作者按照 CC BY 4.0 进行授权