目录
导语
01、用进化的视角看数据库和查询语言的演进
02、图语言(GQL)的大道至简之法
03、Ultipa GQL (嬴图)图语言
图数据库该如何操作和查询呢?我们知道关系型数据库用的是SQL(Structured Query Language),它也是数据库领域第一个国际标准,在大数据库和NoSQL类型数据库广泛发展之前的唯一的一个数据库查询语言国际标准。
本文中,我们会介绍图查询语言GQL(Graph Query Language)的基础概念,以及GQL与SQL之间的差异。
如果读者对SQL语言的演进有所了解的话,就知道是它直接推动了关系型数据库的发展。此外,互联网的崛起还催生了NoSQL的诞生和崛起,其中重要的一个因素是关系型数据库无法很好地应对数据处理速度、数据建模灵活性的诉求。
NoSQL数据库一般被分为以下5类,每一类都有其各自的特性:
键值: 性能和简易性
宽列: 体量与性能
文档:数据多样性
图:深数据+快数据
时序:(可选的)时序,IOT数据、时序优先性能
下图中,SQL被认为是最先进的数据处理与查询语言,并代表了一大批人的认知,如果更深入地去探究SQL的演化历史,就能够帮助我们有一个更全局化的概念了。
SQL的出现已将近半个世纪了,并且迭代了很多版本(平均每3至4年就一次大迭代),其中最知名的非SQL的92/99莫属,例如92版本中在FROM语句中增加了子查询功能,在99 年版本中增加了CTE功能等,这些都极大地增加了关系型数据库的灵活性。然而,关系型数据库始终存在一个“弱点”,那就是对于递归型数据结构的支持。
所谓递归数据结构,指的是有向关系图的功能实现。令人感到讽刺的是,关系型数据库的名字虽然包含了关系,但它在设计伊始就很难支持关联关系的查询。为了实现关联查询,关系型数据库不得不依赖表连接操作,而每一次表连接都意味着潜在的表扫描操作,以及随之而来的性能上的指数级下降和SQL语句、代码复杂度的直线上升。相信这也是很多程序员的梦魇吧。
表连接操作的性能损耗是直接源自于关系型数据库的基础设计思想:
数据正则化(Data Normalization)
固定化的、预先设定的表模式(Fixed/Predefined Schema)
如果我们看一下NoSQL中的核心理念,在数据建模中突出了数据去正则化。
所谓数据正则化,指的是用空间换取时间(牺牲空间来换取更高的性能!)在NoSQL(也包括Hadoop等,例如典型的3、5、7份拷贝的理念)中,数据经常被以多份拷贝的方式存储 ,而这样做的好处在于数据可以以近邻计算资源的方式被处理。这种理念和SQL中的只存一份正则化设计思路是截然相反的。后者或许可以节省一些存储空间,但是对于复杂的SQL操作而言,带来的是性能的损耗。
预先定义数据的表模式的理念是SQL与NoSQL的另一大差异。对于初次接触这一概念的读者而言,理解这个点会有些复杂,从下面这个角度去理解或许更直观:
在关系型数据库中,系统管理员(DBA),需要先定义表的结构(schema),然后才会加载第一行数据进入数据库,他不可能动态的更改表的结构。这种僵化性对于固定模式、一成不变的数据结构和业务需求而言或许不是什么大问题。但是,让我们想象一下,如果数据模式可以自我调整,并能根据流入的数据动态调整,这就给了我们极大的灵活性。
对于强SQL背景的人而言,这是很难被想象的。但是,如果我们暂时抛弃掉我们僵化的、限制性的思维,取而代之以一种成长性的思维方式 ——我们所要达成的目标是一种“Schema-Free或Schemaless”的数据模式,也就是无需预先设定数据模式,数据之间的关联性不需要预先定义和了解,随着数据的流入,它们会自然地形成某种关联关系 ——而数据库所需要做的是对应着这些数据“因地制宜”地来处理如何查询与计算。
在过去几十年中,数据库程序员已经被训练的一定要先了解数据模型,不论它是关系型表结构还是实体E-R模式图。了解数据模型当然有它的优点,然而,这也让开发流程变得更加复杂和缓慢。如果读者们还记得上一次你参与的交钥匙解决方案的开发周期有多长?一个季度、半年、一年还是更久?在一个有8000张表的Oracle数据库中,没有任何一个DBA可以完全掌握所有表之间的关联关系。这个时候,我们更愿意把这套脆弱的系统比作一个定时炸弹,而你的所有业务都绑定在其上!关于无模式(Schema-Free),笔者并不想解释文档型数据库或者宽列据库,尽管它们都多少有一些和图数据库相似的设计理念。在下文中会用一些具体的图数据库上的例子来帮助读者理解无模式意味着什么。
在图数据库中,逻辑上只有两类基础的数据类型:
顶点(Nodes 或Vertices)
边(Edges)
一个顶点具有它自己的ID和属性(标签、类别及其它属性)。
边也类似,除了它通常是由两个顶点的顺序决定的(所谓有向图的概念指的是每条边由一个初始顶点对应一个终止顶点,再加上其它属性所构成,例如边的方向、标签、权重等)。
此外,图数据库并不需要任何预先定义的模式或表结构。这种极度简化的理念恰恰和人类如何思考以及存储信息有着很大的相似性 —— 我们通常并不在脑海中设定表结构,我们是随机应变的!
现在,让我们看一看一些真实世界场景中的图数据库实现,例如下图中的一个典型的图数据集中的顶点的属性定义,它包含了最初的几个字段的定义,例如desc, level, name, type,但是也存在一些动态生成、扩增的字段,例如#cc, #pr, #khop_1等等。如果比照着关系型数据库而言,整个表的结构是动态可调整的。
注意上图中的name和type字段的属性为STRING类型,它可以最大化兼容广谱的数据类型,进而提供最大化的灵活性。顶点之间如何产生关联也无需被预先定义,这样所形成的关联网络也是灵活的。
细心的读者一定会问到,这种灵活性怎么来实现和保证性能优化呢?常识的做法是通过存储与计算分层来实现,例如为了实现极佳的计算性能,数据可以动态地加载进入内存计算(LTE vs. UFE = Load-to-Engine vs. Unload-from-Engine),当然内存计算只是一部分,支持并发计算的数据结构也是必不可少的。
键值库可以被看作是前-SQL(Pre-SQL在这里表达的是一种相对于SQL而言更原始的特型)的非关系型架构库,图数据库则可被看作是后SQL(Post-SQL在这里表达了一种相对而言的先进性)时代的,真正意义上支持递归式数据结构的数据库。
今天的不少NoSQL数据库都试图通过兼容SQL来获得认可,但是在笔者看来,SQL的设计理念是极度表限定的,所谓“表限定”(table-confined),指的是它的整个理念都是限定在二维世界中的,当要进行表连接操作时,就好比要去进入到三维或更高维的空间进行操作,而这也是非常低效和反直觉的,这是基于关系型数据模式—SQL本身的低维属性决定的。
图数据库天然是高维的,除非它的实现是基于关系型数据库或列数据库实现的,那么本质上这种非原生图的设计依然是低维驱动的,它的效率又怎么可能会高呢?!
在图上面的操作天然是属于递归式的,例如广度优先搜索或深度优先搜索。当然,仅仅从语言的兼容性而言,图上面一样也可以支持SQL类操作来保持向关系型用户群的习惯兼容,就像Spark SQL或CQL一样,无论它的意义到底有多大或多长久。
下面,让我们来看一些通过用Ultpa GQL(Ultipa Graph Query Language,Ultipa 图数据库查询语言)实现的图查询功能实例。同时,请仔细思考用SQL 或是其它NoSQL 数据库将如何才能完成同样的任务?
任务1:从某个顶点出发,找到它的第1到第K层(跳)的所有邻居并返回。
Ultpa GQL 是与Ultipa Graph 高并发实时图数据库匹配的查询语言,又名嬴图查询语言。除了明显的性能优势外,Ultpa GQL的另外一个重要特点是高易用性,容易掌握,并有贴近自然语言的易读性。可以通过Ultipa Manager、Ultipa CLI或Ultipa SDK/API的接口调用,只需要1行Ultipa GQL即可实现上面的查询。
spread().src(123).depth(6).spread_type(“BFS”).limit(4000);
上面的语句简单易懂,基本上不需要太多解释,调用spread()函数,从顶点123出发,搜索深度为6层,以BFS的方式进行搜索,限定返回最多4000个顶点(以及关联的边)。
在上图中,红色的小点就是起始顶点,通过以上语句操作的全部返回的顶点和边所形成的子图直接显示在Ultipa Graph的WEB界面上了。
事实上,spread()这个操作相当于允许从任何顶点出发找到它的联通子图——或者说它的邻居网络的形态可以被直接计算出来,并通过可视化界面直观地展示出来。用这种方式也可以看出生成的联通子图中的顶点和边所构成的热点、聚集区域等图上的空间特征,而并不需要传统数据库中的 E-R模型图。
任务2:给定的多个顶点,自动组网(形成一张顶点间相互联通的网络)。
本查询相对于习惯使用传统数据库的读者来说或许就显得过于复杂了,用SQL也许无法实现这个组网功能。
但是,对于人的大脑而言,这是个很天然的诉求——当你想在张三、李四、王五和赵六之间组成一张关联关系的网络的时,你已经开始在脑海里绘制下面这张图了(见下图)。
很显然,Ultipa GQL倾向于继续使用1行代码来实现这个“不可能”的操作:
autoNet().addSrcs(12,21,30,40,123).depth(4).limit(5)
autoNet( )就是我们调用的主要函数,它的名字已经非常直白了——自组网操作。你只需要提供一组顶点的ID信息,组网搜索的深度(4层=4跳),以及任意两个顶点间的路径数量限制(5)。下面,我们来从纯数学的角度来分析一下这个组网操作的计算复杂度:
可能返回路径数量:C(5, 2) * 5 = (5 * 4 / 2) * 5 = 50 条;
预估图上计算复杂度:50 * (E/V)4= 50 * 256 = 12800。
注:
假设图中的(边数/顶点数)比例为4(平均值),也就是E/V=4,搜索深度为4的时候每条路径需
要平均计算256(44)次。
这个查询在现实世界应用中的意义非同凡响。
例如执法机关会根据电话公司的通话记录来跟踪多名嫌疑人的通话所组成的深度网络的特征来判断是否有其它嫌疑人关联其中,犯罪集团是否存在某种异动,或者任意个数的嫌疑人构成的犯罪组织间的微妙的联动关系等。
在传统大数据技术框架上,这种多节点的组网操作极为复杂,甚至是没有可能完成的任务。
原因是因为计算复杂度太高 ,对于计算资源的需求太大,在短时间内没有可能完成,或者是以T+7(亦或T+15、T+30)的方式实现,等到结果出来的时候,嫌疑人早已逃之夭夭或者罪案已发生良久了。
假设有1000个嫌疑人需要参与组网,他们之间形成的网络的路径至少有50万条(1,000 * 999 / 2)。如果查询路径深度为 6层,如上所述,这个计算复杂度是20亿次(假设E/V=4,实际上E/V可能>=10,那么计算次数可能达到50万亿次)。基于Spark架构的计算平台可能需要数天来完成运算。
而利用Ultipa Graph,该操作是以实时到近实时(T+0)的方式完成的,我们在不同的数据集上做过性能评测,Ultipa的性能至少是Spark框架的几百倍到数千倍——Spark系统需要1天完成的计算,Ultipa 仅需数秒、数分钟!当与罪犯斗争的时候,每一秒都很宝贵。
对于实时高并发图数据库,性能肯定是“第一等公民”,但是这并没有让我们把语言的简洁、直观、易懂性当做次等公民。绝大多数人会发现Ultipa GQL是如此的简单,掌握了最基本的语法规则后,通常阅读操作手册几分钟到30分钟内,就可以开始写出你自己的Ultipa GQL查询语句了。
Ultipa GQL 借鉴并采用了锁链式查询(chain-query)的语言风格,对于熟悉文档型数据库MongoDB的读者而言,上手Ultipa GQL 就更加简单了。例如,一个简单的链式路径(点到点)查询语句,看下图:
这个例子查询两个顶点间深度为5度的路径,限定返回5条路径,并且返回匹配的属性“name”(通常是顶点或边的名称属性)。
我们再来看一个稍微复杂一点的例子——模板查询,当然,它所完成的功能也更加的强大。例如下面的例子中t()代表调用模板查询,t(a)表达的是设定当前模板别名为a,从顶点12开始,经过一条边抵达到属性age值为20的顶点b(别名),返回这个模板所匹配的结果a和抵达顶点b的名字。
和传统SQL类似的地方是可以对任何过滤条件设置别名,不同之处在于当异构结果a和b.name一同被返回的时候,a表达的是整个模板搜索所对应的路径结果的集合,而b.name则是一组顶点的属性的数组集合,如下面两张图所示,这种异构灵活性是SQL不具备的。
下面,我们再用一个例子来说明在图查询中使用简单的查询语言实现深度的、递归式的查询:
t(a1).n(n1{age:20}).e(e1{rank:{$bt:[20,30]}})[3:7].n(n2).limit(50).return(a1, n1, e1, n2._id, n2.name)
这个语句中,从年龄=20岁的顶点(可能有多个)出发,进行深度为3至7层的路径搜索查询抵达某些顶点,并且路径中每条边的权重介于20至30之间,找到50条路径,并返一系列异构的数据(模板匹配的路径本身、起始顶点、边、终止定点的两个属性)。
这种灵活度在SQL当中,如果不通过书写大量的封装代码是很难实现的,而且这种搜索深度也是令关系型数据库望而却步的—通常会发生因内存或系统资源耗尽而导致数据库出现SEG-FAULT。
任务3:数学统计类型的查询,例如count( ),sum( ), min( ), collect( )等。
例1:这个例子对于SQL编程爱好者而言一点都不陌生——统计一家公司员工的工资总和。
t(p).n(12).le({type:"works_for"}).n(c{type: "human"}).return(sum(c.salary))
在Ultipa GQL中也是一句话即实现:
·从公司顶点12出发
·找到所有工作于(边关系)本公司的员工,别名为c
·返回他们全部工资之和
在一张小表中,这个操作在SQL语境下同样毫无压力,但是在一张大表中(千万或亿万行),或许这个SQL操作就会因为表扫描而变得缓慢了。
而在Ultipa数据库中因为采用相邻哈希+近邻存储的存储逻辑及并发逻辑优化,这种面向一步抵达的邻居顶点的数学统计操作几乎不会受到数据集大小的影响,进而可以让任务执行时间基本恒定。
例2:统计该公司的员工都来自于哪几个省:
t(p).n(12).e({type:"works_for"}).n(c{type:"employee"}).return(collect(c.province))
上面的两个例子是来说明通过Ultipa GQL的方式可以实现传统关系型SQL查询所能实现的功能。同样,返回结果也可以以关系型数据库查询结果所常用的表单、表格的方式来呈现,例如下面两张图所示:
在上图中,khop( )操作返回的是从初始顶点出发经过depth( ) 限定的深度搜索后返回的第K层的邻居的集合,使用select( )可选定需要具体返回的属性。
下图中展示的是类似的操作在Ultipa CLI中返回的结果示例。注意该图中的时间有两个维度,引擎时间和全部时间,其中引擎时间是内存图计算引擎的运算耗费时间,而全部时间还包括一些持久化存储层的数据转换的时间。
任务4:强大的基于模板的全文搜索。
如果一个数据库系统中不能支持全文搜索,那么我们很难能称其为完整的数据库。在图数据库支持全文搜索并不是一个全新的事情,例如老牌的图数据库Neo4j中通过集成Apache Lucene的全文搜索框架,让用户可以通过Cypher语句来对顶点(及其属性)进行全文本搜索。
但是,这种集成开源框架的方式存在一个严重的副作用,就是性能预期与实际(查询)操作中的落差——图查询中关注的往往是多层、深度的路径或K-邻查询,而全文搜索匹配仅仅是这类查询的第一步,试想,当系统集成了外部开源框架后,多套子系统间就存在频繁的交互和网络时延,这种查询的效率可想而知。另一个原因是开源的框架可能存在一些不可预知的一些问题,在生产环境中一旦暴露,修复起来非常困难,这个或许可被看作是开源的一个重大弊端吧。
在Ultipa GQL 中完成面向顶点的全文搜索,只需要下面这句简单的查询语句:
find().nodes(~name:"Sequoia*").limit(100).select(name,intro)
这句返回的是找到100个包含“Sequoia”字样的顶点,并返回它们的name和intro属性。这个查询非常类似于传统数据库中的面向某张表的列信息查询。同样地,也可以针对边来进行查询,例如下面:
find().edges(~name:"Love*").limit(200).select(*)
找到图中所有的边上的name属性中存在“爱情”字样的关系。
当然,如果我们的全文检索只是停留在点、边查询,那么这就略显单薄了。在真实的商用化的图数据库应用场景中,我们更可能用一种基于模板的模糊匹配全文查询。
例如,模糊的搜索从“红杉***”出发到“招银*”的一张关联关系网络,网络中的路径搜索深度不超过5层,返回20条路径所构成的子图。注意:这个搜索从模糊匹配顶点出发,到达模糊匹配的另外一套顶点。
t().n(~name: "红杉*").e()[:5].n(~name: "招银*").limit(20).select(name)
如果不用上面这句简单得不能再简单的Ultipa GQL,你能想象如何用其它SQL或NoSQL语言来实现吗?
假设我们在一个工商数据集之上,在天眼查、企查查做类似的查询,你要先找到名字中包含有“红杉”或“招银”字样的公司,然后再分别对每一家公司的投资关系进行梳理,需要查清楚每家被投公司的合作、竞争、董监高等关系,然后再慢慢梳理出来是否能在5步之内关联上名称中包含“红杉”字样的一家公司和包含“招银”字样的另一家公司。这个操作绝对是让人疯狂的,你可能需要花费数天的时间来完成,或者能够通过写代码调用API的方式来“智能化”的实现。无论如何,你很难在下面两件事情上击败Ultipa GQL:
效率和时延(Efficiency and Latency):即实时性
准确率和直观度(Efficacy and Accuracy):直观、易读、易懂
在上图中,这个看起来简单而又实际上非常复杂的查询操作仅仅耗时50ms!这种复杂查询的效率是前所未有的。如果读者知道有任何其它数据库系统可以在更短的时间内完成同样的操作,欢迎联系笔者深度交流。
一门先进的(数据库)查询语言的优美感,不是通过它到底有多复杂,而是通过它有多简洁来体现的。它应该具备这样的一些通性:
易学、易懂(Easy to Learn,Easy to Understand)
高性能(Lightning Fast)本质上取决于底层的数据库引擎
系统的底层复杂性不应该暴露到语言接口层面(System Complexity Shielded-Off)
特别是最后一点,如果读者对于SQL或Gremlin、Cypher 或GraphSQL中复杂的嵌套逻辑心有余悸,你会更理解下面的这个比喻:当古希腊神话中的泰坦Atlas 把整个世界(地球)抗在他的肩膀上的时候,世界公民们(数据库用户)并不需要去感知这个世界有多沉重(数据库有多复杂)。
任务5:复杂的图算法。
相比于其他数据库而言,图数据库的一个明显优势是集成化的算法功能支持。图上有很多种算法,例如出入度、中心度、排序、传播、连接度、社区识别、图嵌入、图神经元网络等等。随着商用场景的增多,相信会有更多的算法被移植到图上或者被发明创造出来。
以鲁汶社区识别算法为例,这个算法出现的时间仅仅十几年,它得名于它的诞生地——比利时法语区的鲁汶大学(Louvain University)。
它最初被发明的目的是用来通过复杂的多次递归遍历一张由社交关系属性构成的大图中的点、边来找到所有的顶点(例如人、事、物)所构成的关联关系社区,紧密关联的顶点会处于同一社区,不同的顶点可能会处于不同的社区。在互联网、金融科技领域,鲁汶算法受到了相当的重视。下面这行Ultipa GQL 语句完成了鲁汶算法的调用执行:
algo().louvain({phase1_loop:5, min_modularity_increase:0.01})
在图数据库中,调用一个算法与执行一个API调用类似,都需要提供一些必须的参数。上例中,用户仅需提供最少两个参数就可以执行鲁汶。当然,用户也可以设定更为复杂的参数集来优化鲁汶算法,因篇幅所限,本文在此并不做过多展开描述。
鲁汶算法因其天然的逻辑复杂性,计算结果与效果如果能通过可视化的方式来呈现,会起到事半功倍的效果。在下图中,我们展示了一种在鲁汶算法执行过程中自动化生成的数据集,可以支持基于全量数据或抽样数据的实时算法结果3D可视化。
图:实时的鲁汶社区识别算法及Web可视化(Ultipa Manager)
注:
原生的鲁汶社区识别算法的实现是串行的,也就是说它需要从全图中的所有顶点出发,逐个顶点、逐条边的逐条边进行反复运算。例如在Python的NetworkX库中,对一个普通的(几十万至几百万顶点)图数据集进行鲁汶运算要耗时数个、数十个小时,但是在Ultipa Graph上,这个计算的耗时通过高度并发被剧烈的缩短到了毫秒到秒级。在这里,我们探讨的不是10倍至100倍的性能超越,而是成千上万倍的性能提升。果读者觉得笔者给出的案例是天方夜谭或为痴人说梦,或许你应当重新审视一下你对于系统架构、数据结构、算法以及它们的最优工程实现的理解了。
图查询语言还可以支持很多功能强大且智能化很高深的操作,上面的5个例子只起到了一个抛砖引玉的作用,笔者希望它们能揭示GQL的简洁性,并唤起读者去思考一个问题:你到底是愿意去绞尽脑汁地书写成百上千行的SQL代码,并借此杀死大量脑细胞来读懂代码,还是考虑用更简洁、方便却更加强大的图查询语言呢?
关于数据库查询语言,笔者认为:
数据库查询语言不应该只是数据科学家、分析员的专有工具,任何业务人员都可以(并应该)掌握一门查询语言。
查询语言应当便于使用,所有数据库底层的架构、工程实现的复杂性应当对于上层的用户而言是透明的。
图数据库有巨大潜力,在未来的一段时间内会大幅替代SQL的负载,有一些业界顶级的公司,例如微软和亚马逊已经预估未来8-10年间,会有40%-50%的SQL负载会迁移到图数据库上完成。让我们拭目以待。
有些人包括一些知名的投资机构和行业“专家”认为,关系型数据库和SQL永远也不会被取代。笔者认为这种看法禁不起推敲,如果我们稍微回顾一下不是很久远的历史就会发现,关系型数据库从20世纪80年代开始取代了之前的导航型数据库,已经称霸了行业近半个世纪了,但是它们越来越难以满足不断迭代与前进的业务需求。
如果历史真正教会我们一些常识,那就是对于任何事情的执着和痴迷都不会长久,特别是在这个科技蓬勃发展且不断推陈出新的时代。
·END·