热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

让我们用SQL开发一个图形数据库吧!

(SELECTnode_idFROMnodeWHEREjson_value(properties;SELECTpropertiesFROMnodeWHEREjson_value(p


文章目录



    • 设计表结构
    • 实现图查询

      • 插入数据
      • 图的遍历
      • 最短距离
    • 索引优化

大家好!我是只谈技术不剪发的 Tony 老师。

图形数据库(Graph Database)是 NoSQL 数据库的一种,使用图结构来存储、表示、处理和查询数据。图是节点(Node)或者顶点(Vertice)和连接(Link)或者边缘(Edge)的集合。节点代表了一个实体(人、物体等),连接代表了两个节点之间的联系(好友、爱好等)。图形数据库非常适合社交关系分析、金融欺诈检测、知识图谱等领域,Neo4j 是著名的图形数据库。

除了使用专门的图形数据库之外,如今主流的关系型数据库都提供了 JSON 文档存储功能以及通用表表达式(WITH 子句)的支持。我们完全可以基于这些功能实现一个简单的图形数据库,同时可以使用 SQL 语句对图形进行操作和查询。

如果你觉得这篇文章有用,欢迎关注❤️、评论📝、点赞👍!

设计表结构

图要由两个结构组成:节点边缘

节点代表了一个实体对象,例如人、地点、事物等。节点可以拥有属性,例如人的姓名、性别等。一个节点对应了关系型数据库中的一行数据或者文档数据库中的一个文档。

边缘代表了两个对象之间的关系,例如某人住在某地。边缘也可以拥有属性,例如何时开始住在某地。一个边缘对应了关系型数据库中的一个外键记录或者文档数据库中的一个链接 id。

基于图的结构,我们可以在关系型数据库中创建两个表:node 和 edge。它们的 ERD 如下图所示:

创建以上表结构的 SQL 脚本如下(MySQL 语法):

-- MySQL
CREATE TABLE IF NOT EXISTS node (
node_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
properties JSON
);
CREATE TABLE IF NOT EXISTS edge (
edge_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
source_id BIGINT NOT NULL,
target_id BIGINT NOT NULL,
properties JSON,
FOREIGN KEY(source_id) REFERENCES node(node_id),
FOREIGN KEY(target_id) REFERENCES node(node_id)
);
CREATE INDEX idx_edge_source_id ON edge(source_id);
CREATE INDEX idx_edge_target_id ON edge(target_id);

对于节点表 node,我们创建了一个自增主键 node_id,以及一个存储节点属性的 JSON 字段 properties。对于边缘表 edge,我们创建了一个自增主键 edge_id,表示关系起点的字段 source_id 和表示关系终点的字段 target_id,以及一个存储关系属性的 JSON 字段 properties。

同时,为了数据操作和提高查询性能,我们创建了两个索引 idx_edge_source_id 和 idx_edge_target_id。

📝完整的表结构和查询脚本可以从 GitHub 或者 CODE CHINA 下载。除了 MySQL 之外,我们还提供了 Oracle、Microsoft SQL Server、PostgreSQL 以及 SQLite 版本。


实现图查询


插入数据

接下来我们使用 SQL 语句插入一些测试数据,首先插入几个节点:

INSERT INTO node(properties) VALUES('{"Label":"Person", "Name":"张三", "Age": 22}');
INSERT INTO node(properties) VALUES('{"Label":"Person", "Name":"李四", "Age": 30}');
INSERT INTO node(properties) VALUES('{"Label":"Person", "Name":"王五", "Age": 28}');

以上语句插入了 3 个节点,它们的 Label 都是 Person,拥有姓名和年龄属性。

然后我们再建立一些关系:

INSERT INTO edge(source_id, target_id, properties)
VALUES((SELECT node_id FROM node WHERE json_value(properties, '$.Name')='张三'),
(SELECT node_id FROM node WHERE json_value(properties, '$.Name')='李四'), '{"Label":"关注", "Degree": 80}');
INSERT INTO edge(source_id, target_id, properties)
VALUES((SELECT node_id FROM node WHERE json_value(properties, '$.Name')='李四'),
(SELECT node_id FROM node WHERE json_value(properties, '$.Name')='王五'), '{"Label":"关注", "Degree": 90}');
INSERT INTO edge(source_id, target_id, properties)
VALUES((SELECT node_id FROM node WHERE json_value(properties, '$.Name')='张三'),
(SELECT node_id FROM node WHERE json_value(properties, '$.Name')='王五'), '{"Label":"关注", "Degree": 60}');

其中,json_value 是 MySQL 提供的 JSON 函数,用于提取 JSON 文档中的元素。

以上测试数据建立的关系图如下所示:

对于节点和边缘的查询和普通表类似,例如:

SELECT properties FROM node WHERE json_value(properties, '$.Name')='张三';
properties |
--------------------------------------------+
{
"Age": 22, "Name": "张三", "Label": "Person"}|
SELECT * FROM edge WHERE source_id = 1 AND target_id = 2;
edge_id|source_id|target_id|properties |
-------+---------+---------+-----------------------------+
1| 1| 2|{
"Label": "关注", "Degree": 80}|

图的遍历

我们从节点“张三”出发,遍历所有的节点:

WITH RECURSIVE traverse(id, relation, hops) AS (
SELECT node_id, cast(json_value(properties, '$.Name') AS CHAR(100)), 0
FROM node
WHERE json_value(properties, '$.Name')='张三'
UNION ALL
SELECT target_id, CONCAT(relation,'->',json_value(n.properties, '$.Name')), hops + 1
FROM edge e
JOIN traverse t ON e.source_id = t.id
JOIN node n ON e.target_id = n.node_id
)
SELECT id, relation, hops
FROM traverse;
id|relation |hops|
--+---------------+----+
1|张三|0|
2|张三->李四|1|
3|张三->王五|1|
3|张三->李四->王五|2|

首先,我们定义了一个递归形式的通用表表达式 traverse,它由两部分组成,使用 UNION ALL 进行组合。第一个 SELECT 语句返回了起点“张三”,第二个 SELECT 语句引用了 traverse 自身,通过不停的迭代返回所有连接的节点。字段 hops 代表了节点之间的跳跃次数。最后的 SELECT 语句返回了全部的节点信息。

📝关于通用表表达式的语法和使用案例,可以参考这篇文章和这篇文章。

从遍历的结果可以看出,MySQL 默认采用的是广度优先搜索方法。如果想要实现深度优先搜索,可以使用 ORDER BY 排序:

WITH RECURSIVE traverse(id, relation, hops) AS (
SELECT node_id, cast(json_value(properties, '$.Name') AS CHAR(100)), 0
FROM node
WHERE json_value(properties, '$.Name')='张三'
UNION ALL
SELECT target_id, CONCAT(relation,'->',json_value(n.properties, '$.Name')), hops + 1
FROM edge e
JOIN traverse t ON e.source_id = t.id
JOIN node n ON e.target_id = n.node_id
)
SELECT id, relation, hops
FROM traverse
ORDER BY relation;
id|relation |hops|
--+---------------+----+
1|张三|0|
2|张三->李四|1|
3|张三->李四->王五|2|
3|张三->王五|1|

最短距离

基于以上图的遍历,我们可以很容易地找出两个节点之间的最短距离。例如,以下语句可以找出“张三”和“王五”之间的最短距离:

WITH RECURSIVE traverse(id, relation, hops) AS (
SELECT node_id, cast(json_value(properties, '$.Name') AS CHAR(100)), 0
FROM node
WHERE json_value(properties, '$.Name')='张三'
UNION ALL
SELECT target_id, CONCAT(relation,'->',json_value(n.properties, '$.Name')), hops + 1
FROM edge e
JOIN traverse t ON e.source_id = t.id
JOIN node n ON e.target_id = n.node_id
)
SELECT id, relation, hops
FROM traverse
JOIN node ON id = node_id
WHERE json_value(properties, '$.Name')='王五'
ORDER BY hops
LIMIT 1;
id|relation|hops|
--+-----------+----+
3|张三->王五 |1|

索引优化

我们使用 EXPLAIN 命令查看一下最短距离搜索语句的执行计划:

EXPLAIN
WITH RECURSIVE traverse(id, relation, hops) AS (
SELECT node_id, cast(json_value(properties, '$.Name') AS CHAR(100)), 0
FROM node
WHERE json_value(properties, '$.Name')='张三'
UNION ALL
SELECT target_id, CONCAT(relation,'->',json_value(n.properties, '$.Name')), hops + 1
FROM edge e
JOIN traverse t ON e.source_id = t.id
JOIN node n ON e.target_id = n.node_id
)
SELECT id, relation, hops
FROM traverse
JOIN node ON id = node_id
WHERE json_value(properties, '$.Name')='王五'
ORDER BY hops
LIMIT 1;
id|select_type|table |partitions|type |possible_keys|key|key_len|ref |rows|filtered|Extra |
--+-----------+----------+----------+------+-------------------------------------+------------------+-------+----------------+----+--------+------------------------------------------+
1|PRIMARY || |ALL| || | |6|100.0|Using temporary; Using filesort |
1|PRIMARY |node| |ALL|PRIMARY|| | |3| 50.0|Using where; Using join buffer (hash join)|
2|DERIVED |node| |ALL| || | |3|100.0|Using where |
3|UNION|t| |ALL| || | |3|100.0|Recursive; Using where |
3|UNION|e| |ref|idx_edge_source_id,idx_edge_target_id|idx_edge_source_id|8|t.id|1|100.0||
3|UNION|n| |eq_ref|PRIMARY|PRIMARY |8|hrdb.e.target_id|1|100.0||

由于 node 表的 properties 字段中的 Name 元素缺少合适的索引,查询使用了大量的全表扫描,如果图中的节点数量很多时性能比较差。对此,我们可以使用数据库的函数索引为 JSON 文档中的节点数据创建索引,从而提高访问性能。例如:

CREATE INDEX idx_node_name ON node ( (json_value(properties, '$.Name')) );

执行以上命令创建索引之后,我们再次查看执行计划:

id|select_type|table |partitions|type |possible_keys|key |key_len|ref |rows|filtered|Extra |
--+-----------+----------+----------+------+-------------------------------------+-------------+-------+-----------------+----+--------+------------------------------------------+
1|PRIMARY |node| |ref|PRIMARY,idx_node_name |idx_node_name|2051|const|1|100.0|Using temporary; Using filesort |
1|PRIMARY || |ref| | |9|hrdb.node.node_id|2|100.0||
2|DERIVED |node| |ref|idx_node_name|idx_node_name|2051|const|1|100.0||
3|UNION|t| |ALL| | | | |2|100.0|Recursive|
3|UNION|e| |ALL|idx_edge_source_id,idx_edge_target_id| | | |2| 50.0|Using where; Using join buffer (hash join)|
3|UNION|n| |eq_ref|PRIMARY|PRIMARY|8|hrdb.e.target_id |1|100.0||

推荐阅读
  • 数据库基本介绍
    1、数据库基本知识概念:数据库:database(DB),是一种存储数据的仓库数据库是根据数据结构组织、存储和 ... [详细]
  • 在Ubuntu中安装MongoDB
    nsitionalENhttp:www.w3.orgTRxhtml1DTDxhtml1-transitional.dtd ... [详细]
  • 本文介绍了Redis的基础数据结构string的应用场景,并以面试的形式进行问答讲解,帮助读者更好地理解和应用Redis。同时,描述了一位面试者的心理状态和面试官的行为。 ... [详细]
  • Oracle Database 10g许可授予信息及高级功能详解
    本文介绍了Oracle Database 10g许可授予信息及其中的高级功能,包括数据库优化数据包、SQL访问指导、SQL优化指导、SQL优化集和重组对象。同时提供了详细说明,指导用户在Oracle Database 10g中如何使用这些功能。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • Python SQLAlchemy库的使用方法详解
    本文详细介绍了Python中使用SQLAlchemy库的方法。首先对SQLAlchemy进行了简介,包括其定义、适用的数据库类型等。然后讨论了SQLAlchemy提供的两种主要使用模式,即SQL表达式语言和ORM。针对不同的需求,给出了选择哪种模式的建议。最后,介绍了连接数据库的方法,包括创建SQLAlchemy引擎和执行SQL语句的接口。 ... [详细]
  • 本文介绍了一个React Native新手在尝试将数据发布到服务器时遇到的问题,以及他的React Native代码和服务器端代码。他使用fetch方法将数据发送到服务器,但无法在服务器端读取/获取发布的数据。 ... [详细]
  • nsitionalENhttp:www.w3.orgTRxhtml1DTDxhtml1-transitional.dtd ... [详细]
  • 《Python3 网络爬虫开发实战》:高效实用的 MongoDB 文档存储
    NoSQL,全称NotOnlySQL,意为不仅仅是SQL,泛指非关系型数据库。NoSQL是基于键值对的,而且不需要经过SQL ... [详细]
  • 架构师必读:日均500万数据,如何进行数据存储选型?
    点击上方关注我,选择“置顶或者星标”作者:麦田里的老农来源:https:zhuanlan.zhihu.comp37964096小编公司有一 ... [详细]
  • 本文介绍了在rhel5.5操作系统下搭建网关+LAMP+postfix+dhcp的步骤和配置方法。通过配置dhcp自动分配ip、实现外网访问公司网站、内网收发邮件、内网上网以及SNAT转换等功能。详细介绍了安装dhcp和配置相关文件的步骤,并提供了相关的命令和配置示例。 ... [详细]
  • Windows7 64位系统安装PLSQL Developer的步骤和注意事项
    本文介绍了在Windows7 64位系统上安装PLSQL Developer的步骤和注意事项。首先下载并安装PLSQL Developer,注意不要安装在默认目录下。然后下载Windows 32位的oracle instant client,并解压到指定路径。最后,按照自己的喜好对解压后的文件进行命名和压缩。 ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • MySQL中的MVVC多版本并发控制机制的应用及实现
    本文介绍了MySQL中MVCC的应用及实现机制。MVCC是一种提高并发性能的技术,通过对事务内读取的内存进行处理,避免写操作堵塞读操作的并发问题。与其他数据库系统的MVCC实现机制不尽相同,MySQL的MVCC是在undolog中实现的。通过undolog可以找回数据的历史版本,提供给用户读取或在回滚时覆盖数据页上的数据。MySQL的大多数事务型存储引擎都实现了MVCC,但各自的实现机制有所不同。 ... [详细]
author-avatar
love_xiao奇
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有