NoSQL学习——图数据库Neo4j(二)图数据模型与查询
# 图数据模型
图是一种常见的数学模型,对于一张图,有点的集合和边的集合,点之间通过边进行连接,边可以是有向或者无向的,二者都可以具有其属性。图可以用于表示几乎所有事物,例如化学中分子可以用图进行表示,数据库中常见的E-R模型也是一种图。
在建立图之前,需要考虑将哪些概念用结点表示,哪些用关系表示,并需要设计两种实体内部的属性以及标签。结点通常表示事物,而关系通常表示结构。标识符是一种重要的建模方法,通过明确每个概念通过什么实现区分能够在一定程度上辅助决策。关系的类型和属性通过是否有共性区分,如是REFERENCED
属于类型,而referenceOrder
可能是关系的属性。
# 常见的图模型
要在数据结构上对图进行建模,通常可以使用邻接表或邻接矩阵。但在更高的层级上,不同的数据库还加入了不同的特性用于支持更多高级的图算法。在MongoDB这类文档型数据库中,我们已经见到了其在聚合阶段使用的$graphLookup
,这与MongoDB在文档中采用类似邻接表的方式组织图有关。在一些其他的图数据库如Apache Jena中,图数据建模采用的是一种基于主谓宾形式的资源描述框架,即主体通过为谓语与宾体形成关系。在此基础上,这些框架采用SPARQL这种面向Spark的查询语言进行查询。
# Neo4j的图模型
在Neo4j中,图建模使用了一种基于属性的方式,每个结点和边都被表示为一个object,并具有自己的标签和属性。通常的表示格式是(n:LABEL {attr1:..., attr2:..., ...})
,其中冒号后是一个标签,大括号内是该结点或边的属性,最外层如果是小括号()
则表示结点,是中括号[]
则表示边。结点通常用于表示实体,例如书店的书本等;边通常表示实体之间的关系,例如书本与作者(被书写)、书本与书本(参考)间的关系。
一个节点或边可以具有多种属性和标签,属性是一个结点自有的特殊属性,其组织方式类似文档,但不提供嵌套,只支持基本的类型及单一类型的列表,支持的类型有:数值、字符串、布尔值、空间点数据和其列表。标签表示一类结点的共性,例如属于一个标签的结点都表示书本等。二者都具有动态属性,可随时修改。此外,还可以用结点的标签表示该结点的某种临时状态,例如 :Suspend、:Active 等。在Neo4j中,边的标签被称为类型,而边始终是有向边,一条边在Neo4j中的路径长度为1,即如果存在一条边时,两结点之间的逻辑距离为1,无论边的属性中是否有距离相关的数值,这为Neo4j的按距离遍历提供了逻辑基础。
在此基础上,Neo4j采用一种形象描述点与边的Cypher语言进行查询,且无需担心双向查询,因为cypher提供了一种反向查询有向边的方法。由于Neo4j采用了一种特殊的图存储方式,它可以不需要使用索引查找连接(此外Neo4j提供了针对属性和标签点的索引),而是使用基于文件结构的算法查询,这将在下一章中提及。
# Neo4j的Cypher查询
Cypher查询为Neo4j独有,受到SQL的启发采用了模式和从句的写法,便于阅读理解,但也存在一些需要注意的规范,例如聚合函数的使用位置等。此处仅给出查询的一些基础情况,更多用法请参考官方文档。
# 建立图
图的建立有两种方式,其一是使用CREATE语句,其二是使用MERGE语句。
CREATE:可用于创建一个点、两个点及其边等。一个例子是
CREATE (n1:BOOK {name: "My life", tags: ['biography', 'novel']})<-[r:WRITTEN_BY {...}]-(n2:AUTHOR {name: "Nill", age: 72})
。需要注意,使用CREATE重复执行时,会创建新的节点,原因是Neo4j每个结点内部有一个自己的<id>
作为标识符,而在使用CREATE时不会检测是否有相同内容的节点。MERGE:MERGE语句有多种用法,在此首先给出建立图的用法。
- 结合数组类型:可以使用
UNWIND
展开数组,使用IN
查找数组内容。例子:
UNWIND [14, 16, 22, 9] AS id MATCH (c:Course) WHERE c.code IN [102, 104] MERGE (n:Student {id: id}) MERGE (n)-[:MAJOR_IN]-(d:DEGREE {name:"Computer Science"}) ON CREATE SET n.year = 2019
- 在上面的例子中,首先使用了
UNWIND
展开,然后使用MATCH
查找,对查找的结果使用MERGE创建新的关系,这里使用了ON CREATE SET
,是指只有在n是被创建时才会设置其year属性,与之相对的是ON MATCH SET
,表示查找到存在的点时设置属性。
- 结合数组类型:可以使用
另外,通过apoc库可以将json数据读取出来并结合merge生成图。一个例子如下:
CALL apoc.load.json("file:///persons.json") YIELD value MERGE (p:Person {name: value.name}) SET p.age = value.age WITH p, value UNWIND value.children AS child MERGE (c:Person {name: child}) MERGE (c)-[:CHILD_OF]->(p);
在这个例子中,我们可以看到,apoc库读取了本地文件,然后将其通过
YIELD
生成为文档式的value,由此生成了一张包含子母关系的图。
# 查询图
Cypher的图查询类似SQL,使用MATCH <pattern> [WHERE <condition>] RETURN <objects>
的方式。以下分别针对各类过程进行说明:
- 查询结点:这条语句与SQL非常类似,尽管其查找是放在了第一和第二行。这里使用了
MATCH (n:BOOK) WHERE n.published > 1980 RETURN n ORDER BY n.published DESC LIMIT 1
ORDER BY
和LIMIT
,类似SQL中的作用。 - 查询链:这里的关系中使用了
MATCH p=(:BOOK {name:"His life"})-[:REFERENCED *3..5]->(src:BOOK) WHERE "biography" IN src.tags AND ... RETURN DISTINCT p, count(nodes(p)) AS length
*[m]..[n]
,这是用于指定遍历的距离,其中下界为m,上界为n,均为闭合区间,此外m、n二者可不指定,则可对应无下界或上界。此外,关系如果没有箭头,则会查找通过REFERENCED指向查询结点的点。此外,该例子中还有一个DISTINCT关键字,这与SQL中的用法一致。 - 复合查询:复合查询使用
WITH
,需要注意的是,在每个关联的查询之前,都需要将最终返回的字段囊括在WITH
中,例子已在上面的apoc一例中给出。 - 使用聚合函数:聚合函数包括count、sum、avg、max、min等,通常用于查找一组结点的特征。
- 使用断言(Predicate)函数:通常用于
WHERE
从句中,包括all、any、exists、none、single,其中除exists外,通用的使用方法是WHERE func(x IN <some objects> WHERE <x condition>)
;exists的用法则是在内部放入与MATCH
类似的查询。在WHERE
中,函数计算的结果表示某查询到的所有结点或边中的符合相应条件的。这些函数也可用于RETURN
从句中,用于返回计算结果,而不会进行过滤。
# 修改图
修改图同样可以使用MERGE
(因为MERGE
表示检查图是否符合查询的条件,如果不符合就将图修改为符合条件的结构),也可以使用MATCH <pattern> SET/REMOVE <properties/labels>
的方法。例如:
- 使用
MATCH
:MATCH (n:BOOK {name:"His life"}) SET n.tags=... REMOVE n:BOOK REMOVE n.subtitle
- 使用
MERGE
:相同的语句对于存在的结点会进行更新。MATCH (n:Student)-[:ENROLLED_IN]->(c:Course) WHERE c.code IN [102, 104] MERGE (n)-[:MAJOR_IN]-(d:DEGREE {name:"Computer Science"}) ON MATCH SET n.year = 2021
# 删除图
删除图时,首先使用MATCH
查找并指定需要删除的变量,然后在DELETE
中删除。删除时,需要注意如果结点之间还存在着边,则要先删除边才能删除结点。下面是删除整个图的方法:
MATCH (n)-[r]-() DELETE n,r; MATCH (n) DELETE n
,这里使用了两个语句并通过分号连接,因为第一个语句只能删除所有度大于零的结点,第二个语句才能删除独立点。
# 总结
这一章中,我们总结了常见的图模型和Neo4j使用的图模型,并对Neo4j的图操作进行了简单的探索。下一章中,我们将简单讨论Neo4j内部的数据存储、索引原理。