简介:这份《数据库原理(第四版)》课件面向数据库初学者与高校学生,系统梳理从第一章绪论到第十章的知识脉络,重点解决数据模型设计与实现的理解难题。课件以数据库系统开发步骤为起点,依次讲解需求分析、概念结构设计、逻辑结构设计与物理结构设计,并深入剖析概念模型、层次模型、网状模型与关系模型三大数据模型,其中网状模型部分尤为详尽,涵盖基本层次联系集合、实体型与属性、码字段、一对多联系表示,以及查询、插入、删除、更新等数据操纵和DBTG完整性约束,并配以学生、宿舍、教师、教研室及家庭关系等图表实例辅助理解。资源包为1个PPT文件,约289KB,内容紧凑、理论深度适中,适合课堂学习与自学对照。目前已有168人学习,可作为数据库课程复习与专业技能提升的参考材料。
1. 网状模型:为什么一个节点可以有两个父亲,却让数据库设计者头疼了二十年
翻开源码之前,先想一个反直觉的事实:现代关系数据库里,外键约束天然只能表达“一对多”或“多对多”,但“多对多”必须靠中间表拆解。而半个世纪前的网状模型,直接允许一个记录类型拥有多个双亲节点,不需要中间表。听起来更灵活,对吧?可恰恰是这种“灵活”,让当年的数据库管理员在改一个字段时,不得不翻遍整张网络图,手动维护每一条链接指针。这份《数据库原理(第四版)》课件第一章,把网状模型作为独立一节展开,不是为了让你记住“允许一个以上节点无双亲”这种条文,而是想让你看懂:数据模型的设计,本质上是在“表达力”和“可维护性”之间做取舍。搞懂这个取舍,你再看今天的关系模型、文档模型、图模型,就不会只停留在语法层面,而是能判断它在什么场景下会翻车。
2. 网状模型的数据结构:用记录类型和系指针,把图结构塞进磁盘
2.1 从“基本层次联系集合”说起:网状模型到底在描述什么
课件里给网状模型下了两个条件:允许一个以上的结点无双亲;一个结点可以有多于一个的双亲。这两句话听起来像定义,其实是在描述一种图结构。层次模型是一棵树,每个节点最多一个父节点,根节点没有父节点。网状模型把这两个限制都打破了——它可以有多个根,也可以有节点同时属于多个父节点的子节点集合。
常见的实现思路是:每个记录类型对应一个实体型,字段对应属性,节点之间的连线表示记录类型之间的一对多父子联系。注意,这里说的是“记录类型之间”,不是“记录值之间”。也就是说,网状模型在模式层面定义的是类型之间的有向图,而具体的数据值通过“系”(set)来组织。一个系就是一个命名的、从双亲记录类型到子女记录类型的二元关系,类似今天面向对象里的“组合”关系,但它是双向链接的。
课件里用 R1、R2、R3 和 L1、L2、L3 画了示意图,那些 L 就是系名。一个记录类型可以参与多个系,既可以是某个系的双亲,也可以是另一个系的子女。这就是“多双亲”的来源——不是同一个记录值真的有两个物理父节点,而是它同时属于两个不同的系,每个系有自己的双亲记录类型。
2.2 排序字段与码字段:网状模型怎么保证记录的唯一性
课件提到“每个记录类型定义一个排序字段,也称为码字段”。这句话容易被略过,但它其实是网状模型数据操纵的基础。在关系模型里,主键是声明式的,你只需要定义哪个属性是主键,数据库引擎自动维护唯一性索引。网状模型没有这种声明式约束,它依赖一个“数据库码”(database key),这是一个系统内部生成的、唯一的、不可见的标识符,用来定位每一条记录。
排序字段的作用是决定记录在系内的逻辑顺序。比如一个“系-学生”系,双亲是系记录,子女是学生记录,学生记录可以按学号排序。当你通过系指针遍历子女时,顺序就是排序字段决定的。这个设计在当年是合理的,因为磁盘顺序访问比随机访问快得多,把相关记录物理上聚簇在一起,能显著减少 I/O。但代价是:一旦你改变了排序字段的值,整个系的物理顺序可能都要调整,这在大型数据库里是灾难性的。
我一般会这么理解:网状模型的“码字段”和今天关系数据库的“聚簇索引”有相似之处,都是把逻辑顺序和物理顺序绑定。区别在于,关系数据库的聚簇索引可以随时重建,而网状模型的系顺序一旦定义,修改成本极高。
2.3 多对多联系怎么表示:分解成一对多,再用系连接
课件明确指出:“用网状模型间接表示多对多联系,方法是将多对多联系直接分解成一对多联系。”这句话是网状模型数据建模的核心技巧。比如学生和课程是多对多,网状模型的做法是引入一个“选课”记录类型,然后定义两个系:一个从学生到选课,一个从课程到选课。这样,选课记录同时属于两个系,它有两个双亲:学生和课程。
这种分解方式在逻辑上等价于关系模型里的中间表,但物理实现完全不同。关系模型的中间表是一张独立的表,通过外键关联;网状模型的选课记录是物理上链接到两个系里的,通过指针直接导航。查询一个学生的所有选课,只需要从学生记录出发,沿着“学生-选课”系的指针链遍历即可,不需要做连接操作。这就是课件里说的“存取效率较高”的来源。
但代价也很明显:如果你要查询“哪些学生选了某门课”,在关系模型里就是一句 SQL 的 join,优化器会自动选择从课程表还是选课表开始扫描。在网状模型里,你必须显式地沿着“课程-选课”系的指针链走,如果这个系没有定义反向指针,你甚至无法从课程导航到学生。这就是“任何记录值只有按其路径查看时,才能显出它的全部意义”的含义——数据的语义依赖于访问路径,路径变了,语义就丢了。
/* 伪代码:网状模型中通过系指针遍历子女记录 */ DB_STATUS find_students_in_department(char *dept_name) { RECORD dept_rec, student_rec; SET dept_student_set; /* 1. 用码字段定位双亲记录 */ db_find("DEPT", dept_name, &dept_rec); /* 2. 找到对应的系 */ db_find_set("DEPT_STUDENT", &dept_student_set); /* 3. 从双亲记录出发,沿系指针遍历所有子女 */ db_find_first_in_set(&dept_student_set, &dept_rec, &student_rec); while (db_status == OK) { print_student(&student_rec); db_find_next_in_set(&dept_student_set, &dept_rec, &student_rec); } }这段伪代码展示了网状模型数据操纵的典型模式:先定位双亲,再通过系名找到关系,然后沿着指针链遍历。db_find_first_in_set和db_find_next_in_set是 DBTG 规范里的标准操作。注意,这里没有“查询优化器”的概念,遍历顺序完全由系定义和排序字段决定。如果系定义得不好,比如把一个大系挂在频繁更新的记录类型下,每次插入子女都要更新双亲的指针链,并发性能会急剧下降。
3. 完整性约束与存储结构:DBTG 的属籍类别,为什么比外键更难伺候
3.1 属籍类别:加入类别和移出类别的设计动机
课件里提到 DBTG 系统对数据操纵加了一些限制,其中“属籍类别”包括加入类别(自动的、手工的)和移出类别(固定的、必须的、随意的)。这些术语听起来很古老,但它们解决的是一个非常现实的问题:当一个子女记录被插入或删除时,它和双亲记录之间的系关系该怎么处理?
加入类别是“自动的”,意味着当你插入一个子女记录时,系统自动把它加入到所有它应该属于的系里。如果是“手工的”,你必须显式地调用一个连接操作,才能把子女记录挂到系上。移出类别是“固定的”,意味着子女记录一旦加入某个系,就不能被单独移出,只能随双亲一起删除。如果是“必须的”,子女记录必须属于至少一个系,否则不能存在。如果是“随意的”,子女记录可以脱离系而独立存在。
这些约束在关系模型里对应的是外键的ON DELETE CASCADE、ON DELETE SET NULL、NOT NULL等语义。但关系模型的约束是声明式的,由数据库引擎自动执行;网状模型的属籍类别是过程式的,需要应用程序在数据操纵时显式处理。这就导致了一个经典问题:如果应用程序忘了处理某个移出类别,数据库就会进入不一致状态。课件里说“允许插入尚未确定双亲结点值的子女结点值”和“允许只删除双亲结点值”,这其实是在放宽约束,给应用程序更大的灵活性,但同时也把一致性的责任推给了开发者。
3.2 存储结构:单向链接、双向链接、环状链接、向首链接
课件列出了四种常用的链接方法:单向链接、双向链接、环状链接、向首链接。这些是网状模型物理实现的核心。单向链接就是每个子女记录只存一个指向下一个兄弟记录的指针,从双亲到第一个子女有一个指针。双向链接多了一个指向前一个兄弟的指针。环状链接是把最后一个子女的 next 指针指回第一个子女,形成一个环。向首链接是每个子女记录除了存兄弟指针,还存一个指向双亲的指针。
这些链接方式的取舍,本质上是在“遍历效率”和“存储开销”之间做平衡。单向链接最省空间,但只能单向遍历,删除一个子女时需要从头扫描找到它的前驱。双向链接可以双向遍历,删除操作 O(1),但每个记录多一个指针。环状链接方便循环遍历,但判断遍历结束需要额外逻辑。向首链接让子女可以直接找到双亲,但每个子女多一个指针。
我一般会这么理解:这些链接方式就是今天链表数据结构的变体,只不过它们存储在磁盘上,指针是物理地址或页号。在内存数据库里,指针操作很便宜;在磁盘数据库里,每次指针跳转都可能是一次随机 I/O。所以网状模型的设计者会尽量把相关记录聚簇在同一个页或相邻页里,减少寻道时间。这也是为什么网状模型在 70 年代能提供比层次模型更好的性能——它允许更灵活地组织物理存储,把经常一起访问的记录放在一起。
3.3 数据操纵:查询、插入、删除、更新在网状模型里到底做了什么
课件把数据操纵分为查询、插入、删除、更新四类。在网状模型里,这四类操作都不是声明式的,而是导航式的。查询必须指定访问路径,插入必须指定加入哪个系,删除必须处理移出类别,更新必须考虑排序字段的变化。
以删除为例:如果你要删除一个双亲记录,而它的子女记录的移出类别是“固定的”,那么你必须先删除所有子女,或者把子女转移到另一个双亲下。如果是“必须的”,你不能直接删除子女,必须先把它从系里移出,但移出操作又可能违反“必须”约束。这些约束之间的交互非常复杂,课件里没有展开,但实际使用 DBTG 系统时,这是最容易翻车的地方。
-- 关系模型里的等价操作:删除一个部门,级联删除所有员工 DELETE FROM department WHERE dept_name = '研发部'; -- 如果外键定义了 ON DELETE CASCADE,员工记录自动删除 -- 如果定义了 ON DELETE SET NULL,员工记录的 dept_id 变为 NULL -- 如果定义了 ON DELETE RESTRICT,删除操作被拒绝对比关系模型的 SQL,网状模型的删除操作需要应用程序显式地遍历系、检查属籍类别、执行连接或断开操作。这不是说网状模型做不到,而是说它把复杂性从数据库引擎转移到了应用程序。在 70 年代,应用程序和数据库是紧耦合的,这种转移是可以接受的;但在今天,应用层和存储层分离,这种设计就很难维护了。
4. 避坑:网状模型学习中最容易踩的五个坑
4.1 把“记录类型”当成“表”,忽略系的语义
现象:看到课件里的 R1、R2、R3,下意识地把它当成关系模型里的表,以为节点之间的连线就是外键。
原因:关系模型的思维惯性太强,看到“记录类型”就想到“表”,看到“连线”就想到“外键”。但网状模型的连线是“系”,系有名字、有方向、有属籍类别,它比外键多了一层物理存储的含义。
解决:把每个系当成一个独立的、有名字的二元关系。一个记录类型参与多个系,就像一张表参与多个外键关系,但系还决定了物理链接方式。读课件时,把每个系的名字和它的双亲、子女记录类型列出来,画成有向图,不要画成 ER 图。
4.2 忽略排序字段对遍历顺序的影响
现象:写查询逻辑时,以为遍历系内子女的顺序是插入顺序,结果发现顺序不对。
原因:网状模型的系内顺序由排序字段决定,不是插入顺序。如果排序字段是学号,那么遍历顺序就是学号顺序,和插入时间无关。
解决:在设计系的时候,明确指定排序字段。如果业务逻辑依赖插入顺序,就把插入时间戳作为排序字段。但要注意,排序字段的值一旦改变,系内顺序就会变,可能影响正在进行的遍历操作。
4.3 混淆“允许插入尚未确定双亲结点值的子女结点值”和“外键可以为 NULL”
现象:以为网状模型允许子女记录没有双亲,就像关系模型允许外键为 NULL。
原因:课件里说“允许插入尚未确定双亲结点值的子女结点值”,这句话容易被理解成“子女可以没有双亲”。但实际上,它说的是在插入时可以先不指定双亲,稍后再通过连接操作把子女挂到系上。这和关系模型的外键 NULL 是两回事——外键 NULL 表示“暂时不知道属于哪个双亲”,而网状模型的“尚未确定”表示“稍后会确定”。
解决:把“尚未确定”理解成一个临时状态,而不是一个永久状态。在数据库设计时,如果某个子女记录可能永远没有双亲,那它就不应该参与那个系,或者应该把移出类别设为“随意的”。
4.4 用关系模型的查询优化思维去理解网状模型的存取效率
现象:看到课件说“存取效率较高”,就以为网状模型比关系模型快,不考虑访问路径。
原因:网状模型的效率高,是建立在“应用程序知道访问路径”的前提下的。如果你要查询的数据不在当前路径上,网状模型可能比关系模型更慢,因为它没有查询优化器来自动选择路径。
解决:把网状模型的查询当成“沿着指针走”,每次查询前先问自己:我从哪个记录类型出发?沿着哪个系?走到哪个记录类型?如果路径不存在,要么修改系定义,要么在应用程序里做多次遍历。
4.5 忽略 DBTG 系统的历史局限性
现象:用现代数据库的标准去评判网状模型,觉得它“落后”“难用”。
原因:DBTG 系统是 70 年代的产物,当时的硬件条件、应用场景和今天完全不同。没有查询优化器,是因为当时的查询很简单;没有声明式约束,是因为当时的应用程序和数据库是紧耦合的。
解决:把网状模型当成一个历史阶段的产物来理解,重点看它的设计取舍:为什么选择导航式操纵?为什么把完整性约束交给应用程序?这些取舍在今天有哪些变体?比如图数据库的 Cypher 查询语言,就是导航式和声明式的混合;文档数据库的嵌套文档,就是层次模型的变体。理解网状模型,是为了理解“数据模型的设计空间”有多大。
5. 从网状模型到关系模型:导航式操纵的代价与声明式约束的胜利
网状模型和关系模型的根本分歧,不在于“能不能表示多对多”,而在于“谁负责维护数据之间的关联”。网状模型把关联的维护责任交给应用程序,通过系指针和属籍类别来管理;关系模型把关联的维护责任交给数据库引擎,通过外键和查询优化器来管理。这个分歧决定了两种模型在可维护性、可扩展性和开发效率上的巨大差异。
课件里说网状模型的缺点是“结构比较复杂,而且随着应用环境的扩大,数据库的结构就变得越来越复杂,不利于最终用户掌握”。这句话背后的机制是:网状模型的模式是一张有向图,图的复杂度随着实体类型和联系类型的增加而指数级增长。每增加一个实体类型,你可能需要定义多个系来连接它;每增加一个联系类型,你可能需要引入中间记录类型。最终,这张图会变得连设计者自己都难以理解。
关系模型用一张二维表来统一表示所有实体和联系,用外键来声明关联,用查询优化器来自动选择访问路径。它的模式复杂度是线性的——增加一个实体就是增加一张表,增加一个联系就是增加一个外键或中间表。这种线性复杂度让关系模型在大型系统中占据了统治地位。
但网状模型并没有完全消失。今天的图数据库(如属性图模型)在某种程度上是网状模型的回归:节点和边直接表示实体和联系,导航式查询(从某个节点出发,沿着边遍历)是图数据库的典型操作。区别在于,图数据库提供了声明式的查询语言(如 Cypher、Gremlin),并且有索引和查询优化器来加速遍历。它把网状模型的“导航式操纵”和关系模型的“声明式约束”结合了起来。
我一般会这么理解:数据模型的设计空间有三个维度——表达力、可维护性、查询效率。网状模型在表达力和查询效率上得分很高,但在可维护性上失分严重。关系模型在可维护性上得分很高,但在表达力上做了妥协(多对多需要中间表)。图数据库试图在三个维度上都拿高分,但代价是存储和计算开销更大。没有银弹,只有取舍。
这份课件把网状模型放在第一章讲,不是为了让你去用 DBTG 系统,而是为了让你在学关系模型之前,先理解“导航式操纵”和“声明式约束”的区别。当你后面学到 SQL 的JOIN、外键约束、查询优化器时,你会更容易理解为什么关系模型要这样设计——因为它要解决网状模型解决不了的可维护性问题。
从那以后我每次设计数据模型,都会先问自己:这个关联是让应用程序维护,还是让数据库引擎维护?如果让应用程序维护,我能不能保证所有代码路径都正确处理?如果让数据库引擎维护,我能不能接受它带来的性能开销?想清楚这两个问题,再动手写 DDL。希望帮到你。
本文还有配套的精品资源,点击获取