前言:
随着图数据库[1] 技术在工业领域的有效开展和深入应用,在建模时,到底“图数据”与“表数据”有哪些不同?本文,笔者以搭建一个简单的医院信息管理系统为例,具体对比并阐述了用关系型查询语言SQL和图查询语言UQL(Ultipa GQL)在结构设计、查询语句和呈现结果上的不同。同时,希望这些思考也能带给更多的图开发者、使用者、决策者以及爱好者些许启迪,欢迎大家留言交流。
表 VS.图
我们知道,在图数据库的知识领域,开发人员注重深耕图查询语言【更多了解,点击阅读:文库 | 图数据库基础知识—贰】的运用,解决方案人员更关心图算法,管理员则专注于图数据库的维护和管理。今天我们来深入探讨系统架构师所关心的核心议题——图建模,这个过程涉及到如何定义不同类型的节点[2] 和边[3] 。
图建模的过程类比到关系型数据库中就是表的构建过程。许多图数据库的用户都面临一个共同的难题:如何将项目中的数据从传统的关系型数据库顺利迁移到图数据库中。当初学者们面对包含上百个字段的数十张关系表时,并不确定这些表应该被转化为节点还是边,哪些字段应该被保留为属性,哪些则应该被抽离成为节点或边。
考虑到每张关系表和每个点/边schema(模式)本质上都是结构化的,我们可以首先将表定义为schema,然后将表内字段定义为schema的属性。这种抄作业似的图模型设计虽然操作简单,但必将在后续图查询时暴露出不足之处。
事实上,一张关系表需要历经多个改造步骤,才能真正成为图中的一个schema。在这个改造过程中,我们需要特别关注原先表的主键和外键,因为它们将在未来成为节点ID以及边的起点和终点。
让我们站在操作者的视角,分别使用SQL Server和嬴图Graph搭建一个简单的医院信息管理系统。
SQL Server 建表
1、表的结构设计
假设一个医院信息管理系统需要记录最基本的医生接诊、患者住院等日常工作内容,依托SQL Server构建的该系统需要存储以下几个关系表:
(1) 科室表(科室名,科电话,科地址)
(2) 病床表(病房号,床位号,科室名)
(3) 医生表(医生工号,医生姓名,年龄,科室名,职称)
(4) 患者表(病历号,患者姓名,性别)
(5) 诊治记录表(病历号,诊断时间,主治医生,诊断结果)
(6) 住院登记表(病历号,入院时间,病房号,床位号)
其中单下划线的字段(红色)代表主键,单下划线的字段(绿色)代表外键。这里我们进行了一个巧妙的处理,就是让字段“科室名”在科室表中既充当科室的名称,又作为主键使用。
这种设计的好处在于,当病床表和医生表引入外键“科室名”时,无需再增加一个用于记录科室名称的冗余字段。通过这个外键,我们能够直接获取科室的名称,避免了额外的表连接查询,提高了系统效率和性能。
然而在诊治记录表中,情况就没这么便利了。该表中的字段“主治医生”选取的是医生表中的“医生姓名”,而非“医生工号”,因此该字段不能作为外键使用。
这意味着,在查询诊治信息时,我们只能看到主治医生的姓名,如果希望获得更多医生的信息,如所属科室、职称,就需要在诊治记录表中添加外键“医生工号”,并进行表连接查询后才能获知。针对诊治记录表缺少外键的这一情况,我们暂时不进行修正,而是在后续与图结构进行对比时再回过来讨论这个问题。
2、表的创建
按照设计好的结构,依次将这6个表创建到SQL Server的某个库中。为了突出重点,我们省略了建表的SQL语句,直接将创建后的表头展示如下:
(1) 科室表(科室名,科电话,科地址)
(2) 病床表(病房号,床位号,科室名)
(3)医生表(医生工号,医生姓名,年龄,科室名,职称)
(4) 患者表(病历号,患者姓名,性别)
(5) 诊治记录表(病历号,诊断时间,主治医生,诊断结果)
(6) 住院登记表(病历号,入院时间,病房号,床位号,)
3、数据插入
接下来我们将一些测试数据插入到这些表中,为了节省篇幅也不展示SQL代码了,直接来看插入后的结果:
(1)科室表
(2)病床表
(3)医生表
(4)患者表
(5)诊治记录表
(6)住院登记表
不可否认,表数据的呈现方式虽然机械,但其列表形式的确可以详细地展示每个字段的信息。然而对于表间关系,如“哪个科室的医生诊治了哪个病人”或“哪个病人入住了哪个科室的哪个病房”,表数据就没法进行直观表达了。与接下来将要构建的图数据相比,这种差异将格外明显。
嬴图Graph 构图
1、图的结构设计
图模型的设计并不需要一蹴而就,我们先基于之前的表结构初步定义节点和边:
(1) 科室-点(科室名,科电话,科地址)
(2) 病床-点(病房号,床位号,科室名)
(3) 医生-点(医生工号,医生姓名,年龄,科室名,职称)
(4) 患者-点(病历号,患者姓名,性别)
(5) 诊治-边(病历号,诊断时间,主治医生工号,诊断结果)
(6) 入住-边(病历号,入院时间,病房号,床位号)
在这个图模型中,我们将科室、病床、医生、患者定义成了点,下划线(红色)的信息代表点的ID。同时,诊治、入住被定义成了边,下划线(蓝色)的信息代表边的起点、终点。
通过与之前的表格进行对比,我们可以明显看到一些差异。例如:
- 由于表(5)和表(6)被设计成了图中的边,我们不再关注它们原来的ID,也就是表的主键,而是将注意力放在了边的起点和终点上。
- 边的起点和终点通常以点的ID来表示,如果放回到原来的表中,它们是可以充当外键的。
- 对于表(6),其“病历号”和“床位号”分别代表患者和病床的ID,因此表(6)可以直接设计为诊治边。
- 但对于表(5),我们需要将原来表示医生姓名的字段“主治医生”替换为代表医生ID的“主治医生工号”。
- 这个修改同时回应了前文中提到的外键缺失问题。此前,我们将外键缺失问题遗留在了表中,而现在,我们在边上对其进行纠正。
需要注意的是,虽然节点的ID沿用了之前表结构中的字段名,但它们并不是同一个概念。这是因为在Ultipa图系统中,节点的ID必须在整个图中是唯一的,而不仅仅在某一种类型的节点之间唯一,也就是说不同类型的点(科室、病床、医生、患者)的ID均不同。相比之下,关系型数据库中的主键仅在表内唯一,而不需要在整个数据库中唯一。这个区别会对将节点和边数据插入图中时的一些操作细节产生影响。
2、建立schema和属性
将图数据库中的点、边的不同“类型”理解为schema,将每种类型应记录的信息理解为属性,那么图数据库的创建schema和属性的过程就相当于关系型数据库的建表过程。
根据前文提供的图模型,我们本应创建4个点schema和2个边schema。然而为了更贴合实际情况,我们还需要做一些改进。首先,将病房的信息从病床中抽离出来,成为一个独立的schema。这个改进将有助于更有效的统计和管理病房空余床位等信息。改进后的5个点schema分别为:
(1) 科室-点(科室名,科电话,科地址)
(2) 病房-点(病房号,科室名)
(3) 病床-点(床位号,病房号)
(4) 医生-点(医生工号,医生姓名,年龄,科室名,职称)
(5) 患者-点(病历号,患者姓名,性别)
我们特意使用单下划线(绿色)标识了一些属性,这些属性相当于关系型数据库中的外键。将这5个点schema创建到Ultipa 图数据库的某个图集中,它们在Ultipa Manager中展示为:
细心的读者可能会注意到,我们似乎只创建了一部分属性,点的ID还有双下划线标识的“外键”并未出现在属性列表中。
这有两个原因:
其一,红色的点ID作为Ultipa图集中的系统属性是由系统生成的,其属性名为_id,并且不会在Ultipa Manager的属性列表中显示,因此我们无法直接看到它。
其二,绿色的“外键”在图中通常以边的形式存在,我们可以将病房归属于科室、病床归属于病房、医生归属于科室这三类关系创建为某种边schema。
从另一个角度来看,在图数据库的设计中,我们不鼓励引入关系型数据结构中常见的所谓的“冗余信息”,因为借助图数据库强大的关联查询与计算能力,信息之间的关联关系可以轻松通过路径查询获取,而无需冗余数据。
点schema改进后,边schema也要相应调整。这里指的就是为上述三种“属于”关系添加一种schema:
(1) 诊治-边(病历号,诊断时间,主治医生工号,诊断结果)
(2) 入住-边(病历号,入院时间,病房号,床位号)
(3) 属于-边(病房号/床位号/医生工号,科室名/病房号/科室名)
对于边schema我们同样用双下划线标识了一个“外键”。继续在当前所用的图集中创建这3个边schema:
就像我们在创建点schema时遵循的原则一样,创建边schema时也采取了一些"省略"措施:
其一,蓝色的边起点和边终点,这两个系统属性同样由系统自动生成,并且不会在Ultipa Manager的属性列表中显示。
其二,绿色的入住外键“病房号”可以根据“床位属于病房”的关系自动推导,因此无需将其创建为属性。这种能够减少冗余信息的简化,当然也是利用了图数据库强大的关联查询功能。
3、插入数据
使用Ultipa Manager导入图数据时,系统支持上传UTF-8编码的CSV文件。在制作这些文件时,需要特别注意之前提到的点ID与主键的区别。某些时候,需要对表数据的主键进行全图唯一编码,才能生成有效的点ID。
此外,由于Ultipa图系统要求边的起点和终点必须是已经存在于图集中的点,因此必须先导入节点数据,再导入边的数据。
在成功导入所有数据后,Ultipa Manager中的2D全貌将展现出整个图的结构:
在这张图中,我们运用了丰富的色彩来区分不同的实体。淡紫色的节点代表医生,黄色节点代表患者,深紫色节点代表科室,蓝色节点代表病房,绿色节点代表床位。诊治边用蓝色表示,入住边以橙色展示,属于边则以黄色呈现。这些鲜明的颜色使医生、患者、床位等各种实体之间的关系清晰可见,我们只需一瞥就能轻松辨别图中哪些节点之间存在密切联系,甚至可以追溯到某个病床对应的患者的主治医生是谁。图数据库在可视化方面的这种直观性和便捷性让关系型数据库望尘莫及。
SQL和UQL 查询的对比
拥有先进技术支持的图数据库在查询方面自然胜过关系型数据库,对于初学者,即使尚未深入理解底层技术的奥妙,也能从代码编写和查询速度等方面明显感受到这种差距。由于这次实践所准备的测试数据较小,让我们先搁置查询速度的探讨,直接比较关系型数据库的SQL和图数据库的UQL在编写特定查询时的差异。
具体的查询需求是:寻找医生王五接诊的患者的信息、诊断结果,并显示医生姓名。
SQL代码:
SELECT 患者表.PNO, 患者表.PNAME, 患者表.PSEX, 诊治记录表.DIAGONSRESULT, 诊治记录表.DNAMEFROM 诊治记录表 JOIN 患者表 ON 诊治记录表.PNO = 患者表.PNOWHERE DNAME = '王五'
SQL运行结果:
UQL代码:
n({@`医生`.`姓名` == "王五"} AS D).e({@`诊治`} AS A).n({@`患者`} AS P) RETURN table(P._id, P.`姓名`, P.`性别`, A.`结果`, D.`姓名`)
UQL运行结果:
SQL和UQL的运行结果完全一致,但对比这两段代码时,我们发现了一件有趣的事情。SQL采用英文语序,对于以汉语思维为主的人来说,它是一种颠倒的表达方式。
在本次所举的例子中,SQL先使用SELECT声明要返回的数据列,然后用FROM JOIN说明这些列来自于哪些表的连接结果,同时用ON来强调这些表是基于哪些主键进行连接的,最后通过WHERE来指定条件,对连接后表中的数据行进行筛选。
我们常常强调,形成编程思维方式意味着开发者需要放下自己的母语表达习惯,遵循编程语言的语法规则来进行表达。这种思维方式的转变通常是学习编程语言时的最大挑战之一。
那么,什么样的编程语言语法能够最好地平衡不同人之间语言、思维模式的差异呢?
让我们深入研究一下这段UQL代码:首先,它使用了一个n().e().n()路径模板,精简描述了医生与患者之间的诊治关系。之后,它将路径上的各种数据元素组装在一起以供返回。
这段UQL代码将路径中的筛选条件(王五)保留在了路径描述中的,而不是像SQL的WHERE子句那样在事后进行补充说明。这使得UQL更加紧凑和连贯,同时也增强了代码的可读性。由于数据存储结构的不同以及数据建模的差异,UQL将抽象的表连接以及分散的过滤条件巧妙地整合成了一条链式路径模型。
还有一点必须强调,SQL代码中常见的使用ON子句提示的表连接的锚点“主键”在UQL中并未出现,这是因为实体之间的连接关系已经在点和边的定义中内部化了,不需要额外的描述。这种简化和集成的方式使得代码更加清晰,同时也是图数据库针对关联关系的查询效率远高于关系型数据库的根本原因。
UQL的语法不是基于某种特定的自然语言,而是建立在更普遍的思考方式之上——这种思考方式强调事情的处理顺序和基本逻辑。
人类大脑的思考过程常常以一种连锁反应的方式进行联想和推理,每一步都会附带相关的条件。因此,学习UQL的人通常不太会受到语言或文化方面的限制。学习UQL的过程实际上是提高学习者自身高维空间思维和联想能力的过程。
最后,尽管这两段代码返回了相同的结果,但它们实际完成的查询功能并不相同。事实上SQL只实现了患者表和诊治记录表之间的连接,而它返回的“医生姓名”只是诊治记录表中的一个字段。UQL实现的则是患者、诊治、医生之间的关联,相当于在关系型数据库中进行了三张表的连接查询,它所返回的医生姓名是“医生”schema的属性。
如果我们需要增加返回“医生职称”的需求,只需在上述UQL代码的table()函数中添加一个D.`职称`即可实现,而SQL代码则需要再添加一个JOIN ON语句,将医生表也连接进来才能返回医生的职称信息,代码也会更加冗长。
这凸显了UQL在实现相同查询功能时的简洁性与高效性。
我们之所以用表格的形式呈现了UQL语句的查询结果,是为了更好地将其与SQL进行对比。在实际使用UQL进行路径查询时,我们更倾向于以2D/3D的形式来展示结果。当我们将上述UQL代码改写并在Ultipa Manager中运行时,系统会自动以2D模式呈现查询结果,让我们更直观地理解和分析数据。
改写后的UQL代码:
n({@`医生`.`姓名` == "王五"}).e({@`诊治`}).n({@`患者`}) as pRETURN p{*}
结语
在实际应用场景中,图数据库的图模型构建还会涉及各种错综复杂的情况,而本次实践仅是为了激发对这个话题的初步思考——精心雕琢图模型对于确保高效查询至关重要。图模型的构建必须与未来的查询业务密切协同,并在业务需求发生变化时做出灵活的调整。后续,笔者还将推出系列文章,深入探讨和总结这些微妙之处。毕竟,图模型的精准塑造是高效图应用开发的第一步。
通过此文,笔者也期望能够协助初学者更好地理解并选择适合其需求的数据库,同时为更深入的图数据库学习和探索奠定基础。此外,看文章的你,如果想了解更多关于图数据库与关系型数据库的区别,可点击扩展阅读。【文/孙婉怡;图/王紫嫣】
【注释】