教程目录 引自:https://neo4j.com/docs/getting-started/data-modeling/modeling-designs/
数据建模设计
在本节中,您将学习如何使用各种建模决策来表示图数据。数据模型的构建方式会影响查询和性能。我们的目标是教您如何评估模型并进行适当的更改,以便为您的用例定义最佳解决方案并最大化查询性能。
为什么数据模型很重要
与任何数据库一样,您设计的数据模型对确定查询逻辑和数据存储结构都很重要。这种实践也适用于图数据库,但有一个例外:Neo4j 是无模式的,这意味着您的数据模型可以随业务需求轻松调整和变化。
常见场景
- 需要收集新字段并进行新分析?
- 需要改变对客户或其他实体的解释方式并修改其定义?
- 法规要求系统减少信息采集或限制可读性(更改数据格式/类型)?
您可能在一家公司工作过,其中每个区域或部门对领域的定义都不同。以通用客户域为例:对于业务内的不同区域,客户可以被定义为不同类型的个体。这些定义也可能随时间变化,或者公司可能决定统一各部门之间客户的含义。
Neo4j的优势
如果您使用过其他类型的数据库,您会熟悉这些场景所涉及的开发和管理工作。然而,Neo4j允许您轻松地对图的局部或整体进行详细和广泛的更改:
- 支持随时间推移的小规模更改
- 支持包含实体多种所需信息的广泛定义
开发人员和架构师只需要确定:
- 数据模型的结构
- 如何定义用于查询的实体
属性与关系的对比
在早期设计决策中,您可能会遇到是将某些内容建模为:
- 节点上的属性
- 还是与独立节点的关系
例如,可以将电影类型作为 Movie 节点上的属性进行建模。
这种设计选择将影响:
- 查询的编写方式
- 遍历图数据的性能
- 数据的维护和扩展性
在接下来的内容中,我们将介绍查看不同数据集的几种方法,并展示每种方法如何影响图数据的查询和遍历性能。
编写查询来查找特定电影的类型是很简单的。只需找到想要了解的 Movie 节点,然后返回 genre 属性中列出的值。但是,要找出共享类型的电影,需要一个更复杂的查询来:
- 查找每个 Movie 节点
- 遍历每个电影的类型属性数组
- 与第二部电影的类型属性数组中的每个值进行比较
这会影响性能(嵌套循环和节点属性比较),查询也会更加复杂。
//查找特定电影的类型
MATCH (m:Movie {title:"The Matrix"})
RETURN m.genre;
//查找共享类型的电影
MATCH (m1:Movie), (m2:Movie)
WHERE any(x IN m1.genre WHERE x IN m2.genre)
AND m1 <> m2
RETURN m1, m2;
现在,相反,如果您将电影及其类型建模为单独的节点,并在它们之间创建一种关系,您将得到如图2所示的模型。
这将为流派创建一个完全独立的实体(节点),允许您将所有具有共享流派的电影连接到该流派节点。让我们看看这如何改变我们的查询。为了找到特定电影的流派,它首先需要找到它正在寻找的电影节点(在本例中为“矩阵”),然后找到通过IN_GENRE关系连接到该电影的节点。
最大的区别在于第二个查询的语法,该查询用于查找哪些电影具有相同的类型。它比我们早期的版本简单得多,因为它使用自然的图形模式(实体-关系-实体)来查找所需的信息。首先,Cypher找到一部电影及其相关的类型,然后寻找同一类型的第二部电影。
//find the genres for a particular movie
MATCH (m:Movie {title:"The Matrix"}),
(m)-[:IN_GENRE]->(g:Genre)
RETURN g.name;
//find which movies share genres
MATCH (m1:Movie)-[:IN_GENRE]->(g:Genre),
(m2:Movie)-[:IN_GENRE]->(g)
RETURN m1, m2, g
数据模型的版本没有好坏之分,但是“最佳”选项很大程度上取决于您打算对数据运行的查询类型。
如果您计划对单个项目进行分析,并且只返回关于该实体的详细信息(比如特定电影的类型),那么第一个数据模型将非常适合您的需求。但是,如果您需要运行分析来寻找实体之间的共同点或者查看一组节点,那么第二个数据模型肯定会提高这些类型的查询的性能。
复杂数据结构
众所周知,不是所有的数据模型都简单直接。数据是混乱的,模型必须尝试更好地组织它以帮助我们看到模式并做出决策。
漫威漫画数据是一个难以建模的复杂数据结构的典型例子。在漫威宇宙中,漫画中有角色出场或担任主角。漫画可以按特定时间的故事线或叙事组织成系列,重大事件可能发生在定义角色路径或系列的漫画中。创作者(包括作家、插画师等)是漫画的作者,定义故事线、角色改编和发生的事件。多个创作者也可以互换参与创作漫画或系列。
这个数据集已经看起来很复杂,有多个实体和关系在起作用。在尝试对这里存在的层次结构和中间实体进行建模时,又增加了一层复杂性。
如果您有时间,可以在 Vimeo 上观看 Peter 演讲的完整视频链接,但我们想强调 Peter 在数据集中讨论的两个关键挑战。
首先,他发现漫画角色往往极其动态。许多角色无法通过名字、服装或任何特定属性来识别,因为这些都经常变化。
其次,Peter 发现了时序问题。对于漫画宇宙的新手来说,有些人可能想确定从哪里开始或接下来看哪些漫画。然而,漫画期刊并不总是按顺序编号的,甚至有些故事情节会跨越多个系列来回出现。这使得分离某些故事块或事件以及角色的演绎变得极其困难。
示例:中间节点
在这个模型中有用的一种建模技术是超边的概念。超边通常用于建模存在于两个以上实体之间的关系。Neo4j 不支持两个以上节点之间的关系,而是使用中间节点来建模这种关系。它们通常用于表示多个实体在某个时间点的连接。
这方面的一个常见例子是大学课程。同一门课程可能有多个开设班次,使用相同的教师在相同的建筑物中等。课程的每个部分(或开设班次)就成为课程的一个实例。
Marvel 的 Peter 处理他们数据中的中间节点的方式是创建一个 Appearance 节点,该节点表示特定时间点上 Person 和 Alias 的交集。这个 Appearance 可以关联到多个 Moment 节点,在这些节点中人物和化身作为一个整体出现。这在下面的模型中表示(也在视频中)。
在关系型存储中,试图对所有这些复杂方面进行分类和关联将极其困难,并进一步使整体数据的分析和审查变得复杂。图模型允许他们对这个高度动态的宇宙进行建模,并跟踪其数据中所有不断变化的连接。对于这个用例,图是完美的选择。
时间绑定数据和版本控制
建模时间特定数据和关系的一种方式是在关系类型中包含数据。由于 Neo4j 专门针对实体之间的关系遍历进行了优化,因此您通常可以通过将日期指定为关系类型并仅遍历特定日期的关系来提高查询性能。
一个常见的例子是对航空公司航班进行建模。航空公司在特定日期有从特定地点到特定地点的航班。我们可以从下面的图 4 所示的模型开始,展示航班如何在机场之间运行。
我们很快就会意识到,我们需要对存在于两个目的地之间的飞行实体进行建模,因为多架飞机可以在一天内多次往返于两个目的地之间。
然而,您的查询可能仍然显示了该模型在过滤特定机场的所有航班方面的弱点——特别是对于伦敦和其他主要城市,这些城市在任何时间跨度内都有数百个航班连接到某个机场节点。检查每个航班节点的几个属性在资源上可能是昂贵的。
如果我们要为特定的机场日创建一个节点,并在类型中创建一个带有日期的关系,那么我们可以编写查询来查找任何指定日期(或日期范围)从机场起飞的航班。这样,您就不需要检查每个航班与机场的关系。相反,你只会关注那些你在乎的日期的关系。这个模型结果如下图所示。
版本控制类似于上面的模型,我们创建了一个日期关系类型,我们也可以用它来跟踪数据的版本。跟踪数据结构的变化或显示当前和过去的值对于审计、趋势分析等非常重要。
例如,如果您想在一个人和他的当前地址之间创建一个新的有生效日期的关系,但是还想保留过去的地址,那么您可以使用相同的原则在关系类型中包含一个日期。为了找到这个人的当前地址,查询将查找最近的关系。
有时,您可能会发现一个模型对于您需要的一个场景确实很好,但是另一个模型对于其他场景更好。例如,一些模型可以更好地处理写查询,而另一些模型可以更好地处理读查询。这两种能力对您的用例都很重要,那么您会怎么做呢?
在这些情况下,您可以将这两种模式结合起来,利用各自的优势。是的,您可以在您的图表中使用多个数据模型!
代价是现在您需要维护两个模型。每当您创建一个新的节点或关系,或者更新图的一部分时,您都需要进行更改以适应这两种模型。这也会影响查询性能,因为更新每个模型可能需要两倍的语法。
虽然这肯定是一个可能的选择,但是您应该知道维护成本,并评估这些成本是否能够被您将看到的每个所需查询的性能改进所抵消。如果是这样,能够使用多个数据模型是一个很好的解决方案!