## 5.8 图数据库的结构设计
图数据库采用图结构表示现实世界中的实体和关系，其图数据模式由节点、边与属性构成：节点表示现实世界中的实体，对应ERD概念模型中的实体。节点具有标签（名称）和属性，其中一个属性为唯一属性，作为该节点的唯一标识。边表示两个节点之间的关系，对应ERD概念模型中的关系。边具有方向，即它有起始节点和终止节点。边也具有属性但不必需要唯一属性，其起始节点和终止节点可作为唯一标识。本章节以属性图模型为例，介绍图数据库的设计。

### 5.8.1 图结构设计规则

ERD概念模型包含一组具有属性的实体，这些实体通常表示真实世界中的对象，并描述实体之间的联系。通常而言，任何ERD概念模型都存在一个对应的属性图模式。实体和关系可以分别映射为属性图模型中的顶点类型和边类型，节点和关系属性分别存储实体和关系的属性。然而，某些ERD概念模型可能包含无法直接映射到图数据模型的结构。例如，ERD概念模型可能包含多元关系，而属性图模式仅支持节点和二元边。因此，在将ERD概念模型转换为图数据库模式时，需要先将原始ERD模型中的多元关系转换为二元关系。

我们以图5-1-2中用户、运营商和手机之间的1:1:n三元关系为例，说明ERD概念模型中多元关系的调整方法。首先，将该三元关系“使用”映射到一个弱实体，并为该弱实体与各相关实体建立二元关系。如果该关系具有属性，则将这些属性添加到弱实体中，例如“使用”的时间属性。这种方法将三元关系转换为二元关系，而对于三元以上的多元关系，转换方法类似，即将多元关系分解为多个二元关系。
对于更为复杂的ERD概念模型的结构，如父类-子类的ISA联系等，本书不再对其调整方法进行详细讨论。

将调整过的ERD概念模型映射为图数据库模式时，遵循以下设计规则：
- 实体转换规则。一个实体转换为一个节点。如5-1-1中的员工实体映射为一个员工节点（employee），项目实体转换为项目节点（project）。
- 属性转换规则。将ERD概念模型中实体的属性转换为节点的属性。例如，图5.1中的员工实体的属性有员工编号、姓名、性别、年龄等，这些属性映射为员工节点的属性。而对于多值属性，则将其转换为新节点，并建立二元关系。具体来说，多值属性的具体值成为新节点的属性。例如，图5-1-1中员工的“技能”多值属性技能转换为“技能”节点，该节点包含“技能”属性，并与对应员工建立“具有”关系。
- 联系转换规则。实体之间的每个联系都映射到连接相应节点的边。边的名称可以是关系的名称，或基于所连接节点的角色命名。与 ERD 概念模型不同，在图数据库中，边是有向的，因此需要区分每条边的起始节点和终止节点。例如，对于员工和项目之间的关系，有两种关系映射方式：员工工作于项目（即，起始节点为员工，终止节点为项目，关系名称为“工作”）和项目雇佣员工（起始节点为项目，终止节点为员工，关系名称为“雇佣”）。
<!-- 在图 2 中，节点的属性写在节点矩形的底部，从（带下划线的）键属性开始。如果存在边的属性，则将其写在边名称后的括号中。如果存在属性的约束，则将其写在属性名称后。 -->
<!-- 将ERD概念模型转换为图数据库模型包含两个步骤：调整ERD以适配图数据库模型；将调整后的ERD转换为图数据库模型。
 -->

### 5.8.2 图结构设计
基于以上的规则，我们对图5-1-3(b)的购物网站ERD概念模型进行图结构设计。在后面的图数据库设计中，我们用(起始节点)-[:边名称{边属性}]->(终止节点)的形式表示边的设计。
```SQL
节点设计
用户节点: User(Uid, Uname, Uadd, Tel)，Uid为主码；
商品节点: Product(Pid, Pname, Category, Price, Padd)，Pid为主码；
订单详情节点: OrderLine(Oid, Date)，Oid为主码；
喜好节点: Preference(Pref)，Pref为主码；

边设计
“下单”边: User - [:HAS_ORDER] -> OrderLine
“包含”边: OrderLine - [:HAS_PRODUCT {Quantity}] -> Product
“喜好”边: User - [:HAS_Pref] -> Preference
```

首先，将图5-1-3(b)中每个实体类转换为一个节点模式，实体类的唯一属性和单值属性构成节点的属性，并将其唯一性属性设为主码。以此实体转换规则生成的节点包括：用户节点User(<u>Uid</u>, Uname, Uadd, Tel)，商品节点Product(<u>Pid</u>, Pname, Category, Price, Padd)以及订单详情节点OrderLine(<u>Oid</u>, Date)，其中下划线标注的属性为主码。对于多值属性，依据转换规则，创建喜好节点Preference(Pref)，其中Pref为唯一属性，并通过“喜好”关系HAS_Pref与用户节点关联。

接下来，依据关系转换规则，将图5-1-3(b)中的联系转换为图数据模式中的边：“下单”边User-[:HAS_ORDER]->OrderLine，“包含”边OrderLine-[:HAS_PRODUCT {Quantity}]->Product。其中，HAS_ORDER表示“下单”关系，HAS_PRODUCT表示“包含”关系，Quantity表示订单详情节点OrderLine与商品节点Product之间的联系属性“数量”。

### 5.8.3 合理利用数据冗余

与关系型数据库和文档数据库类似，基于非规范化（Denormalization）的数据冗余设计对于图数据库也是一把双刃剑。虽然数据冗余设计会增加存储和更新操作的开销，但它也能提高查询效率。例如，如果要查询2024年以来的所有订单号及其用户姓名。基于上一节的图数据库设计，需要执行下面的查询语句：
```SQL
MATCH (u:User)-[:HAS_ORDER]->(o:OrderLine)
WHERE o.Date >= '2024-01-01'
RETURN o.Oid, u.Uname
```

在该查询中，图数据库需要遍历OrderLine节点以及通过HAS_ORDER边相连的User节点，以获取订单的用户姓名。但是，如果采用数据冗余的方式，在OrderLine节点中增加一个属性Uname来存储订单对应的用户姓名，那么查询语句可以简化为：
```SQL
MATCH o:OrderLine
WHERE o.Date >= '2024-01-01'
RETURN o.Oid, o.Uname
```
<!-- 规范化原则指导了关系型数据库中的数据如何被分割成表以及这些表如何相互链接。例如，实现第三范式（3NF）通常涉及消除传递依赖，以确保每个非主码属性仅依赖于主码。这种结构化的方法在关系数据库中帮助维护一致性，并促进更简单的数据管理。
然而，在图数据库中，关系型数据库中的规范化原则有时会导致效率低下，特别是在涉及大型和互联数据集的场景中。在关系数据库中将数据分割成单独表的传统方法，并不总是与图数据库的本质相契合，图数据库的重点在于数据点之间的关系，而不仅仅是数据本身。 -->
<!-- 反规范化虽然在提高图数据库的性能和查询效率方面具有优势，但也伴随着一系列权衡。关键挑战在于找到适当的平衡，以最大化效率而不对数据库管理的其他方面产生不利影响。 -->
这种设计虽然提升特定查询的执行性能，但是易引发数据冗余和一致性维护的问题。即，用户姓名同时存储于OrderLine和User节点中，一方面增加了数据的存储开销，另一方这可能导致数据的不一致性。比如，如果用户节点的姓名发生变化，若不同时更新对应OrderLine中的用户姓名，则会导致数据不一致性问题。
因此，非规范化的数据冗余设计需要根据查询工作负载精心设计。例如，不常更新但频繁访问的属性适合进行数据冗余。通过权衡这些因素，可以在提高图数据库性能的同时，最小化非规范化带来的负面影响。

### 练习题

**1.** 在图数据库中，以下哪种情况最适合使用数据冗余？

 <ol type="A">
  <li>频繁更新的属性，以保证数据一致性。</li>
  <li>很少被查询的属性，以节省存储空间。</li>
  <li>不常更新但频繁访问的属性，以提高查询效率。</li>
  <li>主键属性，以保证数据唯一性。</li>
 </ol>

**2.** 某大学数据库中有如下ER模型：

```
Student(SID, Name, Major)
Course(CID, Title, Credits)
Enroll(SID, CID, Grade, EnrollTime)
```
其中Enroll是一个多对多关系，表示学生选课信息。

请将上述 ER 模型映射为图数据库结构，并说明每种实体或关系在图中的表示形式（节点 / 边 / 属性），并思考将Enroll 建模为边或中间节点各自的优劣。

**3.** 在某电影知识图谱中，电影被建模为节点 `(:Movie {Title, Year, Genre, BoxOffice, ReviewScore, Length})`。若系统中“按评分区间筛选影片”和“按类型查询高票房影片”等操作非常频繁，是主要查询热点。为提高查询效率，应该如何优化当前的建模方式？

**4.** 对于5.3节习题中的第3题（员工建模），请思考以下问题：
1. 在图数据库中你会如何表示这种“继承结构”？员工是否应建为单一节点类型，还是多个节点类型？对应的属性应该如何存储？请提出不同的建模方案，并分析它们各自有什么优劣。
2. 如果使用多个节点类型来建模不同的子类，假设需要对员工进行大量类型聚合查询（如 HR 查询所有员工的绩效信息），如何对模式进行优化（例如：冗余存储）？

[**上一页<<**](chapter5.7-D.md) | [**>>下一页**](chapter5.9-G.md)