建模设计

在本节中,您将学习如何使用各种建模决策来表示图数据。您构建数据模型的方式会影响您的查询和性能。我们的目标是向您展示如何评估您的模型并进行适当的更改,以便您可以为您的用例定义最佳解决方案并最大限度地提高查询性能。

为什么数据模型会带来不同

与任何数据库一样,您设计的数据模型对于确定查询逻辑和存储中的数据结构至关重要。这种做法也适用于图数据库,但有一个例外。Neo4j 是无模式的,这意味着您的数据模型可以随着您的业务轻松适应和更改。

需要开始收集新字段并捕获新分析吗?或者需要改变您对客户或其他实体的理解方式并修改其定义吗?或者法规要求系统捕获更少信息或限制可读性(更改数据格式/类型)?

您可能曾在一家公司工作,其中每个区域或部门对领域的定义都不同。以一个通用的客户领域为例。对于业务中不同的区域,客户可以被定义为不同类型的个体。这些定义也可能随时间而变化,或者公司可能会决定统一跨部门的客户含义。

如果您使用过其他类型的数据库,您应该已经熟悉这些场景所涉及的开发和管理工作。然而,Neo4j 允许您轻松地调整图的局部或整体的详细和广泛更改。无论是随时间推移的小幅更改,还是包含实体所需各种信息的广泛定义,数据库都能够处理。这仅仅取决于开发人员和架构师来确定数据模型的结构以及如何为查询定义实体。

在接下来的几段中,我们将介绍几种不同的数据看待方式,并展示每种方式如何影响图数据遍历的查询和性能。

属性 vs 关系

您可能遇到的最早的决策之一是将某事物建模为节点上的属性,还是建模为与单独节点的关系。例如,下面的数据将电影类型建模为 Movie 节点上的一个属性。

编写一个查询来查找特定电影的类型非常简单。它会找到它想知道的 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

两种数据模型都没有优劣之分,但“最佳”选择高度取决于您打算对数据运行的查询类型。

如果您计划对单个项目进行分析并仅返回该实体的详细信息(例如特定电影的类型),那么第一个数据模型将非常适合您的需求。然而,如果您需要运行分析来查找实体之间的共同点或查看一组节点,那么第二个数据模型肯定会提高这些类型查询的性能。

复杂数据结构

很多人可能会同意,并非所有数据模型都简单明了。数据是杂乱的,模型必须尝试更好地组织它,以帮助我们发现模式并做出决策。

一个难以建模的复杂数据结构的绝佳例子是漫威漫画数据。在漫威宇宙中,有些漫画有出现角色或扮演主角的角色。漫画可以组织成特定时间的一系列故事情节或叙事,漫画中也可能发生定义角色路径或系列的主要事件。创作者(包括作家、插画师等)是漫画的作者,他们定义故事情节、角色改编和发生的事件。多个创作者也可以互换参与创作一部漫画或系列。

这个数据集已经看起来很复杂,有多个实体和关系在起作用。当试图建模这里存在的层次结构和中间实体时,它又增加了一层复杂性。

如果您有时间,可以在 Vimeo 上查看Peter 的演示的完整视频链接,但我们想强调 Peter 在数据集中讨论的两个主要挑战。

首先,他发现漫画人物往往极具动态性。许多人物无法通过姓名、服装或任何特定属性来识别,因为所有这些都经常变化。

其次,Peter 提出了时间顺序的问题。对于漫画新手来说,有些人可能想知道从哪里开始或接下来是哪部漫画。然而,漫画刊号并不总是按顺序编号的,甚至有些故事情节会跨多个系列反复出现。这使得分离某些故事块或事件以及人物形象变得异常困难。

示例:中间节点

在此模型中一种有用的建模技术是超边(hyperedge)的概念。超边通常用于建模存在于两个以上实体之间的关系。Neo4j 不支持两个以上节点之间的关系,而是使用中间节点来建模这种关系。它们通常用于表示多个实体在某个时间点的连接。

一个常见的例子是大学课程。同一门课程可能有很多授课班级,由同一位教师在同一栋楼里授课等。每个班级(或授课)都将成为该课程的一个实例。

漫威的 Peter 处理数据中中间节点的方式是,创建一个 Appearance 节点,表示 PersonAlias 在特定时间的交集。这个 Appearance 可以与多个 Moment 节点相关联,其中人物和别名作为一个单元出现。这在下面所示的模型中有所体现(也在视频中)。

在关系型存储中,尝试对所有这些复杂方面进行分类和关联将非常困难,并会使数据的整体分析和审查进一步复杂化。图模型使他们能够建模这个高度动态的宇宙,并跟踪数据中所有不断变化的连接。对于这个用例,图是完美的契合。

时效数据与版本控制

建模特定时间数据和关系的一种方法是将数据包含在关系类型中。因为 Neo4j 专门针对实体之间的关系遍历进行了优化,所以您通常可以通过将日期指定为关系类型并仅遍历特定日期关系来提高查询性能。

一个常见的例子是建模航班。一家航空公司在某一天有一趟从特定地点出发并飞往特定地点的航班。我们可以从下面图 4所示的模型开始,以展示航班如何往返于机场之间。

我们很快就会意识到,我们需要建模一个存在于两个目的地之间的 Flight 实体,因为多架飞机可以在一天内多次往返于两个目的地之间。

然而,您的查询可能仍然显示出该模型在筛选特定机场所有航班方面的弱点——特别是对于伦敦和其他主要城市而言,这些城市在任何时间段内都有数百个航班连接到 Airport 节点。检查每个 Flight 节点的多个属性可能会消耗大量资源。

如果我们为特定机场的某一天创建一个节点,并创建一个类型中包含日期的关系,那么我们就可以编写查询来查找在任何指定日期(或日期范围)从某个机场出发的航班。这样,您就不需要检查每个航班与机场的关系。相反,您只需查看您关心的日期的关系。这个模型结果如下所示。

有关航班建模过程的完整演练,请参阅博客文章:在 Neo4j 中建模航班

版本控制

类似于上面我们创建带有日期的关系类型的模型,我们也可以用它来跟踪数据的版本。跟踪数据结构的变化或显示当前和过去的值对于审计目的、趋势分析等来说都非常重要。

例如,如果您想在一个人及其当前地址之间创建一个新的有效日期关系,但同时也保留过去的地址,您可以使用相同的原则,即在关系类型中包含日期。要查找此人的当前地址,查询将查找最近日期的关系。

兼顾两全其美

有时,您可能会发现一个模型非常适合您需要的一种场景,而另一个模型则更适合其他场景。例如,某些模型在写入查询方面表现更好,而其他模型在读取查询方面处理得更好。这两种能力对您的用例都很重要,那么您该怎么办呢?

在这些情况下,您可以结合两种模型并利用各自的优点。是的,您可以在图中使用多个数据模型!

权衡在于,现在您需要维护两个模型。每次创建新节点或关系或更新图的各个部分时,您都需要进行更改以适应这两种模型。这还可能影响查询性能,因为您可能需要双倍的语法来更新每个模型。

虽然这肯定是一个可能的选择,但您应该了解维护成本,并评估这些成本是否能被每项所需查询的性能改进所抵消。如果是这样,能够使用多个数据模型是一个很好的解决方案!

© . All rights reserved.