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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
简介: 本文介绍了使用 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数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
相关文章
|
18天前
|
存储 人工智能 Cloud Native
云栖重磅|从数据到智能:Data+AI驱动的云原生数据库
在9月20日2024云栖大会上,阿里云智能集团副总裁,数据库产品事业部负责人,ACM、CCF、IEEE会士(Fellow)李飞飞发表《从数据到智能:Data+AI驱动的云原生数据库》主题演讲。他表示,数据是生成式AI的核心资产,大模型时代的数据管理系统需具备多模处理和实时分析能力。阿里云瑶池将数据+AI全面融合,构建一站式多模数据管理平台,以数据驱动决策与创新,为用户提供像“搭积木”一样易用、好用、高可用的使用体验。
云栖重磅|从数据到智能:Data+AI驱动的云原生数据库
|
4月前
|
SQL 关系型数据库 索引
关系型数据库SQLserver插入数据
【7月更文挑战第28天】
42 4
|
4月前
|
缓存 关系型数据库 分布式数据库
PolarDB产品使用问题之有时会读到表中过去已删除的数据,是什么原因
PolarDB产品使用合集涵盖了从创建与管理、数据管理、性能优化与诊断、安全与合规到生态与集成、运维与支持等全方位的功能和服务,旨在帮助企业轻松构建高可用、高性能且易于管理的数据库环境,满足不同业务场景的需求。用户可以通过阿里云控制台、API、SDK等方式便捷地使用这些功能,实现数据库的高效运维与持续优化。
|
1月前
|
存储 人工智能 Cloud Native
云栖重磅|从数据到智能:Data+AI驱动的云原生数据库
阿里云瑶池在2024云栖大会上重磅发布由Data+AI驱动的多模数据管理平台DMS:OneMeta+OneOps,通过统一、开放、多模的元数据服务实现跨环境、跨引擎、跨实例的统一治理,可支持高达40+种数据源,实现自建、他云数据源的无缝对接,助力业务决策效率提升10倍。
|
2月前
|
存储 人工智能 Cloud Native
云栖重磅|从数据到智能:Data+AI驱动的云原生数据库
阿里云数据库重磅升级!元数据服务OneMeta + OneOps统一管理多模态数据
|
3月前
|
存储 SQL Cloud Native
揭秘!PolarDB-X存储引擎如何玩转“时间魔术”?Lizard多级闪回技术让你秒回数据“黄金时代”!
【8月更文挑战第25天】PolarDB-X是一款由阿里巴巴自主研发的云原生分布式数据库,以其高性能、高可用性和出色的可扩展性著称。其核心竞争力之一是Lizard存储引擎的多级闪回技术,能够提供高效的数据恢复与问题诊断能力。本文通过一个电商公司的案例展示了一级与二级闪回技术如何帮助快速恢复误删的大量订单数据,确保业务连续性不受影响。一级闪回通过维护最近时间段内历史数据版本链,支持任意时间点查询;而二级闪回则通过扩展数据保留时间并采用成本更低的存储方式,进一步增强了数据保护能力。多级闪回技术的应用显著提高了数据库的可靠性和灵活性,为企业数据安全保驾护航。
47 1
|
3月前
|
数据库 Windows
超详细步骤解析:从零开始,手把手教你使用 Visual Studio 打造你的第一个 Windows Forms 应用程序,菜鸟也能轻松上手的编程入门指南来了!
【8月更文挑战第31天】创建你的第一个Windows Forms (WinForms) 应用程序是一个激动人心的过程,尤其适合编程新手。本指南将带你逐步完成一个简单WinForms 应用的开发。首先,在Visual Studio 中创建一个“Windows Forms App (.NET)”项目,命名为“我的第一个WinForms 应用”。接着,在空白窗体中添加一个按钮和一个标签控件,并设置按钮文本为“点击我”。然后,为按钮添加点击事件处理程序`button1_Click`,实现点击按钮后更新标签文本为“你好,你刚刚点击了按钮!”。
258 0
|
4月前
|
关系型数据库 MySQL 分布式数据库
PolarDB产品使用问题之在执行ALTER TABLE语句后,备份数据的物理空间占用增加,是什么原因
PolarDB产品使用合集涵盖了从创建与管理、数据管理、性能优化与诊断、安全与合规到生态与集成、运维与支持等全方位的功能和服务,旨在帮助企业轻松构建高可用、高性能且易于管理的数据库环境,满足不同业务场景的需求。用户可以通过阿里云控制台、API、SDK等方式便捷地使用这些功能,实现数据库的高效运维与持续优化。
|
3月前
|
SQL 关系型数据库 数据库
|
4月前
|
SQL 关系型数据库 数据库
关系型数据库SQLserver查询数据
【7月更文挑战第28天】
47 4

相关产品

  • 云原生数据库 PolarDB