图数据库越来越受欢迎和被采用。随着来自许多不同来源的数据量越来越大,能够理解数据并了解它们之间的联系至关重要。或许有些小伙伴可能听说过图数据库 (GDB),有些人可能还没有听说过。在本文中,我们将准确介绍它们是什么,以及它们与更传统的关系数据库管理系统 (RDBMS) 的比较,后者一直是过去 40 多年的主要软件应用程序。
受 Neo4j 用作图查询指导性介绍的一个小电影数据集的启发,我们将查看并排示例以及数据模型或查询在图数据库和关系型数据库。
在本文中,我们将:
- 介绍图数据库,简要介绍现有的两种模型
- 从概念上了解关系范式和图范式之间的差异
- 查看电影数据集,从 GDB 和 RDBMS 的角度比较和对比数据模型
- 基于 Cypher(用于 GDB)或 SQL 比较和对比一些查询
- 讨论电影示例中出现的更有趣的查询,并准确地找出正在发生的事情
如果您想在阅读文章之前(或在阅读期间!)使用示例演练电影数据集,我们非常欢迎您这样做。
什么是图数据库?
首先,在我们深入研究什么是图数据库之前,让我们定义这个术语。图数据库是一种“不仅仅是 SQL”(NoSQL,全程为Not Only SQL)的数据存储。它们旨在以图结构存储和检索数据。
使用的存储机制可能因数据库而异。一些 GDB 可能使用更传统的数据库结构,例如基于表,然后在顶部有一个图形 API 层。其他的将是“原生”GDB——从存储、管理和查询到数据库的整个构造维护数据的图形结构。许多当前可用的图形数据库通过将实体之间的关系视为“一等公民”来做到这一点。
不同类型的图数据库
广义上有两种类型的 GDB,资源描述框架 (RDF)/三重存储/语义图数据库和属性图数据库。
RDF GDB 使用三元组的概念,它是由三个元素组成的语句:主语-谓语-宾语。
主语将是图中的资源或节点,对象将是另一个节点或文字值,谓词表示主题和对象之间的关系。节点或关系上没有内部结构,一切都由唯一标识符以 URI 的形式标识。
这种结构背后的动机是交换和发布数据。
GDB 属性专注于存储接近逻辑模型的数据的概念。这反过来将基于数据本身所寻求的问题,并专注于使该表示尽可能高效地存储和查询。
与基于 RDF 的图不同,节点和关系上有内部结构,可提供丰富的数据表示以及相关的元数据。
属性图数据库剖析
在本文的其余部分,我们将重点关注原生属性图数据库,特别是 Neo4j。让我们检查一下主要组件。
属性图数据库的主要组成部分如下:
- 节点:在图论中也称为顶点——构建图的主要数据元素
- 关系:在图论中也称为边——两个节点之间的链接。它将有方向和类型。没有关系的节点是允许的,没有两个节点的关系是不允许的
节点和关系
- 标签:定义一个节点类别,一个节点可以有多个
- 属性:丰富一个节点或关系,不可以为空值!
标签、类型和属性
图数据库与关系数据库
关系数据库回顾
许多开发人员都熟悉传统的关系数据库,其中数据存储在定义良好的模式中的表中。
表中的每一行都是一个离散的数据实体。行中的这些元素之一通常用于定义其唯一性:主键。它可能是一个唯一的 ID,也可能是一个人的身份证号码之类的东西。
然后我们通过一个称为规范化的过程来减少数据重复。在规范化中,我们将引用(例如某人的地址)移动到另一个表中。因此,我们从代表实体的行到代表该人地址的行获得了一个引用。
例如,如果某人更改了他们的地址,您不希望该人的地址到处都是多个版本,并且必须尝试记住该人地址所在位置的所有不同实例。规范化确保您拥有一个版本的数据,因此您可以在一个地方进行更新。
然后当我们查询时,我们要重构这个归一化的数据。我们执行所谓的 JOIN 操作。
在我们的主实体行中,我们有一个主键,用于标识实体的 ID,比如说人。我们还有一个叫做外键的东西,它代表我们地址表中的一行。我们通过主键和外键连接这两个表,并使用它在地址表中查找地址。这称为 JOIN,这些 JOIN 在查询时和读取时完成。
当我们在关系数据库中执行 JOIN 时,它是一个集合比较操作,我们在其中查看我们的两组数据重叠的位置(在这种情况下,集合是人员表和地址表)。在高层次上,这就是传统关系数据库的工作方式。
原生图数据库的工作原理:连接和无索引邻接
让我们快速浏览一下原生图形数据库及其工作原理。
我们谈到了关系数据库中的离散实体是表中的一行。在原生图形数据库中,该行相当于一个节点。它仍然是一个离散的实体,所以我们仍然有这个标准化的元素。
一个节点将是一个实体。如果我们有个人节点,我们将有一个人一个节点。我们会有一定程度的独特性,比如说社会安全号码。
然而,关键的区别在于,当我们将这个人节点连接到另一个离散实体(例如地址)时,我们会在这两个点之间创建物理连接(也称为关系)。
地址会有一个指针,说明连接到节点的关系的出站部分是什么?然后我们有另一个指向关系的入站部分的指针指向另一个节点。
因此,实际上,我们正在收集一组指针,这是这两个实体之间物理连接的体现。这就是最大的不同。
在关系数据库中,您将在读取时使用连接重新构建数据,这意味着在查询时,它会尝试找出事物如何映射在一起。
在图数据库中,由于我们已经知道这两个元素是相连的,所以我们不需要在查询时查找映射。我们所做的只是跟踪与其他节点的存储关系。
这就是我们所说的无索引邻接。与其他数据库系统相比,这种无索引邻接的概念是理解原生图形数据库性能优化的关键。
无索引邻接意味着在局部图遍历期间,按照连接图中节点的这些指针(关系),操作的性能不依赖于图的整体大小。这取决于连接到您正在遍历的节点的关系数量。
当我们说 JOIN 是一个集合操作(交集)时,我们使用关系数据库中的索引来查看这两个集合的重叠位置。这意味着 JOIN 操作的性能随着表变大而开始变慢。
在大 O 符号术语中,这类似于使用索引的对数增长——类似于 O(log n) 并且随着查询中的 JOIN 数量呈指数增长。
另一方面,图中的遍历关系更多的是基于我们实际遍历的节点中的关系数量的线性增长,而不是图的整体大小。
这是图数据库为我们提供无索引邻接的基本查询时间优化。从性能的角度来看,当我们考虑原生图形数据库时,这确实是最重要的事情。
电影图简介
我们已经谈到了图形和关系数据库之间的理论差异。现在让我们开始看一些并排比较。
电影图由一个数据集组成,该数据集由演员、导演、制片人、作家、评论家和电影组成,以及有关它们如何相互连接的信息。
电影数据集包括:
- 133个人节点/实体
- 38个电影节点/实体
- 上述实体之间的 253 个关系/连接,描述的连接例如:
- 导演电影的人
- 在电影中扮演的角色和扮演的角色
- 写电影的人
- 制作电影的人
- 评论过电影并给出评分和摘要的人
- 跟随另一个人的人
虽然它是一个相对较小的数据集,但它全面地描述了图的力量。
比较数据模型
首先我们来看看我们各自数据库的数据模型。与所有数据模型一样,它们的外观最终取决于您提出的问题类型。所以让我们假设我们要问以下类型的问题:
- 一个人演过哪些电影?
- 一个人与哪些电影有关?
- 一个人曾经合作过的所有合作演员是谁?
基于这些,以下是相关的潜在数据模型:
电影图的实体关系数据模型
电影图的属性图数据模型
你会立刻发现一些东西——那些 ID 不见了!因为一旦我们知道那里有连接,我们就将数据连接在一起,我们不再需要它们,或者那些映射表来让我们知道不同的数据行如何连接在一起。
比较查询
现在让我们继续比较一些查询。从:PLAY movies示例中选取一些最初的查询,让我们看一下 Cypher 查询的一些并排比较,以及等效的 SQL 查询是什么样的。
什么是 Cypher,我听到你问?Cypher 是一种图查询语言,用于查询 Neo4j 图数据库。还有一个OpenCypher版本,许多其他供应商都在使用它。
随着我们进行查询,它应该开始变得更加清晰,图数据库以及一种帮助探索关系的查询语言是如何真正开始发挥作用的。让我们开始寻找汤姆汉克斯吧!
如何找到汤姆汉克斯
MATCH (p:Person {name: "Tom Hanks"})
RETURN p
Cypher
SELECT * FROM person
WHERE person.name = "Tom Hanks"
SQL
如何找到汤姆汉克斯的电影
MATCH (:Person {name: “Tom Hanks”})-->(m:Movie)
RETURN m.title
SELECT movie.title FROM movie
INNER JOIN movie_person ON movie.movie_id = person_movie.movie_id
INNER JOIN person ON person_movie.person_id = person.person_id
WHERE person.name = "Tom Hanks"
如何找到汤姆汉克斯导演的电影
MATCH (:Person {name: "Tom Hanks"})-[:DIRECTED]->(m:Movie)
RETURN m.title
SELECT movie.title FROM movie
INNER JOIN person_movie ON movie.movie_id = person_movie.movie_id
INNER JOIN person ON person_movie.person_id = person.person_id
INNER JOIN involvement ON person_movie.involve_id = involvement.involve_id
WHERE person.name = "Tom Hanks" AND involvement.title = "Director"
如何找到汤姆汉克斯的合作演员
MATCH (:Person {name: "Tom Hanks"})-->(:Movie)<-[:ACTED_IN]-(coActor:Person)
RETURN coActor.name
WITH tom_movies AS (
SELECT movie.movie_id FROM movie
INNER JOIN person_movie ON movie.movie_id = person_movie.movie_id
INNER JOIN person ON person_movie.person_id = person.person_id
WHERE person.name = "Tom Hanks")
SELECT person.name FROM person
INNER JOIN person_movie ON tom_movies = person_movie.movie_id
INNER JOIN person ON person_movie.person_id = person.person_id
INNER JOIN involvement ON person_movie.involve_id = involvement.involve_id
WHERE involvement.title = "Actor"
使用 Cypher 进行更多查询
希望您能了解 Cypher 和 SQL 查询之间的差异。也许您也很高兴了解更多关于它们的信息!我们将在博客文章中进一步提供一些参考。
现在,让我们看看您可以在:PLAY movies图表示例中找到的其他一些 Cypher 查询,并解释发生了什么。
没有典型的培根数问题,任何电影图表都是不完整的,我们的电影图表也不例外!
到目前为止,我们看到的例子每次都遍历一个关系。我们可以轻松地利用这些“写入时连接”来遍历许多关系来回答有趣的问题。
所以,回到凯文培根的数字。以下查询将从 Kevin Bacon 人物节点开始,然后从该起点出发最多 4 跳,以带回所有连接的电影和人物。
MATCH (bacon:Person {name:"Kevin Bacon"})-[*1..4]-(hollywood)
RETURN DISTINCT hollywood
我们可以通过使用*1..4查询模式的关系部分的语法来做到这一点:
- * 表示一切
- 1..4 表示范围 – 1 表示距离 1 跳,4 表示最多 4 跳
我们可以在这个电影数据集上做的另一件事是两个节点之间的最短路径。
在这个例子中,让我们找出Kevin Bacon 和 Meg Ryan 之间的最短路径。您会发现我们*再次将语法用于关系模式——指示一切。
对您来说可能是新的东西是p=. 您已经看到我们如何使用节点的引用(例如bacon或meg在我们当前的查询中),并且我们可以对关系执行相同的操作。
我们还可以对整个路径(即所有涉及的节点和关系)进行引用。我们为此使用的语法是refName =,在本例中是p=。
我们还使用 Cypher 函数shortestPath()——这是一个简单的最短路径函数,它将返回两个指定节点之间的第一个最短路径。请注意,可能还有另一条同样短的路径,但这个简单的函数只会带回遇到的第一个路径。
对于那些对其他路径相关功能感兴趣的人,请查看 APOC 和 GDS 中可用的功能。
MATCH p=shortestPath(
(bacon:Person {name:"Kevin Bacon"})-[*]-(meg:Person {name:"Meg Ryan"}))
RETURN p
给大家一个警告:您可能会看到这一点,[*]并试图在没有shortestPath()函数或1..4范围约束的情况下运行您的图形。但这很可能会导致一些意想不到的事情。
在我们与 Kevin Bacon 和 Meg Ryan 的示例中,即使在这个非常小的数据集中只有 253 个关系,节点和关系之间的所有可能的路径组合也很容易遇到 Bacon 和 Ryan 之间的数百万条不同的路径。
当使用*在你的关系作为查询的一部分,非常谨慎使用!这个问题没有提出最短路径,因为当遇到比当前识别的最短路径更长的潜在路径时,它会立即被丢弃。
一个简单的推荐查询
这里有两个查询真正展示了图形数据库的强大功能,我们可以轻松地使用数据中的连接来提出一些建议。
在我们的第一个查询中,我们正在为汤姆汉克斯寻找新的合作演员,以与他尚未合作的人合作。查询通过以下方式执行此操作:
- 首先,找到他已经合作过的所有合作演员
- 然后,找出所有的co-actors的co-actors(简称co-co-actors)
- 接下来,我们要排除那些已经和汤姆合作过的合作演员,并确保合作演员不是汤姆本人
- 最后,我们返回建议的合作演员姓名,我们将对其进行排序,但与他们合作的合作演员的数量 – 与该合作演员合作的合作演员越多,建议越好。
MATCH (tom:Person {name:"Tom Hanks"})-[:ACTED_IN]->(m)<-[:ACTED_IN]-(coActors),
(coActors)-[:ACTED_IN]->(m2)<-[:ACTED_IN]-(cocoActors)
WHERE NOT (tom)-[:ACTED_IN]->()<-[:ACTED_IN]-(cocoActors)
AND tom <> cocoActors
RETURN cocoActors.name AS Recommended, count(*) AS Strength
ORDER BY Strength DESC
太好了,所以我们找到了一些潜在的合作演员。在下一个查询中,我们想推荐汤姆克鲁斯作为汤姆汉克斯合作的潜在新合作演员。但是,谁来介绍这些汤姆斯呢?回到我们去的电影图表。
在这个查询中,我们:
- 找到汤姆汉克斯的合作演员,然后找出哪些合作演员也和汤姆克鲁斯合作过
- 然后我们将返回合作演员以及他们与汤姆汉克斯和汤姆克鲁斯共同出演的电影
MATCH (tom:Person {name:"Tom Hanks"})-[:ACTED_IN]->(m)<-[:ACTED_IN]-(coActors),
(coActors)-[:ACTED_IN]->(m2)<-[:ACTED_IN]-(cruise:Person {name:"Tom Cruise"})
RETURN tom, m, coActors, m2, cruise