暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

《一起学图数据库》之四:理论与实践齐飞,聊聊图数据库的建模

图数据库杂谈 2021-04-16
626

声明:本系列文章是《图数据库(第二版)》的读书笔记,内容是笔者消化之后多转述。需要阅读原书的可以自己购买,或者从网站(www.graphdbs.com)/公众号(图数据库杂谈)下载高清扫描版电子书。


《一起学图数据库》系列好久没更新了,之前我们介绍了图数据库的基础、图数据库的市场格局、图数据库的优势。新的一章节,将要开始数据建模的学习,也要进入实战阶段,我们将以neo4j做为基础环境来学习这部分内容。这一章节内容会比较丰富,第一部分我们先讲述基础的概念,第二部分进入实战演练。

一、理论基础

1、带标签的属性图

图一:简单的社交网络图模型

第一章的时候我们简单介绍过带标签的属性图,这里做下回顾。上图是一个简单的图模型,表示了一个社交网络。但是实际场景中社交关系要比这个复杂的多的多,一个节点只有一个名字属性,一个边只有一个关注标签是不可能满足需求的,所以带标签的属性图(labeled property graph)几乎是图数据库的标配,也是目前最流行的图模型形式。带标签的属性图有几个特点:

  • 它包含节点和联系,同时也具有属性和标签。

  • 节点上有属性,如年龄、性别、学历、情感状态等。我们也可以把节点想象成存储属性的文件,属性可以是任意的键值对。

  • 节点上有一个多个标签,比如动漫、科技、美食等。标签把节点组织在一起,并表示它们在数据集中的角色。

  • 联系有名字和方向,并且总有一个开始节点和结束节点。没有放空都联系,联系连接起节点,从而组成图。

  • 联系也有属性,比如关注、夫妻、跑男队友等。通过在联系上添加属性,可以给图算法提供元数据,也可以给联系增加额外都语义(比如权重、特性等),还可以用于运行时都约束查询。

有了这些特点,我们就可以创建出比较复杂、语义也更加丰富的数据模型了。而当数据模型写入到数据库里,我们就需要一种机制来创建、操作和查询这些数据,这就是查询语言。


2、图数据库查询语言

查询语言一直以来都是用户关注的一大方面,简单易用的查询方式,会大大都增加用户好感度和产品普及度。不同的数据库都有不同都数据库查询方式,比如Neo4j的cypher、ArangoDB的AQL、Graph Engine的LIKQ等。不过很多图数据库都支持基于RDF的查询语言SPARQL,也支持基于路径的查询语言Gremlin。这些查询语言我们后续会做一个单独的篇章展开,因为实战部分的需要,这一章节主要介绍Neo4j的特有查询语言--Cypher。


2.1、Cypher的设计理念

Cypher是一种言简意赅的图数据库查询语言,在设计之初做了很多简化,使得不管是开发人员、DBA,还是业务相关同学都可以轻松的理解Cypher,使用它来查询数据。从直观性讲,我们习惯用示意图来描述图,Cypher的易用性正式基于此来设计的。

图二、一个简单的用示意图表示的图模型

图二的模型中描述了3个朋友的关系,用Cypher中都ASCII字符画表达出来就是:

(emil)<-[:KNOWS]-(jim)-[:KNOWS]->(ian)-[:KNOWS]->(emil)

这个语句描述了一条路径,它将一个叫jim的节点和另外两个叫ian和emil的节点连接起来,同时也将ian和emil两个节点连接起来。这里面ian、jim和emil都是标识符(Identifiers)。如果查询语句需要定位到特定的节点或者联系上,我们必须要明确一些属性值和节点标签,比如:


(emil:Person {name:'Emil'} )

<-[:KNOWS]-(jim:Person {name:'Jim'} )

-[:KNOWS]->(ian:Person {name:'Ian'} )

-[:KNOWS]->(emil)


2.2、Cypher的子句

和大多数查询语言一样,Cypher也是由子句组成的,最简单的查询包括一个MATCH子句和一个RETURN子句。如果按照图二的模型,我们想找出Jim的哪个朋友和Jim有共同好友,这个好友是谁,可以用下面都语句表示:


MATCH (a:Persion {name:'Jim'})-[:KNOWS]->(b)-[:KNOWS]->(c), (a)[:KNOWS]->(c)

RETURN b,c


不看细节语法,我们来猜测这个语句的意思。

1、首先,找到Person节点,并且属性中name为Jim的人,绑定到变量a上

2、然后找到a KNOWS的人,绑定到b上,这里就是Lan和Emil

3、之后再找到b KNOEWS的人,绑定到c,这里只有Lan可以找到,并将Emil绑定到c,而第二步中的Emil因为没有KNOWS所以这条路径失败

4、另外还要求a KNOWS c,验证后a确实KNOWS c,符合查找要求。

5、返回b Lan和c Emil。


MATCH 子句是绝大多数Cypher查询的核心,其实也是在描述需求。除了通过 (a:Persion {name:'Jim'}) 这种方式确认节点外,还可以通过Where子句。在Cypher中,用()表示节点、用-->和<--来表示联系、>和<表示了联系的方向、两个横杠中间把联系的名字放到[]中,冒号开头。

RETURN 子句用来表示在已经匹配的查询数据中,哪些节点、联系和属性是需要返回的。

WHERE 子句用来提供过滤条件,匹配查询结果。

CREATE和CREATE UNIQUE 子句用来创建节点和联系。

此外还有DISTINCT、MERGE、DELETE、SET、FOREACH、UNION、WITH、START、EXPLAIN等子句。


2.3、关系型数据库建模和图数据库建模的区别

为了更好的理解图模型,我们将针对同一个场景,同时基于关系型数据库和图数据库进行建模。因为大部分同学都对关系型的数据库比较了解,相关建模也更加熟悉,因此这部分将会对二者做下对比,看看相似的地方,也关注下差异之处。

图三、一个简单的数据中心架构

如图,我们选取了一个简单的数据中心作为建模场景。在这个场景中,有以下几个角色:

  • 数据库集群:Server*,作为数据存储,为APP提供依赖

  • 应用程序:App*,直接为用户提供服务

  • 虚拟机:VM*,可以理解为虚拟化层,比如App运行在Docker中,Docker运行在服务器上

  • 服务器:Server*,真实的服务器,物理机

  • 机架:Rack*,服务器安装在机架上

  • 负载均衡:Load Balancer*

  • 用户:User*

通常,在做数据中心架构的时候,我们还需要关心2个问题。一是,是否会出现单点故障问题,怎么在出现之前提前做出预警来避免?二是,整个数据流程中,各个环节的资源消耗。然而,当我们准备实施架构部署,进行数据库建模的时候,我们又发现另外一个问题,也是最重要的问题--随着应用组合的变化、数据中心物理布局的发展、或者虚拟机实例的迁移,我们希望底层的数据模型也可以随之更新,而不需要重新设计。

带着这几个问题,我们再开始做数据库的建模。


2.3.1、关系型数据库中的建模

建模的时候,我们通常会先画出E-R图,将脑子里的概念模型转化逻辑模型。E-R图也称实体-联系图(Entity Relationship Diagram),提供了表示实体类型、属性和联系的方法,用来描述现实世界的概念模型,联系有三种类型:一对一、一对多和多对多 。我们按照图三中的数据中心,画出的E-R图如下:

图四、数据中心的E-R图

E-R图中,方框表示实体,前面我们提到的角色都是一种实体;线表示实体直接的关系,可以和自己关联(比如DB),也可以和其他实体关联;关联有一个名字,例如Salve、Uses等;另外,需要标明实体之间的联系类型。在画完E-R图后,我们需要将E-R图映射成表,如下图所示:

图五、数据中心的关系型数据库建模

这一步简单的就像把E-R图用表格的形式誊写一遍,所以常常可以省略E-R的步骤。但是,我们需要特别留心的是,已经有一批出乎预料的复杂度已经存在于模型当中,比如:

  • 支持一对多联系的外键模型,标记为FK

  • 支持多对多联系的连接表,比如AppDatabase


它们的存在仅仅是为了在查询的时候可以找到表与表直接的具体关系,而且存在感很强,不仅扰乱了数据中心原本的模型,而且对于用户而言毫无用处。但是也有一定的好处,模型当中并没有重复的数据。


在项目的开发和设计阶段,我们经常会根据业务需求,来修改数据模型,是因为一旦项目上线,数据模型的修改成本是巨大的,不仅会很耗时,还要面对非常高的风险。所以通常在设计和开发阶段要进行多次的修改,原本非常规范化的模型,不得不去面向业务和用户,来反规范化。反规范化模型的问题是,它阻碍了系统业务需求的快速发展。从数据中心的架构图,然后到E-R图,再到规范化和反规范化的数据库建模,为了适应关系模型,我们对最初的模型做了很多变化,这些变化使得概念模型和数据的真实存储之间产生了无法逾越的鸿沟。而这也使得系统的演化远远落后于业务的发展。


2.3.2、图数据库中的建模

通过上面的示例演练,我们已经知道了关系型数据库是如何建模的,以及它的缺陷。这部分我们来看看图数据库的建模是怎么样的,以及它如何避免关系型数据库中产生的诸多问题。

图数据库建模最有用的地方就是,它和领域模型(类图三中的简单架构模型)是完全同构的。用图数据库的术语来说,就是保证每个节点上都配有恰当的标签和属性,让它可以满足它在整个数据中心中所担任的职责;同时在节点之间创建有命名的、有方向的、有属性的联系。最后得到的图模型如下:

图六、数据中心的图数据库建模

我们来解读下这张图。

  • 节点:所有节点都有一个特定的角色,通过标签来表示;没给节点具有2个属性,一个是Id(int),一个是status(up/down)。

  • 标签:实体右上角的是标签,模型中有2中标签,一种是特定类型标签,比如Database、App、VM等;还有一个通用的标签叫Asset,资产。两个标签,使我们既可以查询特定的角色,也可以查询所有。

  • 联系:联系必须具有source和target节点,同时有一个名字,比如SLAVE_OF、USES等,但是联系没有属性。


数据模型设计完成后,我们还需要做下验证,判断可用性,例如:

  • 一共有3个应用实例,实例1和2运行在数据库1上(2做Salve),实例3运行在数据库3上

  • 机架2上只有服务器3,服务器3上运行这虚拟机31,虚拟机31上跑着App 3

  • 如果遇到故障,可以直接定位查询([*1..5]是一种高级语法,表示长度可变路径):

MATCH (user:User)-[*1..5]-(assert:Asset)

WHERE user.name = 'User 3' AND asset.status = 'down'

RETURN DISTINCT asset


二、实战部分

1、neo4j的安装

登录neo4j的官网,www.neo4j.com在首页可以选择【download】下载安装neo4j来体验,也可以选择【learn more】在线体验。安装很简单,不在赘述。【本文中使用Mac版的Neo4j,可能不同版本直接略有差异。】

2、neo4j的操作

下载完成后,新建Project, 选择数据源(示例选择了local, neo4j 3.3.1),启动后选择Open Browser即可。

图七、neo4j的启动

图八、neo4j的首页

点击【Learn about Neo4j】,可以回顾下图数据库的几个概念:

  • 图数据可以通过Nodes、Relationships、Properties三个概念存储任意类型的数据

  • 最简单的图只包含一个节点,并包含一些称为属性的键值对

  • 节点可以通过标签进行分组和关联,比如社交图中,节点的标签就是Person

  • 相似的节点之间具有不同的属性,属性可以是string、number和boolean

  • 两个节点之间通过联系(Relationship)进行关联,联系具有名字

  • 联系也可以具有属性,比如A knows B since 2001,knows 为联系的名字,since:2001为属性和值

然后点击【Cypher,可以回顾下Cypher的基础语法:

  • 创建节点:CREATE (person:Person { name: "Emil", from: "Sweden" })

  • 查找节点:MATCH (person:Person) WHERE ee.name = "Emil" RETURN person;

  • 创建关系:

MATCH (a:Person),(b:Movie)

WHERE a.name = 'Robert Zemeckis' AND b.title = 'Forrest Gump'

CREATE (a)-[r:DIRECTED]->(b)

RETURN r;


  • 推荐:

MATCH (js:Person)-[:KNOWS]-()-[:KNOWS]-(surfer)

WHERE js.name = "Johan" AND surfer.hobby = "surfing"

RETURN DISTINCT surfer


  • 分析:

EXPLAIN MATCH (js:Person)-[:KNOWS]-()-[:KNOWS]-(surfer)

WHERE js.name = "Johan" AND surfer.hobby = "surfing"

RETURN DISTINCT surfer


3、Northwind Graph

选择【Jump into code】,可以看到有2个示例Graph可以选择,第一个Movie Graph是电影、演员关联的一个示例,主要是熟悉图数据库的基本应用;第二个Northwind Graph主要演示从关系型数据库到图数据库的迁移,借此可以学习下关系表到图的概念转换。


3.1、Product Catalog

图九、northwind的关系模型

Northwind主要销售由供应商(Suppliers)提供的几类(Categories)商品(Products),上图是三个表的关系模型。在Products表中,通过外键CategoryID和SupplierID两个外键来进行关联。



3.2、数据导入

LOAD CSV WITH HEADERS FROM "http://data.neo4j.com/northwind/products.csv" AS row

CREATE (n:Product)

SET n = row,

n.unitPrice = toFloat(row.unitPrice),

n.unitsInStock = toInteger(row.unitsInStock), n.unitsOnOrder = toInteger(row.unitsOnOrder),

n.reorderLevel = toInteger(row.reorderLevel), n.discontinued = (row.discontinued <> "0")

Added 77 labels, created 77 nodes, set 1155 properties, completed after 2637 ms.


LOAD CSV WITH HEADERS FROM "http://data.neo4j.com/northwind/categories.csv" AS row

CREATE (n:Category)

SET n = row

Added 8 labels, created 8 nodes, set 32 properties, completed after 1340 ms.


LOAD CSV WITH HEADERS FROM "http://data.neo4j.com/northwind/suppliers.csv" AS row

CREATE (n:Supplier)

SET n = row

Added 29 labels, created 29 nodes, set 348 properties, completed after 1223 ms.


CREATE INDEX ON :Product(productID)

Added 1 index, completed after 88 ms.


CREATE INDEX ON :Category(categoryID)

Added 1 index, completed after 2 ms.


导入完成后,可以输入:match(n) return n  查询所有节点,但是这时候的节点都是孤立的,没有任何关联。在最上面可以点击节点类型,查看节点具有哪些属性;点击节点后,最下方可以看到节点的属性-值的具体信息。

图十、neo4j查询所有节点

3.3、添加关联

图十一、产品类目关系图

通过节点中存在的外键关系,我们可以轻松的建立起节点之间的连接,然后按照事实情况,给这些关联定义一个名字。


MATCH (p:Product),(c:Category)

WHERE p.categoryID = c.categoryID

CREATE (p)-[:PART_OF]->(c)

Created 77 relationships, completed after 152 ms.


MATCH (p:Product),(s:Supplier)

WHERE p.supplierID = s.supplierID

CREATE (s)-[:SUPPLIES]->(p)

Created 77 relationships, completed after 28 ms.


3.4、查询

数据导入成功后,怎么使用就是一个关键了。我们简单对比2个查询语句,来体验下图数据库的查询方式。

1、列举每个供应商可以提供哪类的商品

  • select s.CompanyName, GROUP_CONCAT(distinct c.CategoryName)  from Products p , Categories c, Suppliers s  where p.CategoryID=c.CategoryID and p.SupplierID=s.SupplierID group by s.CompanyName


  • MATCH (s:Supplier)-->(:Product)-->(c:Category) RETURN s.companyName as Cp, collect(distinct c.categoryName) as Cate


2、查询分类produce的供应商

  • select s.CompanyName from Products p , Categories c, Suppliers s where p.CategoryID=c.CategoryID and p.SupplierID=s.SupplierID and c.categoryName='Produce'


  • MATCH (c:Category {categoryName:"Produce"})<--(:Product)<--(s:Supplier) RETURN DISTINCT s.companyName as ProduceSuppliers


参考:

《图数据 第二版》第三张3.1~3.4

https://www.cnblogs.com/ljhdo/p/5516793.html

https://www.jianshu.com/p/dde4b48c3805


文章转载自图数据库杂谈,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

文章被以下合辑收录

评论