基于PolarDB的图分析:通过表格将数据快速导入到图

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云原生内存数据库 Tair,内存型 2GB
简介: 本文介绍了使用 PolarDB PostgreSQL兼容版的AGE插件时,在大数据量下,快速导入数据的方法。可以快速将图数据库中亿级以上的节点和边快速导入到数据库中,避免了插入边时进行查询带来的性能瓶颈。

背景和简介

PolarDB PostgreSQL兼容版可以使用AGE插件进行图数据的存储,管理,和查询。

在将图数据库中的数据进行导入和导出时,我们通常需要先将节点数据和边数据转换csv等表格格式,再先导入节点表,再通过边表在节点上构建边。

但使用cypher的MATCH...CREATE语句创建边时,需要查询对应的两侧节点,造成导入速度降低。针对千万级以上数据量的边进行导入时,往往性能无法满足需求。

本文介绍了一种新的图数据导入方式。通过直接生成和管理图上节点的唯一id,跳过MATCH步骤,直接写入边的记录。可以将图的导入效率提升两个数量级。这种导入方法包含以下步骤:

  1. 将图中的节点导入为数据库中的普通表。通常,节点表上有一列(或多列的组合)作为其唯一性标识。
  2. 将图中的边导入为数据库中的普通表。边表上先是左侧节点的唯一性标识的列的值,然后是右侧节点的唯一性标识的列的值,最后是边自身的属性。
  3. 在节点表上添加唯一标识列,在边表上,通过join或计算的方式,添加起始点和终止点的唯一标识列。
  4. 通过唯一标识列将节点表导入到图
  5. 通过唯一标识将边表导入到图

作为例子,我们构造一个简单的场景:

  • 有两种类型的点,分别是v_user和v_product。
  • v_user通过两列type和uid构成唯一标识,不同type下uid可能重复
  • v_product使用一列product_id作为唯一标识,但其是一列复杂字符串,无法转换为数值型id,需要人工创建标号
  • 有一种类型的边,名为e_own。
  • 点和边的数据都已经导入到了名为xxx_raw的表中。
  • 需要将点和边都导入到名为toys的图中

image.png

导入前结构

  • 插件创建

数据库中需要预先创建age插件

create extension age;

在每个会话执行时,需要设置search_path并执行SQL来加载扩展:

SET search_path = ag_catalog, "$user", public;
select * from  get_cypher_keywords() limit 0;

如不想在每个会话中设置search_path,可对数据库进行该项操作

ALTER DATABASE xxx
SET search_path = ag_catalog, "$user", public;
  • 创建图,包括两种节点类型和一种边类型(v代表vertex即节点, e代表edge即边)
SELECT create_graph('toys');
SELECT create_vlabel('toys','v_user');
SELECT create_vlabel('toys','v_product');
SELECT create_elabel('toys','e_own');
  • 创建节点的原始表,代表要导入的节点。
CREATE TABLE public.v_user_raw(type text, uid text, name text, age integer);
INSERT INTO v_user_raw VALUES
('A','U1', 'Alice', '33'),
('B','U1', 'Bob', '21');
CREATE TABLE public.v_product_raw(product_id text, price double precision);
INSERT INTO v_product_raw VALUES
('INAKLIDAS', '50'),
('ENKUCLKSD', '80'),
('IIUIHAKLS', '320'),
('SDVDSUHEE', '340');
  • 创建边的原始表,代表要导入的边。
CREATE TABLE public.e_own_raw(user_type text, user_uid text, product_id text, buy_price text);
INSERT INTO e_own_raw VALUES
('A', 'U1', 'INAKLIDAS', '45'),
('B', 'U1', 'ENKUCLKSD', '70'),
('B', 'U1', 'INAKLIDAS', '50'),
('B', 'U1', 'SDVDSUHEE', '330');

创建唯一标识

  • 对v_user节点,我们假设只有10个type(A-J),则可以使用uid+type组合的方式创建唯一标识,这里将此映射关系注册为函数
CREATE OR REPLACE FUNCTION v_user_idgen(type text, uid text) RETURNS bigint AS
$$
SELECT (ASCII(type) - ASCII('A')) + substring(uid, 2)::bigint * 10
$$
language SQL;

之后在涉及的节点表和边表上添加标识列

ALTER TABLE v_user_raw ADD COLUMN _id bigint UNIQUE CHECK(_id < 281474976710656 AND _id>=0);
UPDATE v_user_raw SET _id = v_user_idgen(type, uid);
ALTER TABLE e_own_raw ADD COLUMN _fromid bigint CHECK(_fromid < 281474976710656 AND _fromid>=0);
UPDATE e_own_raw SET _fromid = v_user_idgen(user_type, user_uid);
  • 对v_product节点,因为其字符串无法直接转换为bigint类型的数值,因此使用PostgreSQL的bigserial对其自动添加编号,并且通过join的方式对边表添加对应的ID(需要对组成唯一标识的列建立索引以加速,如有多列,使用CREATE INDEX ON v_product_raw(column1, column2,...);
ALTER TABLE v_product_raw ADD COLUMN _id bigserial UNIQUE CHECK(_id < 281474976710656 AND _id>=0);
CREATE INDEX ON v_product_raw(product_id);
ALTER TABLE e_own_raw ADD COLUMN _toid bigint CHECK(_toid < 281474976710656 AND _toid>=0);
UPDATE e_own_raw SET _toid = v_product_raw._id
FROM v_product_raw 
WHERE v_product_raw.product_id = e_own_raw.product_id;
  • 两种方法的区别是:使用函数的话,后续也可以通过此函数管理属性到节点id的映射,同时速度更快;而使用自增id的话,不要求原先的列可以转换为数值类型,但需要额外的一次join操作,同时后续无法使用简单的函数计算出属性到节点id的映射,可能会无法进行特定的操作。

快速导入

创建辅助函数

先创建下列函数,辅助生成pg内部的唯一id

CREATE OR REPLACE FUNCTION age_name_to_idx_start(graph_name text, kind_name text, label_name text)
RETURNS bigint
AS 'SELECT id::bigint<<48 FROM ag_catalog.ag_label WHERE kind = kind_name and name = label_name and graph = (SELECT graphid FROM ag_catalog.ag_graph WHERE name = graph_name)'
language SQL IMMUTABLE STRICT PARALLEL SAFE;
CREATE OR REPLACE FUNCTION age_name_to_seq(graph_name text, kind_name text, label_name text)
RETURNS text
AS 'SELECT graph_name || ''.'' || seq_name::text FROM ag_catalog.ag_label WHERE kind = kind_name and name = label_name and graph = (SELECT graphid FROM ag_catalog.ag_graph WHERE name = graph_name)'
language SQL IMMUTABLE STRICT PARALLEL SAFE;

导入节点

导入节点前,最好通过创建唯一约束,防止产生重复的id。但也可以省略此步骤。此索引对后续查询也有益处,使用后不必删除。

CREATE UNIQUE INDEX ON toys.v_user(id);
CREATE UNIQUE INDEX ON toys.v_product(id);

导入节点时,我们可以在_raw表中选择需要的列加入节点,而新增的_id等列可以不需要加入节点

INSERT INTO toys."v_user"
SELECT (age_name_to_idx_start('toys', 'v', 'v_user') +  _id)::text::graphid,
row_to_json((select x FROM (select type, uid, name, age) x))::text::agtype FROM v_user_raw;
SELECT setval(age_name_to_seq('toys', 'v', 'v_user'), (SELECT max(_id) + 1 FROM v_user_raw));
INSERT INTO toys."v_product"
SELECT (age_name_to_idx_start('toys', 'v', 'v_product') + _id)::text::graphid,
row_to_json((select x FROM (select product_id, price) x))::text::agtype FROM v_product_raw;
SELECT setval(age_name_to_seq('toys', 'v', 'v_product'), (SELECT max(_id) + 1 FROM v_product_raw));

导入边

通过_fromid, _toid生成边。可以不记录两侧节点的属性,只记录其自身的buy_price属性。

INSERT INTO toys."e_own" 
SELECT
(age_name_to_idx_start('toys', 'e', 'e_own') + nextval(age_name_to_seq('toys', 'e', 'e_own')))::text::graphid,
(age_name_to_idx_start('toys', 'v', 'v_user') + _fromid)::text::graphid,
(age_name_to_idx_start('toys', 'v', 'v_product') + _toid)::text::graphid,
row_to_json((select x FROM (select buy_price) x))::text::agtype
FROM e_own_raw;

测试验证

查询节点

SELECT * FROM cypher('toys', $$
MATCH (v:v_user)
RETURN v
$$) as (v agtype);
                                                             v                                                              
----------------------------------------------------------------------------------------------------------------------------
 {"id": 844424930131978, "label": "v_user", "properties": {"age": "33", "uid": "U1", "name": "Alice", "type": "A"}}::vertex
 {"id": 844424930131979, "label": "v_user", "properties": {"age": "21", "uid": "U1", "name": "Bob", "type": "B"}}::vertex
(2 rows)
SELECT * FROM cypher('toys', $$
MATCH (v:v_product)
RETURN v
$$) as (v agtype);
                                                         v                                                         
-------------------------------------------------------------------------------------------------------------------
 {"id": 1125899906842625, "label": "v_product", "properties": {"price": "50", "product_id": "INAKLIDAS"}}::vertex
 {"id": 1125899906842626, "label": "v_product", "properties": {"price": "80", "product_id": "ENKUCLKSD"}}::vertex
 {"id": 1125899906842627, "label": "v_product", "properties": {"price": "320", "product_id": "IIUIHAKLS"}}::vertex
 {"id": 1125899906842628, "label": "v_product", "properties": {"price": "340", "product_id": "SDVDSUHEE"}}::vertex
(4 rows)

查询边:查找Alice购买各个产品时使用的折扣

SELECT buy, sell, sell::int - buy::int as discount FROM cypher('toys', $$
MATCH (v1:v_user)-[e:e_own]-(v2:v_product)
WHERE v1.name = 'Bob'
RETURN e.buy_price, v2.price
$$) as (buy agtype, sell agtype);
  buy  | sell  | discount 
-------+-------+----------
 "70"  | "80"  |       10
 "50"  | "50"  |        0
 "330" | "340" |       10
(3 rows)

后续导入和更新

  • 在导入完成后,命名为xxx_raw的表和xxx_idgen的函数即可删除,不会影响图中的数据
  • 如果后续依然想要用此种方式插入边,则可以保留这些表和函数。但需要注意在导入节点和导入边过程中应当跳过已经导入过一次的节点和边,以免创建重复的节点和边。
  • 对后续插入一个节点的场景:
INSERT INTO v_user_raw VALUES
('A','U2', 'Alien', '99', v_user_idgen('A', 'U2'));
INSERT INTO toys.v_user
SELECT (age_name_to_idx_start('toys', 'v', 'v_user') +  _id)::text::graphid,
row_to_json((select x FROM (select type, uid, name, age) x))::text::agtype FROM (SELECT (('A','U2', 'Alien', '97', v_user_idgen('A', 'U2'))::v_user_raw).*) raw;
SELECT setval(age_name_to_seq('toys', 'v', 'v_user'), (SELECT max(_id) + 1 FROM v_user_raw));
INSERT INTO v_product_raw(product_id, price) VALUES
('AIEEEEEEE', '999');
INSERT INTO toys."v_product"
SELECT (age_name_to_idx_start('toys', 'v', 'v_product') + _id)::text::graphid,
row_to_json((select x FROM (select product_id, price) x))::text::agtype FROM v_product_raw WHERE product_id = 'AIEEEEEEE';
SELECT setval(age_name_to_seq('toys', 'v', 'v_product'), (SELECT max(_id) + 1 FROM v_product_raw));
  • 对后续插入一条边的场景,先删除原有的边表的内容,再重新执行导入边步骤:
DELETE FROM e_own_raw;
INSERT INTO e_own_raw VALUES
('A', 'U2', 'AIEEEEEEE', '9999');
UPDATE e_own_raw SET _fromid = v_user_idgen(user_type, user_uid);
UPDATE e_own_raw SET _toid = v_product_raw._id
FROM v_product_raw 
WHERE v_product_raw.product_id = e_own_raw.product_id;
INSERT INTO toys."e_own" 
SELECT
(age_name_to_idx_start('toys', 'e', 'e_own') + nextval(age_name_to_seq('toys', 'e', 'e_own')))::text::graphid,
(age_name_to_idx_start('toys', 'v', 'v_user') + _fromid)::text::graphid,
(age_name_to_idx_start('toys', 'v', 'v_product') + _toid)::text::graphid,
row_to_json((select x FROM (select buy_price) x))::text::agtype
FROM e_own_raw;
  • 在使用了CREATE的cypher语法创建过节点后,则不能再使用本文介绍的快速导入方式,因为使用CREATE可能会破坏通过xxx_idgen函数或JOIN所生成唯一标识之间的对应关系。

总结

本文介绍了通过表格的方式,快速将图数据库外的节点和边,导入到数据库内的图中,以便查询的方式。相比原有的MATCH ... CREATE ...方式,性能通常有两倍以上的提升。

试用体验

欢迎访问PolarDB免费试用页面,选择试用“云原生数据库PolarDB PostgreSQL版”,体验PolarDB的图计算能力

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
相关文章
|
2月前
|
关系型数据库 MySQL 数据库
navicat 查看,设计并导出数据库 ER图
navicat 查看,设计并导出数据库 ER图
200 5
|
2月前
|
运维 关系型数据库 分布式数据库
PolarDB产品使用问题之如何导出视图中的数据
PolarDB产品使用合集涵盖了从创建与管理、数据管理、性能优化与诊断、安全与合规到生态与集成、运维与支持等全方位的功能和服务,旨在帮助企业轻松构建高可用、高性能且易于管理的数据库环境,满足不同业务场景的需求。用户可以通过阿里云控制台、API、SDK等方式便捷地使用这些功能,实现数据库的高效运维与持续优化。
|
3月前
|
存储 DataWorks 关系型数据库
DataWorks产品使用合集之在使用数据集成中的同步任务从mysql同步表到oss,存储为csv时,最终生成的文件中没有表头,这个属性可以在哪里配置么
DataWorks作为一站式的数据开发与治理平台,提供了从数据采集、清洗、开发、调度、服务化、质量监控到安全管理的全套解决方案,帮助企业构建高效、规范、安全的大数据处理体系。以下是对DataWorks产品使用合集的概述,涵盖数据处理的各个环节。
|
3月前
|
SQL 运维 数据管理
数据管理DMS产品使用合集之导出工单列表时是否展示SQL内容
阿里云数据管理DMS提供了全面的数据管理、数据库运维、数据安全、数据迁移与同步等功能,助力企业高效、安全地进行数据库管理和运维工作。以下是DMS产品使用合集的详细介绍。
|
4月前
|
Cloud Native 关系型数据库 MySQL
云数据仓库ADB问一下,数据批量导入失败的有地方导出吗?
云数据仓库ADB问一下,数据批量导入失败的有地方导出吗?
53 0
|
存储 小程序 JavaScript
小程序读取excel表格数据,并存储到云数据库
小程序读取excel表格数据,并存储到云数据库
402 0
|
存储 分布式计算 MaxCompute
基于MaxCompute的图计算实践分享-图加载过程
一、前言 MaxCompute Graph 是基于飞天平台实现的面向迭代的图处理框架,为用户提供了类似于 Pregel 的编程接口。MaxCompute Graph(以下简称 Graph )作业包含图加载和计算两个阶段: 加载,将存储在表中的数据载入到内存中,以点和边的形式存在;
5997 1
|
数据库
LeetCode(数据库)- 制作会话柱状图
LeetCode(数据库)- 制作会话柱状图
89 0
|
SQL Cloud Native 算法
PolarDB-X 1.0-用户指南-数据表管理-调整拆分键
您可以在PolarDB-X控制台上管理数据表,调整数据表的拆分键。
194 0
PolarDB-X 1.0-用户指南-数据表管理-调整拆分键

相关产品

  • 云原生数据库 PolarDB