在本文中,将学习如何使用各种建模决策来表示图数据,如何构建数据模型可能会影响查询和性能。
本文的目标是向您展示如何评估模型和进行适当的更改,以便您可以为用例定义最佳解决方案并最大程度地提高查询性能。
一、为什么图数据模型更有优势?
与任何数据库一样,设计的数据模型对于确定查询的逻辑和存储中的数据结构非常重要。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;
相反,如果将电影和类型建模为单独节点,并在两者间建立关系,那么我们将得到一个类似于下图的模型。
这样会为电影类型创建一个完全独立的实体(节点),使您可以将具有相同类型的所有电影连接到该Genre
节点。
让我们看看这如何改变我们的查询:要查找特定电影的流派,它首先需要找到要查找的Movie
节点(在本例中为“黑客帝国”),然后通过IN_GENRE
关系找到与该电影连接的节点。
最大的区别在于第二个查询的语法,以查找相同电影类型的电影。它比我们的第一种方式简单得多,因为它使用自然的图模式(实体-关系-实体)来查找所需的信息。首先,Cypher找到一部电影及其相关类型,然后寻找同一类型的第二部电影。
//查找指定电影的类型
MATCH (m:Movie {title:"The Matrix"}),
(m)-[:IN_GENRE]->(g:Genre)
RETURN g.name; //查找相同类型的电影
MATCH (m1:Movie)-[:IN_GENRE]->(g:Genre),
(m2:Movie)-[:IN_GENRE]->(g)
RETURN m1, m2, g
数据模型的哪个版本都不错,但“最佳”选项在很大程度上取决于实际业务要对数据执行的查询。如果您打算对单个节点进行分析并且仅返回有关该实体的详细信息(例如特定电影的类型),那么第一个数据模型将非常适合您的需求。但是,如果您需要分析并查找实体之间的共同点或查看一组节点,则第二个数据模型肯定会提高这些类型的查询的性能。
三、复杂的数据结构
正如我们许多人所想,并非所有数据模型都是简单明了。数据杂乱无章,模型则必须尝试更好地组织数据,以帮助我们了解模式并制定决策。
难以建模的复杂数据结构的一个很好的例子就是漫威的漫画数据。在漫威宇宙中,有些漫画中的人物出场或扮演主角,可以在特定时间内将漫画组织成一系列特定的故事情节或叙述,并且可以在漫画中定义重大事件,以定义角色路径或系列。创作者(包括作家,插图画家等)是漫画的作者,定义故事情节,角色改编和发生的事件,多个创作者还可以互换参与创作漫画或系列作品。
这个数据集看起来很复杂,有几个实体和关系在起作用,尝试为层次结构和中间实体建模时,它增加了一层新的复杂性。
首先,我们发现喜剧人物往往具有极强的动态性,无法通过名称、服装或任何特定属性来识别字符,因为所有这些字符都会经常更改。
另一个问题是年代问题。对于漫画世界的新手,有些人可能想确定从哪里开始,或接下来的漫画顺序。但是,漫画并不总是按顺序编号,甚至有些故事情节出现在多个系列中,然后又一次出现,这使得分离故事或事件的某些部分以及角色的呈现变得异常困难。
3.1 超边或中间节点
超边的概念是在此模型以及其他模型中显而易见的一种建模技术。通常创建超边(或中间节点)来建模两个以上实体之间存在的关系。通常创建它们是为了表示某个时间点上多个实体的连接。一个常见的例子是大学课程。在同一座建筑物中,同一位老师可能会同时提供同一门课程的多种课程,等等。课程的每个部分(或课程)将成为该课程的一个实例。
漫威(Marvel)处理其数据中超边的方式是通过创建一个Appearance
节点来表示Person
和Alias
在特定时间的交集。这Appearance
可能与Moment
的Person和Alias作为一个单元出现的多个节点有关。
这在下面显示的模型中表示。
在关系存储中,尝试对所有这些复杂方面进行分类和关联将非常困难,并且会使整个数据的分析和审查更加复杂。图模型使他们能够为这个高度动态的世界建模,并跟踪整个数据中所有变化的连接。在此用例中,图非常适合。
3.2 时间相关数据
对时间特定的数据和关系建模的一种方法是在关系类型中包括数据。由于Neo4j是专门针对遍历实体之间的关系而优化的,因此通常可以通过将日期指定为关系类型并仅遍历特定日期的关系来提高查询性能。
一个常见的示例是对航空公司航班进行建模。航空公司在某一天有往返特定位置的特定航班。我们可以从下面的第一个图开始,从一个模型开始,以显示航班如何从一个机场到另一个机场。
我们很快就会意识到,我们需要对Flight
两个目的地之间存在的实体进行建模,因为一天中多个飞机可以在两个目的地之间航行多次。
但是,您的查询可能仍会显示该模型在过滤特定机场的所有航班方面的不足-尤其是对于伦敦和其他主要城市,这些城市Airport
在任何时间段内都有数百个航班与一个节点相连。检查每个Flight
节点的几个属性可能在资源使用时代价很高。
如果要为特定的机场日期创建一个节点,并为该日期中的类型创建一个关系,则可以编写查询以查找在任何指定日期(或日期范围)内从机场出发的航班。这样,您无需检查与机场的每个航班关系。取而代之的是,您只会查看您关心的日期的关系。事实证明,该模型类似于以下模型。
3.3 版本变化
与上面创建日期关系类型的模型类似,我们也可以使用它来跟踪数据的版本。对于审计、趋势分析等而言,跟踪数据结构的变化或显示当前和过去的值非常重要。
例如,如果您想在一个人与其当前地址之间创建新的有效日期关系,但又保留过去的地址,则可以使用相同的原理在关系类型中包括一个日期。为了找到该人的当前地址,查询将查找最近约会的关系。
四、兼顾两全其美
有时,您可能会发现一个模型在所需的一种情况下确实可以很好地工作,而另一种模型在其他情况下则更好。例如,某些模型在写查询时将表现更好,而其他模型将更好地处理读查询。这两种功能对您的用例都很重要,那么您该怎么办?
在这些情况下,您可以组合使用两种模型并利用每种模型的优势。是的,您可以在图中使用多个数据模型!
需要权衡的是,现在您将需要维护两个模型。每次创建新节点、关系或更新图时,都需要进行更改以适应两个模型。这也可能影响查询性能,因为更新每种模型所需的语法可能加倍。
尽管这是一个可能的选择,但您应该了解维护成本,并评估在每个所需查询中看到的性能改进是否可以不 Care 这些成本。如果是这样,能够使用多个数据模型则是一个很好的解决方案!
图数据库Neo4j数据建模系列:
图数据库Neo4j数据建模系列(一) — 数据建模基础
图数据库Neo4j数据建模系列(二) — 数据建模准则
图数据库Neo4j数据建模系列(三) — 关系与图模型的转变
图数据库Neo4j数据建模系列(四) — 图数据建模
图数据库Neo4j数据建模系列(五) — 建模技巧及模型重构
至此,数据建模的相关概念及技巧就介绍完了,后续将基于一些实际的业务场景共同探讨图数据建模,敬请期待。
近期也在尝试 OrientDB 数据迁移到Neo4j,有时间的话,我也会更新一些迁移细节。