从层次模型到网状模型:多对多、属籍类别与存取路径

发布时间:2026/11/4 3:07:19
从层次模型到网状模型:多对多、属籍类别与存取路径

简介:《数据库原理(第四版)》课件面向数据库初学者、高校学生及需要系统梳理理论体系的从业人员,围绕数据库系统开发全流程与核心数据模型组织内容。压缩包共1个PPT文件,约289KB,以幻灯片形式覆盖第一章至第十章,便于课堂讲授、章节复习和知识点串联。课件从需求分析、概念结构设计、逻辑结构设计、物理结构设计到数据库实施逐步展开,并讲解数据字典、数据独立性、概念模型、层次模型、网状模型和关系模型;其中网状模型部分细分为数据结构、数据操纵、完整性约束、存储结构及优缺点,配有学生、宿舍、教师、教研室和家庭关系等图示案例,帮助理解一对多联系与复杂关系表达。目前已有168人学习,适合用于课程预习、期末复习及数据库基础概念查漏补缺。

1. 数据模型的三层抽象:为什么“画个图”能决定数据库的生死

翻开《数据库原理(第四版)课件》,绪论里最容易被跳过的,是层次模型和网状模型那几页。很多人觉得它们已是历史遗迹,学关系模型和 SQL 就够了。但一个反直觉的结论是:今天图数据库里“一个节点可以有多个父节点”的设计,几乎就是网状模型换了种形式。数据库原理真正有价值的地方,不是某一种模型的语法,而是数据模型如何在“描述现实世界”和“能被机器高效存取”之间做取舍。这一章先把结论摆出来:模型演化不是淘汰赛,而是一条取舍链,每一环都在回答同一个问题——把复杂性放在数据库里,还是放在写程序的人身上。

2. 网状模型:一个结点为什么可以有两个双亲

2.1 从“树”到“图”:双亲数量限制是怎么被打破的

层次模型的结构是一棵树:有且仅有一个结点没有双亲,这个结点叫根;其余每个结点有且只有一个双亲。这个限制在描述组织架构、目录树时很自然,但一碰到“一个学生属于一个系,又住在一栋宿舍,还选了多门课”就立刻不够用了。网状模型的定义正是冲着这条限制来的,它要求满足两个条件的基本层次联系的集合:允许一个以上的结点无双亲;一个结点可以有多于一个双亲。前一条打破了“唯一根”,后一条打破了“单亲”。

把这两个条件翻译成结构语言:层次模型里每个结点最多一条向上的边,网状模型里向上边可以有多条,也可以一条都没有。于是学生结点可以同时连向“系”和“宿舍”两个双亲,教师结点可以同时连向“系”和“教研室”。这不是为了炫技,而是现实世界里实体本来就处在多张关系网里,单亲树装不下。

注意:网状模型仍然以“记录类型”为结点、以“一对多父子联系”为边,它并不是任意图。任意图那种多对多直连,网状模型并不直接支持,这一点后面会专门拆。

层次模型其实是网状模型的一个特例:把网状模型里所有“多双亲”和“无双亲”的结点约束成“只有一个根、其余单亲”,得到的就是层次模型。反过来不成立,这个方向经常被记反,后面避坑章节会展开。

2.2 记录类型、字段与父子连线:网状模型的表示法

网状模型的表示方法和层次模型一致,只是约束松了。实体型用记录类型描述,每个结点表示一个记录类型;属性用字段描述,每个记录类型可以包含若干个字段;联系用结点之间的连线表示记录类型之间的一对多父子联系。注意这里的措辞是“记录类型之间”,不是“记录值之间”——模式层面画的是类型图,运行时的具体记录值再按这张图挂到各自的系下面。

课件里给了一组典型例子:学生、宿舍、系、教师、教研室。学生到宿舍是多对一(多个学生住一间宿舍),学生到系是多对一,教师到系是多对一,教师到教研室是多对一。画成网状图,系结点同时是学生和教师的双亲,宿舍是学生的另一个双亲。这就是“一个结点多个双亲”的直观来源。

每个记录类型还要定义一个排序字段,也叫码字段。这个字段决定同一双亲下子女记录的排列顺序,同时也是记录在系内的标识依据。它和关系模型里的主键不是一回事:关系模型的主键是全局唯一标识,网状模型的码字段更多是“在某个系内按什么顺序排、按什么找”。课件里那句话很关键——任何记录值只有按其路径查看时,才能显出它的全部意义。也就是说,一条学生记录脱离了“系-学生”这条路径,你就不知道它属于哪个系;意义是路径赋予的,不是记录自己带的。

用一张表把几个概念对齐:

概念网状模型里的载体作用
实体型记录类型(结点)描述一类实体
属性字段描述实体特征
联系结点间连线(系)表示一对多父子关系
排序依据码字段系内子女记录的排序与定位
记录意义存取路径离开路径无法完整解释记录

这张表也解释了网状模型为什么“结构复杂”:模式图本身就是程序的一部分,路径变了,程序里的导航逻辑就得跟着变。

2.3 多对多联系:为什么要先拆成两个一对多

网状模型只能直接处理一对多的实体联系。课件里明确写了:多对多联系要用网状模型间接表示,方法是将多对多联系直接分解成一对多联系。这句话听起来像绕口令,落到具体设计上就是引入一个“连接记录类型”,让它同时成为两个实体的子女。

以学生选课为例,学生和课程之间是多对多:一个学生选多门课,一门课被多个学生选。网状模型不画学生直连课程,而是插入一个“选课”记录类型,学生到选课是一对多,课程到选课也是一对多。这样任意一条选课记录都有两个双亲,路径清晰,操纵也能落地。

# 网状模型不直接支持多对多连线,必须先拆成两个一对多 # 以学生选课为例,引入“选课”作为连接记录类型 # 分解前的直觉结构:网状模型画不出这种直连 naive = { "学生": {"学号": "S01", "课程": ["C01", "C02"]}, "课程": {"课程号": "C01", "学生": ["S01", "S02"]}, } # 分解后:选课记录同时是学生和课程的子女 student = {"学号": "S01", "姓名": "A同学"} course = {"课程号": "C01", "课程名": "数据库原理"} enrollment = { "选课号": "E01", # 排序字段,也是这条连接记录在系内的定位依据 "学号": "S01", # 指向学生双亲的引用 "课程号": "C01", # 指向课程双亲的引用 "成绩": 92, }

这段代码的重点不在数据结构本身,而在“选课”这个记录类型的双重身份。参数上,选课号是排序字段,决定同一学生下选课记录的排列顺序;学号和课程号是两个双亲引用,分别挂到“学生-选课”系和“课程-选课”系下面。查询“某学生选了哪些课”,先从学生记录沿“学生-选课”系找到全部选课记录,再顺着每条选课记录里的课程号定位课程;查询“某门课有哪些学生”,方向反过来。两条路径都能走,但走法不同,这就是导航式访问的代价:查询效率取决于你从哪头进。

课件里还给了家庭关系的例子:父母与子女、树种植与砍伐、养育与赡养。父母到子女是一对多,但“养育”和“赡养”是两种不同性质的父子联系,网状模型允许两个结点之间有多种联系,这叫复合联系。层次模型做不到这一点,因为它一条边只能表达一种父子语义。

2.4 层次模型是网状模型的特例,这句话反过来不成立

课件在网状模型与层次模型的区别里列了五条:网状允许多个结点没有双亲;允许结点有多个双亲;允许两个结点之间有多种联系;可以更直接地描述现实世界;层次模型实际上是网状模型的一个特例。这五条里,前三条是结构差异,后两条是结论。

为什么“更直接”值得单独说?因为层次模型在描述非树形关系时,要么冗余存储,要么引入虚拟结点,要么把多对多硬拆成两条独立路径。网状模型把这些约束放开后,现实世界里的“学生既属于系又住宿舍”可以原样画出来,不需要为了迎合模型去扭曲业务。代价是模式图变复杂,用户必须理解路径才能取数。

这里有个容易被忽略的推论:既然层次模型是网状模型的特例,那么网状模型的所有操纵原语在层次模型里都能用,只是层次模型额外保证“单亲、单根”。反过来说,学网状模型不是学一个过时玩意儿,而是在学一个更一般的结构,关系模型后来用“表+外键”把路径藏起来了,但底层的父子语义并没有消失。

3. 数据操纵与完整性约束:DBTG 为什么给删除留了后悔药

3.1 四种操纵的本质:改的是链接指针,不是行

网状模型的数据操纵包括查询、插入、删除、更新四类。听起来和关系模型一样,但动作对象完全不同。关系模型改的是表里的行,网状模型改的是记录之间的链接指针。插入一条子女记录,不只是往存储里写一条数据,还要把它挂到对应的系上;删除一条双亲记录,不只是删一行,还要决定它下面的子女记录怎么办。

课件的完整性约束部分提到,DBTG 规范对数据操纵加了一些限制,提供了一定的完整性约束:码、双亲结点与子女结点之间是一对多联系、属籍类别。这三条里,码和一对多是结构约束,属籍类别才是操纵约束的核心。理解网状模型的数据操纵,关键是理解“一条记录在系里的进出规则”,而不是背四条操作名。

3.2 加入类别与移出类别:DBTG 给记录和系定的“户口规则”

属籍类别是 DBTG 里最容易被略过、但设计动机最强的一部分。它分两个维度:加入类别和移出类别。

加入类别管的是“子女记录怎么进系”,有两种:自动的和手工的。自动加入的意思是,子女记录一插入就自动挂到系上,不需要额外命令;手工加入的意思是,记录先作为游离记录存在,应用显式调用加入操作才挂链。手工加入不是多此一举,它给“先录入、后归属”的场景留了口子,比如新生数据先入库、分院系之后再挂到“系-学生”系上。

移出类别管的是“子女记录怎么出系”,有三种:固定的、必须的、随意的。固定的是说这条子女记录一旦入系就不能移出;必须的是说移出之前必须先连到另一个双亲,不能悬空;随意的是说可以自由移出,移出后成为游离记录。这三种规则直接决定了删除双亲时子女的命运。

类别取值语义典型场景
加入类别自动插入即挂链强归属关系,如选课记录必须属于某学生
加入类别手工显式挂链先录入后归属,如新生先入库再分系
移出类别固定不许移出关键从属记录,不允许脱离双亲
移出类别必须移出前必须改挂不允许悬空的强约束关系
移出类别随意可自由移出弱归属关系,如临时借调

这套规则的本质,是把“参照完整性”从一句口号变成了可执行的挂链、解链策略。关系模型用外键约束和级联动作表达类似语义,网状模型则把它下沉到系一级,由 DBMS 在操纵时强制执行。

3.3 允许悬空双亲值:批处理场景下的后悔药

课件里有一条很反直觉的完整性约束:允许插入尚未确定双亲结点值的子女结点值;允许只删除双亲结点值。按关系模型的直觉,这两条几乎是“破坏完整性”的同义词——外键怎么能为空?双亲怎么能删了子女还在?

但放在网状模型的历史场景里,这两条是刻意留出的回退空间。早期数据库大量用于批处理录入,数据往往分阶段到位:先把子女记录批量灌进去,双亲记录稍后补;或者先删双亲做逻辑下线,子女记录留给后续迁移程序处理。如果 DBMS 强制度绝悬空,这些批处理流程就得在应用层拆成更细的步骤,反而更容易出错。允许悬空,等于把“暂时不一致”的责任交给应用,DBMS 只保证结构上的链接合法。

class SetLink: """模拟一个 DBTG 风格的系:一个双亲记录 + 若干子女记录""" def __init__(self, owner, member_type, insert_mode, remove_mode): self.owner = owner self.members = [] self.member_type = member_type self.insert_mode = insert_mode # "automatic" 或 "manual" self.remove_mode = remove_mode # "fixed" / "mandatory" / "optional" def insert_member(self, record): # 自动加入:插入子女记录时直接挂到系上 if self.insert_mode == "automatic": self.members.append(record) record["owner"] = self.owner # 手工加入:先作为游离记录存在,等应用显式 attach return record def attach(self, record): # 手工加入的挂链入口 self.members.append(record) record["owner"] = self.owner def remove_owner(self): # 固定:双亲记录不允许删除 if self.remove_mode == "fixed": raise RuntimeError("双亲记录被固定,拒绝删除") # 必须:子女必须先改挂到另一个双亲,否则拒绝 if self.remove_mode == "mandatory": raise RuntimeError("子女记录必须先改挂到另一个双亲") # 随意:子女可以脱离,留下悬空的双亲引用 for member in self.members: member["owner"] = None self.members.clear()

这段代码把属籍类别的三个取值落到了具体分支上。insert_mode控制插入时是否自动挂链,remove_mode控制删双亲时子女的解链行为。remove_owner里“随意”分支把owner置空而不是删子女,正是课件里“允许只删除双亲结点值”的实现方式:双亲没了,子女还在,引用变成悬空,后续由应用决定是补挂还是清理。参数上,owner是双亲记录的引用,members是子女记录列表,member_type用来区分这个系挂的是哪类子女。

提示:悬空双亲值是“允许”不是“鼓励”。应用层如果没写补偿逻辑,查出来的子女记录会带一个空引用,导航到一半断链,这类问题在批处理系统里很常见。

3.4 码字段与一对多约束:完整性从哪来

网状模型的完整性约束有三根支柱。第一根是码,每个记录类型定义排序字段,保证记录在系内可定位、可排序。第二根是双亲与子女之间的一对多联系,这是结构层面的硬约束,一条子女记录只能有一个双亲(在同一个系里),不存在“一对多变成多对多”的模糊地带。第三根是属籍类别,管的是记录进出系的规则。

这三根支柱合起来,构成了网状模型的完整性基础。它不像关系模型那样有统一的声明式约束语言,而是把约束分散在模式定义和操纵语义里。好处是执行路径短,DBMS 在挂链、解链时顺手就检查了;坏处是约束和存取路径绑得太死,改一条联系往往要动一片程序。这也是后面设计取舍章节要展开的核心矛盾。

4. 存储结构:四种链接方式到底在省什么

4.1 单向链接:只存一个方向,遍历便宜反向贵

网状模型存储结构的关键,是实现记录之间的联系。课件列了四种常用方法:单向链接、双向链接、环状链接、向首链接。这四种不是四种数据库产品,而是同一套父子关系在指针层面的四种摆法。

单向链接最省指针:每个子女记录只存一个指向“下一个兄弟”的指针,双亲记录存一个指向“第一个子女”的指针。遍历时从双亲出发,沿第一个子女一路往后走,走到空就结束。插入一条新子女记录,改两个指针即可;删除一条子女记录,要找到它的前驱再改指针,所以单向链接的删除代价取决于能不能快速定位前驱。

class Record: def __init__(self, key): self.key = key self.first_child = None # 双亲记录:指向第一个子女 self.next_sibling = None # 子女记录:指向下一个兄弟 self.prev_sibling = None # 双向链接时才启用 self.owner = None # 向首链接:指回双亲 def walk_children(owner): """单向链接的正向遍历:从双亲走到最后一个子女""" cursor = owner.first_child result = [] while cursor is not None: result.append(cursor.key) cursor = cursor.next_sibling return result

walk_children只依赖first_child和next_sibling两个指针,正向遍历是 O(n),但拿不到反向信息:给定一个子女记录,你不知道它的双亲是谁,也不知道它的前一个兄弟是谁。反向查找要么从头再扫一遍,要么在记录里额外存指针。这就是单向链接“省指针、费反向”的取舍。

4.2 双向链接与环状链接:用指针换回退能力

双向链接在单向的基础上给每个子女记录加一个prev_sibling,指向前一个兄弟。代价是每条子女记录多一个指针,插入和删除时要同时维护前后两个方向。收益是反向遍历和前驱定位变成 O(1),删除一条子女记录不用再从头找前驱。

环状链接把“最后一个子女的 next 指向第一个子女”或者“指向双亲”,形成一个环。这样从任意一个子女出发都能转一圈回到起点,适合需要循环处理兄弟集合的场景,比如轮询分配、循环扫描。环状链接的坑在于边界判断:遍历终止条件不再是“指针为空”,而是“回到起点”,代码里多一个哨兵判断,写错就容易死循环。

链接方式正向遍历反向定位插入代价删除代价额外指针
单向链接O(n)需重扫低需找前驱1 个
双向链接O(n)O(1)中O(1)2 个
环状链接O(n)视实现中中1~2 个
向首链接O(n)O(1) 到双亲低低1 个

这张表不是让你背,而是说明一个事实:网状模型的“存取效率高”不是免费的,它是靠模式设计阶段就把路径和指针摆法定死换来的。你选了单向链接,就接受了反向查找要重扫;你选了双向链接,就接受了每条记录多一个指针、每次插入多维护一次。

4.3 向首链接:为什么子女要指回双亲

向首链接是给子女记录加一个指向双亲(首记录)的指针。它解决的是“从子女反查双亲”的问题:给定一条选课记录,不用回到“学生-选课”系的入口,直接顺着owner指针就能找到学生记录。课件里把向首链接和单向、双向、环状并列,说明它是按需选配的,不是所有系都默认加。

向首链接的代价很直接:每条子女记录多一个指针,插入时要写,删除时要清。收益是导航路径可以双向走,应用不用记住“我是从哪个双亲下来的”。在多层嵌套的网状结构里,这个收益会被放大——从叶子记录一路向首回到根,比从根重新导航下来要快得多。

注意:向首链接只解决“回到双亲”,不解决“回到兄弟”。如果需要兄弟间自由移动,还是得叠双向或环状链接。四种方式可以组合,不是四选一。

4.4 四种链接的代价对比与选型

把这四种方式放在一起看,会发现它们优化的是不同方向的访问模式。单向链接优化“从双亲到子女”的正向扫描;双向链接优化“前后移动”;环状链接优化“循环遍历”;向首链接优化“从子女回双亲”。选型不是选最先进的,而是选最匹配查询模式的。

课件里提到的典型网状数据库系统,是上世纪七十年代依据 DBTG 规范推出的一批产品,它们奠定了数据库系统的基本概念、方法和技术。这些系统在存储层大量使用上述链接方式,把存取路径固化在模式里。今天看,这种做法的血泪教训是:路径一旦写死,业务查询一变,改造成本极高。但它的历史贡献不能抹掉——关系模型后来能普及,恰恰是因为它把“路径选择”从程序员手里拿走,交给了查询优化器。

5. 避坑与排查:网状模型学习中最容易踩空的地方

5.1 把“能画出多对多”当成“能直接处理多对多”

现象:模式图画得很漂亮,学生和课程之间直接连了一条线,写操纵时却发现没有对应的系可以挂链,查不出“某学生选了哪些课”。

原因:网状模型直接支持的是一对多父子联系,多对多必须通过连接记录类型分解。模式图上画一条多对多直连,只是概念模型的表达,不是网状模型的实现结构。

解决:画完概念图后强制做一步分解——每一条多对多联系,都问一句“中间记录类型是谁”。选课、订单明细、借阅记录,这些都是典型的连接记录类型。分解完再检查每条连接记录是不是同时挂了两个双亲。

5.2 排序字段与码字段混为一谈

现象:按码字段查记录,查出来的顺序和预期不一致;或者以为码字段全局唯一,结果不同系下出现了重复值。

原因:网状模型的码字段是记录类型内定义的排序字段,它的职责是“在系内按什么顺序排、按什么定位”,不是关系模型意义上的全局主键。同一个码值在不同双亲下可能重复出现,这在网状模型里是允许的。

解决:把“系内排序”和“全局唯一标识”分开设计。如果业务需要全局唯一,额外加一个唯一标识字段,不要指望码字段包打天下。查询时先定位系,再按码字段在系内找,不要跳过系直接全局扫。

5.3 把层次模型说成网状模型的扩展

现象:复习时把“层次模型是网状模型的特例”记反了,答题或设计时把网状模型当成层次模型加几条边。

原因:两个模型的名字和结构太像,层次模型出现更早,容易形成“先有层次、后有网状,所以网状是扩展”的直觉。课件里的原话是反过来的:层次模型实际上是网状模型的一个特例。

解决:用约束方向记——网状模型放开“单根、单亲”得到层次模型,所以网状更一般。判断时问一句:这个结构允许一个结点多个双亲吗?允许就是网状,不允许就是层次。

5.4 多对多分解后没设移出类别,删除连锁翻车

现象:删除一条学生记录,结果把该学生的选课记录全带走了,甚至连课程记录也受影响;或者删除双亲后,子女记录变成悬空,后续查询断链。

原因:分解出连接记录类型后,没有为“学生-选课”“课程-选课”这两个系设置合适的移出类别。默认行为可能是级联删除,也可能是允许悬空,具体取决于系统实现,但设计时没显式声明,就等于把命运交给默认值。

解决:每条连接记录的两个系都显式设置移出类别。选课记录对学生的归属通常是“必须”或“固定”,对课程的归属也类似;如果业务允许课程下线但保留历史选课,就把课程侧的移出类别设为“随意”,接受悬空并在应用层补课程快照。规则写在模式里,不要靠口头约定。

5.5 低估 DDL、DML 的复杂度,改一个联系重建半张图

现象:原型阶段加一个联系很轻松,上线后业务要调整一条父子关系,改完 DDL 发现周边一片程序编译不过,DML 里的导航路径全要重写。

原因:网状模型的 DDL 和 DML 把存取路径写进了模式,路径即契约。加一条联系不只是加一个结点,还要定义系、排序字段、属籍类别,周边所有依赖这条路径的操纵代码都要跟着改。课件在优缺点里写得很直白:结构复杂,随着应用环境扩大越来越复杂;DDL、DML 语言复杂,用户不容易使用。

解决:模式设计前先做概念模型,把实体和联系理清楚,再决定哪些路径值得固化进网状结构。变化频繁的联系不要硬编码成系,宁可留给应用层做关联查询。已经固化的路径,改之前先盘点依赖,别把“加一条边”当成改一行配置。

6. 设计取舍:效率与可用性之间的那条线

6.1 导航式与声明式:存取路径到底该由谁决定

网状模型和关系模型最根本的分歧,不是数据结构,而是“存取路径由谁决定”。网状模型是导航式的:程序员必须知道从哪个系进、沿哪条链走、走到哪停。查询效率高,因为路径是设计阶段定好的,运行时不用猜。代价是路径和程序耦合,业务一变,程序跟着变。

关系模型是声明式的:程序员只写“要什么”,路径交给查询优化器。单条查询可能不如手写导航快,但模式变了,SQL 往往不用大改。它把复杂性从应用层挪到了数据库内核,用优化器的计算换开发者的心智负担。

图数据库这两年的回潮,本质是在两者之间找折中:它保留了“多跳遍历”的导航能力,但用声明式查询语言把路径细节包了一层。写查询的人不用手工维护指针,内核替你做邻接遍历。这恰好说明网状模型的思想没有死,只是换了一种对开发者更友好的外壳。

6.2 一条可验证的取舍判断线

判断一个场景该不该用“网状式”的显式路径设计,我一般看三条:查询模式是否稳定、路径深度是否固定、变更频率是否足够低。三条都满足,固化路径能换来实打实的效率;有一条不满足,就该把路径选择权交出去。

判断维度适合固化路径适合交给优化器
查询模式长期稳定、可枚举频繁变化、临时组合
路径深度固定几跳、层次清晰深度不定、递归遍历
变更频率低,模式多年不动高,迭代以周计
团队能力熟悉导航式访问更熟悉声明式查询

这张表不是标准答案,它只是把“网状模型为什么赢在效率、输在可用性”拆成了可检查的项。课件在网状模型优缺点里写的那句“存取效率较高”和“用户不容易使用”,其实是同一枚硬币的两面:效率来自路径固化,难用也来自路径固化。

从那以后我每次看到“一个结点多个双亲”的需求,都会先问一句:这是稳定的网状结构,还是只是临时画着好看的概念图。如果是前者,老老实实按系、按属籍类别、按链接方式把路径设计清楚;如果是后者,宁可先用关系表加连接表扛着,也不要把路径写死。希望帮到你。

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