模型设计
在本节中,您将学习如何使用各种建模决策来表示图数据。您构建数据模型的方式会影响您的查询和性能。我们的目标是向您展示如何评估您的模型并进行适当的更改,以便您可以为您的用例定义最佳解决方案并最大化查询的性能。
为什么数据模型会产生影响
与任何数据库一样,您设计的数据模型对于确定查询的逻辑和数据在存储中的结构非常重要。这种实践也扩展到图数据库,但有一个例外。Neo4j 是无模式的,这意味着您的数据模型可以轻松地适应和更改以满足您的业务需求。
需要开始收集新字段并捕获新分析?或者需要更改解释客户或其他实体的方式并修改其定义?或者法规要求系统捕获更少的信息或限制可读性(更改数据格式/类型)?
您可能曾在一家公司工作过,该公司中每个区域或部门都以不同的方式定义域。例如,一个通用的客户域。对于业务内的不同区域,客户可以定义为不同类型的个人。这些定义也可能随着时间而改变,或者公司可能决定统一各部门对客户的含义。
如果您使用过其他类型的数据库,那么您已经熟悉任何这些场景所涉及的开发和管理工作。但是,Neo4j 允许您轻松地调整图的各个部分或整个图的详细和广泛的更改。无论是随着时间的推移进行的小幅更改,还是包含有关您的实体各种所需信息的广泛定义,数据库都能够处理它。它仅仅取决于开发人员和架构师来确定数据模型的结构以及如何为查询定义实体。
在接下来的几段中,我们将介绍几种查看不同数据集的方法,并展示每种方法如何影响遍历图数据的查询和性能。
属性与关系
您可能遇到的最早的决策之一是是否将某件事建模为节点上的属性或作为与单独节点的关系。例如,以下数据将电影类型建模为Movie
节点上的属性。
Movie
节点及其属性 - genre
编写查询以查找特定电影的类型非常简单。它会找到它想要了解的Movie
节点,然后返回在类型属性中列出的值。但是,要找出哪些电影共享类型,您需要一个更复杂的查询来查找每个Movie
节点,遍历属性数组中的每个类型,并将每个值与第二个电影的类型属性数组中的每个值进行比较。这将对性能造成影响(嵌套循环和节点属性的比较),并且查询也会变得更加复杂。
下面的代码块显示了每个查询的语法。您可以看到第二个查询中逻辑和复杂性的变化。
//find the genres for a particular movie
MATCH (m:Movie {title:"The Matrix"})
RETURN m.genre;
//find which movies share genres
MATCH (m1:Movie), (m2:Movie)
WHERE any(x IN m1.genre WHERE x IN m2.genre)
AND m1 <> m2
RETURN m1, m2;
现在,相反,如果您要将电影及其类型建模为单独的节点并在它们之间创建关系,则会得到类似于图 2的模型。
这为类型创建了一个完全独立的实体(节点),允许您将所有具有共享类型的电影连接到该Genre
节点。让我们看看这如何改变我们的查询。要查找特定电影的类型,它首先需要找到它正在查找的Movie
节点(在本例中为“黑客帝国”),然后找到通过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
两种数据模型版本都没有好坏之分,但“最佳”选项在很大程度上取决于您打算对数据运行的查询类型。
如果您计划对单个项目进行分析并仅返回有关该实体的详细信息(例如特定电影的类型),那么第一个数据模型将完全满足您的需求。但是,如果您需要运行分析以查找实体之间的共同点或查看一组节点,那么第二个数据模型绝对会提高此类查询的性能。
复杂数据结构
正如我们许多人可能同意的那样,并非所有数据模型都简单明了。数据很混乱,模型必须尝试更好地组织它以帮助我们发现模式并做出决策。
一个难以建模的复杂数据结构的极好示例是漫威漫画数据。在漫威宇宙中,有一些漫画中的角色会露面或扮演主要角色。漫画可以组织成一系列特定故事线或特定时间段的叙事,并且漫画中可能会发生重大事件,这些事件定义了角色路径或系列。创作者(包括作家、插画家等)是漫画的作者,定义了故事线、角色改编和发生的事件。多个创作者也可以互换参与创建漫画或系列。
此数据集已经看起来很复杂,有多个实体和关系在起作用。在尝试建模此处存在的层次结构和中间实体时,它会增加一层新的复杂性。
如果您有一些时间,您可以查看 Peter 在 Vimeo 上关于演示文稿的完整视频链接,但我们想重点介绍 Peter 在数据集中讨论的两个主要挑战。
首先,他发现漫画角色往往非常动态。许多角色无法通过姓名、服装或任何特定属性来识别,因为所有这些都会经常发生变化。
其次,Peter 确定了年代顺序问题。对于那些刚接触漫画宇宙的人来说,有些人可能想要确定从哪里开始或接下来看哪本漫画。但是,漫画期数并不总是按顺序编号,甚至有一些故事线出现在多个系列中并反复出现。这使得分离某些故事块或事件以及角色的演绎变得极其困难。
示例:中间节点
此模型中一种有用的建模技术是超边的概念。超边通常用于建模存在于两个以上实体之间的关系。Neo4j 不支持两个以上节点之间的关系,而是使用中间节点来建模此类关系。它们通常用于表示多个实体在特定时间点的连接。
一个常见的示例是大学课程。同一门课程可能有多个版本,由同一教师在同一栋楼等地方授课。然后,课程的每个部分(或版本)都将成为课程的一个实例。
Marvel 的 Peter 在处理其数据中的中间节点时,创建了一个Appearance
节点,该节点表示特定时间点Person
和Alias
的交集。此Appearance
可以与多个Moment
节点相关联,在这些节点中,人和别名作为一个单元出现。这在下面所示的模型中进行了表示(也在视频中)。
在关系型存储中,尝试对所有这些复杂方面进行分类和关联将非常困难,并且会进一步复杂化对整个数据的分析和审查。图模型允许他们对这个高度动态的宇宙进行建模,并跟踪其数据中所有不断变化的连接。对于此用例,图是完美的匹配。
与时间相关的和版本控制的数据
对特定时间的数据和关系进行建模的一种方法是在关系类型中包含数据。因为 Neo4j 专门针对实体之间关系的遍历进行了优化,所以您通常可以通过将日期指定为关系类型并仅遍历特定日期的关系来提高查询性能。
一个常见的示例是模拟航空航班。航空公司在特定日期从特定地点到特定地点执行特定航班。我们可能会从如下所示的图 4模型开始,以显示航班如何从一个机场到另一个机场。
我们很快就会意识到,我们需要对存在于两个目的地之间的Flight
实体进行建模,因为多架飞机可以在一天内多次在两个目的地之间飞行。
但是,您的查询可能仍然显示出模型在筛选特定机场所有航班方面的弱点——尤其是在伦敦和其他大型城市,这些城市在任何时间跨度内都有数百个与Airport
节点连接的航班。检查每个Flight
节点的多个属性可能会消耗大量资源。
如果我们为特定机场日期创建一个节点,并创建一个类型中包含日期的关系,那么我们就可以编写查询来查找特定日期(或日期范围)内来自某个机场的航班。这样,您就不需要检查每个航班与机场的关系。相反,您只需要查看您关心的日期的关系。此模型如下所示。
有关航空航班建模过程的完整演练,请参阅博客文章:在 Neo4j 中模拟航空航班。
兼收并蓄
有时,您可能会发现一个模型对您需要的某个场景非常有效,但另一个模型对其他场景更好。例如,某些模型在写入查询方面表现更好,而其他模型在读取查询方面处理得更好。这两种功能对您的用例都很重要,那么您该怎么办呢?
在这些情况下,您可以结合这两种模型并利用各自的优势。是的,您可以在图中使用多个数据模型!
权衡是您现在需要维护两个模型。每次创建新节点或关系或更新图的一部分时,都需要进行更改以适应这两个模型。这也会影响查询性能,因为您可能需要双倍的语法来更新每个模型。
虽然这绝对是一个可行的选项,但您应该了解维护成本并评估这些成本是否超过了您将为每个所需查询看到的性能改进。如果是这样,能够使用多个数据模型是一个很好的解决方案!