概况
图数据库(Graph database,GDB)是一个使用图结构进行语义查询的数据库,它使用节点、边和属性来表示和存储数据。该系统的关键概念是图,它直接将存储中的数据项,与数据节点和节点间表示关系的边的集合相关联。
图形数据库应用场景非常广泛,可以说只要传统关系型数据库能够应用的场景,图数据库都能够胜任。在业界,最著名的应用案例为Meta公司的社交图谱 (Social Graph),几乎Meta所有的产品都是基于社交图谱构建的。著名的社交六度分隔理论,便是基于社交图谱研究而来的。
在图形数据库,节点和边是基本概念
- 点 (Node):也称“节点”,用于描述数据关系中的主体。点可以有属性(attributes)。比如,在社交场景中,人就是点,名字,性别等就为属性。
- 边(Vertex):用于描述数据之间的关系,可以有向也可以无向(也可理解为双向)。一般来说,边应至少包括两个点的标识符。比如,认识关系就是边(单项),夫妻关系也可以是边(双向)。
除了点跟边,一般业界的图数据库还包括:
- 触发器(Trigger):用于触发特定事件。比如某人年龄到达18,可以触发事件“成年”。
- 观察器(Observer):用于监听事件,从而进行实时处理。比如,当“成年”时间触发以后,可以进行一些列处理,比如推送“恭喜成年”消息。
- 验证器(Validator):主要用于保持数据一致性。一致性既可以是点的,也可以是边的。比如人的年龄不能为负数,在多数国家,一个人只能有一条夫妻关系的边,等等。
当然,每个数据库会有一些出入或者特定的机制,这里就不一一列举了。
检索
图数据库通常会提供类似于图遍历(Graph Traversal)的检索机制,并附带语言级别(QL)的检索支持。可惜的是,到目前位置还没有类似于RDBSM那样标准的检索语言(SQL)。有些图数据库会更进一步提供接口API进行数据检索。此外,图数据库非常容易整合ORM(Object-Relational Mapping),进而更好地解耦数据存储和数据模型。
如前文所说,不同数据库检索语言和API会有所差异,但原理基本一致。在此,我们会使用伪代码展示。例如,在社交例子中,我们需要查询用某一个用户的所有认识的人,检索伪代码为
Select(User)
->Where(userID = $USER_ID)
->QueryViaEdge_Know // 访问"认识"边
->Scan() // 返回所有"认识"的用户
如果我们需要进一步访问某一用户在中国认识的其他用户,则
Select(User)
->Where(userID = $USER_ID)
->QueryViaEdge_Know // 访问"认识"边
->Where(Country = "China") // Country 是用户点的一个属性
-> Scan()
再进一步,如果我们想知道某用户在中国认识的用户里所关注的活动。在此,假设活动为点,关注活动为边,则
Select(User)
->Where(userID = $USER_ID)
->QueryViaEdge_Know // 访问"认识"边
->Where(Country = "China")
->QueryViaEdge_FollowEvent // 进一步访问"关注活动"边
->Scan()
有的时候,我们还需要一些更加复杂的检索,比如子检索(Sub-Query)支持。还是回到社交的例子,如果我们想要找到某用户在中国认识并且订阅了该用户的其他用户。此处假设“认识”和“订阅”都是连接不同用户的边。则
Select(User)
->Where(userID = $USER_ID)
->QueryViaEdge_Know // 访问"认识"边
->Where(Country = "China")
->Where(
SubQuery: //所有当前所达点会作为参数传入子检索
TargetNode
-> QueryViaEdge_Subscribe
-> Where(SubscribeToUserID = $USER_ID)
) //在子检索中,会只留下关注了USER_ID的结果并返回主检索
->Scan()
与关系型数据库对比
下面我们用一个实例来展示图数据库相比传统的关系型数据库(RDB)的异同。我们考虑一个论坛,有如下基本功能
- 发表博客
- 点赞博客
- 订阅用户
如果是传统关系型数据库(RDB),则一般需要如下表,为
- 用户
- 博客 (作者为外键,指向用户表中的UserID)
- 点赞连接表(用户 <-> 博客)
- 订阅连接接表(用户 <-> 用户)
如果是图数据库(GDB),则为
- 两种节点(用户,博客)
- 三种关系(赞,拥有,订阅)
我们发现,图数据库更好地描述了数据关系,并且没有复杂的连接表以及外键设置。下面我们考虑改变需求,增加笔记功能:笔记需有一人拥有,但可以多人协作。笔记可以直接发布为博客。如果是RDB,则会更改为如下表格
- 用户
- 博客 (作者为外键,指向用户表中的UserID)
- 笔记 (拥有者为外键,指向用户表中的UserID)
- 协作连接表 (笔记 <-> 用户)
- 点赞连接表(用户 <-> 博客)
- 订阅连接接表(用户 <-> 用户)
我们会看到,对于传统RDB,需要增加更多的表格(对象表,连接表)从而让检索变得相对复杂。比如,我们需要检索某一用户拥有的笔记中的协作者所订阅的用户是否含有该用户,那么至少需要JOIN 笔记表,协作连接表,订阅表。
// Sub queries
WITH (
SELECT note_share.shared_user_id FROM note JOIN note_share ON note.id = note_share.id WHERE UserID = $USER_ID
) AS shared_user_ids,
(SELECT * FROM subscription WHERE subscribe_to_user_id = $USER_ID) AS
subscriber_candidates
// Main query
SELECT COUNT(*) FROM subscriber_candidates WHERE subscriber_user_id IN shared_user_ids
如果是GDB,那么
而检索可以清晰地表达为:
Select(note)
->Where(OwnerUserID = $USER_ID) // 检索该用户拥有的笔记
->QueryViaEdge_SharedWith // 检索笔记分享的用户
->QueryViaEdge_Subscribe // 检索笔记分享用户所订阅的用户
->Where(SubscribeToUserID = $USER_ID)
->Exist()
我们可以清楚地看到,相比于RBD需要JOIN诸多表格,GDB更够更清晰地表达检索语义。
接下来我们继续加码,增加打赏功能:即用户需要连接支付支付账户,并且对喜爱的博客作者打赏,每次打赏的交易信息需要存底保留。如果是RDB,则需要
- 用户
- 账户
- 用户账户连接表 (用户 <-> 账户,一个用户可以连接多个账户,打赏时可以选择从哪个进行支付)
- 交易记录
- 博客 (作者为外键,指向用户表中的UserID)
- 笔记 (拥有者为外键,指向用户表中的UserID)
- 协作连接表 (笔记 <-> 用户)
- 点赞连接表(用户 <-> 博客)
- 订阅连接接表(用户 <-> 用户)
如果我们想要知道某一用户拥有的博客中的点赞者中,除去博客原来笔记协作者外,总共被打赏的金额是多少?我想对于RDB来说,这个SQL已经相当复杂了,绝大多数情况需要进行多次检索,外加大部分内存操作才能完成。
如果我们用GDB,我们需要
- 增加账户点,有一个边连接用户
- 增加一个打赏边,连接用户和博客,该打赏边除了有所连接的连个点ID的属性,还有一个交易记录ID,指向一个交易记录
那么检索依然可以表达为
$transactionIDs =
Select(blog)
->Where(OwnerUserID = $USER_ID) // 检索该用户拥有的博客
->QueryEdge_Reward // 此处我们检索边本身,而不是连接点
->Where(RewardUserID NOT IN
SubQuery:
TargetEdge
->Select(note)
->Where (OwnerUserID = $USER_ID)
->QueryEdge_Share
->ToIDs()
) //在子检索中,根据$USER_ID所拥有的笔记,找到分享对象的UserIDs,并把由这些用户的打赏排除在外
->TransactionIDs();
$amount =
Select(transaction)
->Where(ID in $transactionIDs)
->Sum(value);
总结
图数据库是一种新型的非关系型数据库。相比于传统的以表为基础的关系型数据库,能够更好地描述数据关系,覆盖更广泛的应用场景,提供更强大的查询语义,以及更好地解耦数据存储和数据模型,从而大大地提高了开发效率。