教程:将数据从关系型数据库导入 Neo4j
简介
本教程展示了从关系型数据库(PostgreSQL)导出数据并导入到图数据库(Neo4j)的过程。您将学习如何通过转换模式并使用导入工具将数据从关系型系统迁移到图。
或者,您可以
-
创建 AuraDB 云实例。
-
启动一个空白的 Neo4j 沙箱。
-
下载并安装 Neo4j 桌面。
本指南使用了一个特定的数据集,但其原则可以应用和重新用于任何数据域。
您应该对属性图模型有基本了解,并知道如何将数据建模为图。
关于数据域
在本指南中,我们将使用 Northwind 数据集,这是一个常用的 SQL 数据集。这些数据描述了一个产品销售系统 - 存储和跟踪客户、产品、客户订单、仓库库存、运输、供应商,甚至员工及其销售区域。虽然 NorthWind 数据集经常用于演示 SQL 和关系型数据库,但这些数据也可以构造为图。
下面显示了 Northwind 数据集的实体关系图 (ERD)。

首先,这是一个相当大且详细的模型。为了方便我们的示例,我们可以将其缩减一些,并选择对我们的图最重要的实体 - 换句话说,那些最能从查看连接中受益的实体。对于我们的用例,我们真正想要优化与订单相关的关系 - 涉及哪些产品(以及这些产品的类别和供应商)、哪些员工参与了订单以及这些员工的经理。
使用这些业务需求,我们可以将我们的模型缩减到这些基本实体。

开发图模型
将数据从关系型数据库导入到图中的第一步是将关系型数据模型转换为图数据模型。确定如何将表和行构造为节点和关系可能因您的业务需求而异。
有关根据不同场景调整图模型的更多信息,请查看我们的 建模设计 指南。 |
从关系模型派生图模型时,您应该牢记一些一般指南。
-
行是节点。
-
表名是标签名。
-
联接或外键是关系。
牢记这些原则,我们可以通过以下步骤将我们的关系模型映射到图:
行到节点,表名到标签
-
我们
Orders
表中的每一行都将成为我们图中的一个节点,标签为Order
。 -
我们
Products
表中的每一行都将成为一个节点,标签为Product
。 -
我们
Suppliers
表中的每一行都将成为一个节点,标签为Supplier
。 -
我们
Categories
表中的每一行都将成为一个节点,标签为Category
。 -
我们
Employees
表中的每一行都将成为一个节点,标签为Employee
。
联接到关系
-
Suppliers
和Products
之间的联接将成为一个名为SUPPLIES
的关系(其中供应商供应产品)。 -
Products
和Categories
之间的联接将成为一个名为PART_OF
的关系(其中产品属于某个类别)。 -
Employees
和Orders
之间的联接将成为一个名为SOLD
的关系(其中员工销售了订单)。 -
Employees
与自身之间的联接(一元关系)将成为一个名为REPORTS_TO
的关系(其中员工有经理)。 -
与联接表(
Order Details
)之间的联接(Orders
和Products
之间)将成为一个名为CONTAINS
的关系,具有unitPrice
、quantity
和discount
属性(其中订单包含一个产品)。
如果我们在白板上绘制我们的转换,我们将得到以下图数据模型。
现在,我们当然可以决定要包含关系模型中的所有其他实体,但现在,我们将保留此较小的图模型。
将关系表导出到 CSV
值得庆幸的是,此步骤已为您完成,使用的是您将在本指南的后面使用的 Northwind 数据。
但是,如果您正在处理另一个数据域,则需要从关系表中获取数据并将其放入另一种格式,以供加载到图中。许多系统可以处理的常见格式是逗号分隔值 (CSV) 的平面文件。
以下是一个示例脚本,我们已经运行过它来将 northwind 数据导出到 CSV 文件中供您使用。
export_csv.sql
COPY (SELECT * FROM customers) TO '/tmp/customers.csv' WITH CSV header; COPY (SELECT * FROM suppliers) TO '/tmp/suppliers.csv' WITH CSV header; COPY (SELECT * FROM products) TO '/tmp/products.csv' WITH CSV header; COPY (SELECT * FROM employees) TO '/tmp/employees.csv' WITH CSV header; COPY (SELECT * FROM categories) TO '/tmp/categories.csv' WITH CSV header; COPY (SELECT * FROM orders LEFT OUTER JOIN order_details ON order_details.OrderID = orders.OrderID) TO '/tmp/orders.csv' WITH CSV header;
如果您想使用自己的 northwind RDBMS 自己创建 CSV 文件,您可以在您的 RDBMS 上使用命令 psql -d northwind < export_csv.sql
运行此脚本。
注意:除非您想在自己的 northwind RDBMS 上执行此脚本,否则您无需运行它。
使用 Cypher 导入数据
您可以使用 Cypher® 的 LOAD CSV
命令将 CSV 文件的内容转换为图结构。
当您使用 LOAD CSV
在数据库中创建节点和关系时,您有两个选项来选择 CSV 文件所在的位置:
-
在您可以管理的 Neo4j 实例的 import 文件夹中。
-
从公开可用的位置,例如 S3 存储桶或 github 位置。如果您使用的是 Neo4j AuraDB 或 Neo4j Sandbox,则必须使用此选项。
如果您想将 CSV 文件用于您管理的 Neo4j 实例,您可以从 GitHub 上的 Northwind 文件 复制 CSV 文件,并将它们放置到您的 Neo4j DBMS 的 import 文件夹中。
您可以使用 Cypher 的 LOAD CSV
语句读取每个文件,并在其后添加 Cypher 子句,以获取行/列数据并将其转换为图形。
接下来,您将运行 Cypher 代码来
-
从 CSV 文件加载节点。
-
创建图形中数据的索引和约束。
-
创建节点之间的关系。
创建 **订单** 节点
执行此 Cypher 代码块以在数据库中创建订单节点
// Create orders
LOAD CSV WITH HEADERS FROM 'https://gist.githubusercontent.com/jexp/054bc6baf36604061bf407aa8cd08608/raw/8bdd36dfc88381995e6823ff3f419b5a0cb8ac4f/orders.csv' AS row
MERGE (order:Order {orderID: row.OrderID})
ON CREATE SET order.shipName = row.ShipName;
如果您已将 CSV 文件放置到 **import** 文件夹中,则应使用此代码语法从本地目录加载 CSV 文件
// Create orders LOAD CSV WITH HEADERS FROM 'file:///orders.csv' AS row MERGE (order:Order {orderID: row.OrderID}) ON CREATE SET order.shipName = row.ShipName;
此代码在数据库中创建了 830 个 订单
节点。
您可以通过执行此代码查看数据库中的一些节点
MATCH (o:Order) return o LIMIT 5;
图形视图是
表格视图包含节点属性的这些值
o |
---|
{"shipName":Vins et alcools Chevalier,"orderID":10248} |
{"shipName":Toms Spezialitäten,"orderID":10249} |
{"shipName":Hanari Carnes,"orderID":10250} |
{"shipName":Victuailles en stock,"orderID":10251} |
{"shipName":Suprêmes délices,"orderID":10252} |
创建 **产品** 节点
执行此代码以在数据库中创建产品节点
// Create products
LOAD CSV WITH HEADERS FROM 'https://gist.githubusercontent.com/jexp/054bc6baf36604061bf407aa8cd08608/raw/8bdd36dfc88381995e6823ff3f419b5a0cb8ac4f/products.csv' AS row
MERGE (product:Product {productID: row.ProductID})
ON CREATE SET product.productName = row.ProductName, product.unitPrice = toFloat(row.UnitPrice);
此代码在数据库中创建了 77 个 产品
节点。
您可以通过执行此代码查看数据库中的一些这些节点
MATCH (p:Product) return p LIMIT 5;
图形视图是
表格视图包含节点属性的这些值
p |
---|
{"unitPrice":18.0,"productID":1,"productName":Chai} |
{"unitPrice":19.0,"productID":2,"productName":Chang} |
{"unitPrice":10.0,"productID":3,"productName":Aniseed Syrup} |
{"unitPrice":22.0,"productID":4,"productName":Chef Anton’s Cajun Seasoning} |
{"unitPrice":21.35,"productID":5,"productName":Chef Anton’s Gumbo Mix} |
创建 **供应商** 节点
执行此代码以在数据库中创建供应商节点
// Create suppliers
LOAD CSV WITH HEADERS FROM 'https://gist.githubusercontent.com/jexp/054bc6baf36604061bf407aa8cd08608/raw/8bdd36dfc88381995e6823ff3f419b5a0cb8ac4f/suppliers.csv' AS row
MERGE (supplier:Supplier {supplierID: row.SupplierID})
ON CREATE SET supplier.companyName = row.CompanyName;
此代码在数据库中创建了 29 个 供应商
节点。
您可以通过执行此代码查看数据库中的一些这些节点
MATCH (s:Supplier) return s LIMIT 5;
图形视图是
表格视图包含节点属性的这些值
s |
---|
{"supplierID":1,"companyName":Exotic Liquids} |
{"supplierID":2,"companyName":New Orleans Cajun Delights} |
{"supplierID":3,"companyName":Grandma Kelly’s Homestead} |
{"supplierID":4,"companyName":Tokyo Traders} |
{"supplierID":5,"companyName":Cooperativa de Quesos 'Las Cabras'} |
创建 **员工** 节点
执行此代码以在数据库中创建供应商节点
// Create employees
LOAD CSV WITH HEADERS FROM 'https://gist.githubusercontent.com/jexp/054bc6baf36604061bf407aa8cd08608/raw/8bdd36dfc88381995e6823ff3f419b5a0cb8ac4f/employees.csv' AS row
MERGE (e:Employee {employeeID:row.EmployeeID})
ON CREATE SET e.firstName = row.FirstName, e.lastName = row.LastName, e.title = row.Title;
此代码在数据库中创建了 9 个 员工
节点。
您可以通过执行此代码查看数据库中的一些这些节点
MATCH (e:Employee) return e LIMIT 5;
图形视图是
表格视图包含节点属性的这些值
e |
---|
{"lastName":Davolio,"firstName":Nancy,"employeeID":1,"title":Sales Representative} |
{"lastName":Fuller,"firstName":Andrew,"employeeID":2,"title":Vice President, Sales} |
{"lastName":Leverling,"firstName":Janet,"employeeID":3,"title":Sales Representative} |
{"lastName":Peacock,"firstName":Margaret,"employeeID":4,"title":Sales Representative} |
{"lastName":Buchanan,"firstName":Steven,"employeeID":5,"title":Sales Manager} |
创建 **类别** 节点
// Create categories
LOAD CSV WITH HEADERS FROM 'https://gist.githubusercontent.com/jexp/054bc6baf36604061bf407aa8cd08608/raw/8bdd36dfc88381995e6823ff3f419b5a0cb8ac4f/categories.csv' AS row
MERGE (c:Category {categoryID: row.CategoryID})
ON CREATE SET c.categoryName = row.CategoryName, c.description = row.Description;
此代码在数据库中创建了 8 个 类别
节点。
您可以通过执行此代码查看数据库中的一些这些节点
MATCH (c:Category) return c LIMIT 5;
图形视图是
表格视图包含节点属性的这些值
c |
---|
{"description":Soft drinks, coffees, teas, beers, and ales,"categoryName":Beverages,"categoryID":1} |
{"description":Sweet and savory sauces, relishes, spreads, and seasonings,"categoryName":Condiments,"categoryID":2} |
{"description":Desserts, candies, and sweet breads,"categoryName":Confections,"categoryID":3} |
{"description":Cheeses,"categoryName":Dairy Products,"categoryID":4} |
{"description":Breads, crackers, pasta, and cereal,"categoryName":Grains/Cereals,"categoryID":5} |
对于非常大的商业或企业数据集,您可能会遇到内存不足错误,尤其是在较小的机器上。为了避免这种情况,您可以使用 |
创建图形中数据的索引和约束
创建节点后,您需要创建节点之间的关系。导入关系将意味着查找您刚创建的节点,并在这些现有实体之间添加关系。为了确保节点查找得到优化,您将为查找中使用的任何节点属性(通常是 ID 或其他唯一值)创建索引。
我们还希望创建一个约束(也会创建一个索引),该约束将不允许创建具有相同 ID 的订单,从而防止重复。最后,由于索引是在插入节点后创建的,因此它们的填充是异步进行的,因此我们调用 db.awaitIndexes()
来阻塞直到它们填充完毕。
执行此代码块
CREATE INDEX product_id FOR (p:Product) ON (p.productID);
CREATE INDEX product_name FOR (p:Product) ON (p.productName);
CREATE INDEX supplier_id FOR (s:Supplier) ON (s.supplierID);
CREATE INDEX employee_id FOR (e:Employee) ON (e.employeeID);
CREATE INDEX category_id FOR (c:Category) ON (c.categoryID);
CREATE CONSTRAINT order_id FOR (o:Order) REQUIRE o.orderID IS UNIQUE;
CALL db.awaitIndexes();
执行此代码后,您可以运行以下 Cypher 命令来查看数据库中的索引
SHOW INDEXES;
在创建 Neo4j 数据库时,默认情况下存在两个令牌查找索引(一个用于节点标签,一个用于关系类型)。它们专门解决节点标签和关系类型谓词,并帮助填充其他索引。删除它们可能会对性能产生负面影响。您应该在数据库中看到这些索引(和约束)
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|id|name |state |populationPercent|type |entityType |labelsOrTypes|properties |indexprovider |owningConstraint|lastRead |readCount|
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|7 |category_id |ONLINE|100.0 |RANGE |NODE |[Category] |[categoryID] |range-1.0 |null |null |0 |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|6 |employee_id |ONLINE|100.0 |RANGE |NODE |[Employee] |[employeeID] |range-1.0 |null |null |0 |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|1 |index_343aff4e|ONLINE|100.0 |LOOKUP|NODE |null |null |token-lookup-1.0|null |2023-12-06T12:30:12.510000000Z|2286 |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|2 |index_f7700477|ONLINE|100.0 |LOOKUP|RELATIONSHIP|null |null |token-lookup-1.0|null |null |0 |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|8 |order_id |ONLINE|100.0 |RANGE |NODE |[Order] |[orderID] |range-1.0 |order_id |2023-12-06T13:22:06.950000000Z|3815 |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|3 |product_id |ONLINE|100.0 |RANGE |NODE |[Product] |[productID] |range-1.0 |null |null |0 |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|4 |product_name |ONLINE|100.0 |RANGE |NODE |[Product] |[productName]|range-1.0 |null |null |0 |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|5 |supplier_id |ONLINE|100.0 |RANGE |NODE |[Supplier] |[supplierID] |range-1.0 |null |null |0 |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+
有关索引及其在 Neo4j 中使用的更多信息,请访问 Cypher 手册→索引的使用。
创建节点之间的关系
接下来,您必须创建关系
-
订单和员工之间。
-
产品和供应商之间,以及产品和类别之间。
-
员工之间。
创建订单和员工之间的关系
有了初始节点和索引,您现在可以创建订单与产品和订单与员工之间的关系。
执行此代码块
// Create relationships between orders and products
LOAD CSV WITH HEADERS FROM 'https://gist.githubusercontent.com/jexp/054bc6baf36604061bf407aa8cd08608/raw/8bdd36dfc88381995e6823ff3f419b5a0cb8ac4f/orders.csv' AS row
MATCH (order:Order {orderID: row.OrderID})
MATCH (product:Product {productID: row.ProductID})
MERGE (order)-[op:CONTAINS]->(product)
ON CREATE SET op.unitPrice = toFloat(row.UnitPrice), op.quantity = toFloat(row.Quantity);
此代码在图形中创建了 2155 个关系。
您可以通过执行此代码查看其中一些关系
MATCH (o:Order)-[]-(p:Product)
RETURN o,p LIMIT 10;
您的图形视图应该如下所示
然后,执行此代码块
// Create relationships between orders and employees
LOAD CSV WITH HEADERS FROM 'https://gist.githubusercontent.com/jexp/054bc6baf36604061bf407aa8cd08608/raw/8bdd36dfc88381995e6823ff3f419b5a0cb8ac4f/orders.csv' AS row
MATCH (order:Order {orderID: row.OrderID})
MATCH (employee:Employee {employeeID: row.EmployeeID})
MERGE (employee)-[:SOLD]->(order);
此代码在图形中创建了 830 个关系。
您可以通过执行此代码查看其中一些关系
MATCH (o:Order)-[]-(e:Employee)
RETURN o,e LIMIT 10;
您的图形视图应该如下所示
创建产品和供应商之间以及产品和类别之间的关系
接下来,创建产品、供应商和类别之间的关系
执行此代码块
// Create relationships between products and suppliers
LOAD CSV WITH HEADERS FROM 'https://gist.githubusercontent.com/jexp/054bc6baf36604061bf407aa8cd08608/raw/8bdd36dfc88381995e6823ff3f419b5a0cb8ac4f/products.csv
' AS row
MATCH (product:Product {productID: row.ProductID})
MATCH (supplier:Supplier {supplierID: row.SupplierID})
MERGE (supplier)-[:SUPPLIES]->(product);
此代码在图形中创建了 77 个关系。
您可以通过执行此代码查看其中一些关系
MATCH (s:Supplier)-[]-(p:Product)
RETURN s,p LIMIT 10;
您的图形视图应该如下所示
然后,执行此代码块
// Create relationships between products and categories
LOAD CSV WITH HEADERS FROM 'https://gist.githubusercontent.com/jexp/054bc6baf36604061bf407aa8cd08608/raw/8bdd36dfc88381995e6823ff3f419b5a0cb8ac4f/products.csv
' AS row
MATCH (product:Product {productID: row.ProductID})
MATCH (category:Category {categoryID: row.CategoryID})
MERGE (product)-[:PART_OF]->(category);
此代码在图形中创建了 77 个关系。
您可以通过执行此代码查看其中一些关系
MATCH (c:Category)-[]-(p:Product)
RETURN c,p LIMIT 10;
您的图形视图应该如下所示
创建员工之间的关系
最后,您将创建员工之间的 'REPORTS_TO' 关系来表示报告结构
执行此代码块
// Create relationships between employees (reporting hierarchy)
LOAD CSV WITH HEADERS FROM 'https://gist.githubusercontent.com/jexp/054bc6baf36604061bf407aa8cd08608/raw/8bdd36dfc88381995e6823ff3f419b5a0cb8ac4f/employees.csv' AS row
MATCH (employee:Employee {employeeID: row.EmployeeID})
MATCH (manager:Employee {employeeID: row.ReportsTo})
MERGE (employee)-[:REPORTS_TO]->(manager);
此代码在图形中创建了 8 个关系。
您可以通过执行此代码查看其中一些关系
MATCH (e1:Employee)-[]-(e2:Employee)
RETURN e1,e2 LIMIT 10;
您的图形视图应该如下所示
接下来,您将查询生成的图形以找出它能告诉我们有关新导入数据的哪些信息。
查询图形
我们可能会从几个通用查询开始,以验证我们的数据是否与我们在指南中早期设计的模型相匹配。以下是一些示例查询。
执行此代码块
//find a sample of employees who sold orders with their ordered products
MATCH (e:Employee)-[rel:SOLD]->(o:Order)-[rel2:CONTAINS]->(p:Product)
RETURN e, rel, o, rel2, p LIMIT 25;
执行此代码块
//find the supplier and category for a specific product
MATCH (s:Supplier)-[r1:SUPPLIES]->(p:Product {productName: 'Chocolade'})-[r2:PART_OF]->(c:Category)
RETURN s, r1, p, r2, c;
一旦您确信数据与我们的数据模型一致且一切看起来都正确,您就可以开始查询以收集信息和见解以供业务决策使用。
哪个员工的 'Chocolade' 和其他产品的交叉销售次数最多?
执行此代码块
MATCH (choc:Product {productName:'Chocolade'})<-[:CONTAINS]-(:Order)<-[:SOLD]-(employee),
(employee)-[:SOLD]->(o2)-[:CONTAINS]->(other:Product)
RETURN employee.employeeID as employee, other.productName as otherProduct, count(distinct o2) as count
ORDER BY count DESC
LIMIT 5;
看起来员工编号 4 很忙,不过员工编号 1 的表现也不错!您的结果应该如下所示
employee | otherProduct | count |
---|---|---|
4 |
Gnocchi di nonna Alice |
14 |
4 |
Pâté chinois |
12 |
1 |
Flotemysost |
12 |
3 |
Gumbär Gummibärchen |
12 |
1 |
Pavlova |
11 |
员工是如何组织的?谁向谁汇报?
执行此代码块
MATCH (e:Employee)<-[:REPORTS_TO]-(sub)
RETURN e.employeeID AS manager, sub.employeeID AS employee;
您的结果应该如下所示
manager | employee |
---|---|
2 |
3 |
2 |
4 |
2 |
5 |
2 |
1 |
2 |
8 |
5 |
9 |
5 |
7 |
5 |
6 |
请注意,员工编号 5 有人向其汇报,但同时也向员工编号 2 汇报。
接下来,让我们进一步调查一下。
哪些员工间接向对方汇报?
执行此代码块
MATCH path = (e:Employee)<-[:REPORTS_TO*]-(sub)
WITH e, sub, [person in NODES(path) | person.employeeID][1..-1] AS path
RETURN e.employeeID AS manager, path as middleManager, sub.employeeID AS employee
ORDER BY size(path);
您的结果应该如下所示
manager | middleManager | employee |
---|---|---|
2 |
[] |
3 |
2 |
[] |
4 |
2 |
[] |
5 |
2 |
[] |
1 |
2 |
[] |
8 |
5 |
[] |
9 |
5 |
[] |
7 |
5 |
[] |
6 |
2 |
[5] |
9 |
2 |
[5] |
7 |
2 |
[5] |
6 |
每个层次结构部分下了多少订单?
执行此代码块
MATCH (e:Employee)
OPTIONAL MATCH (e)<-[:REPORTS_TO*0..]-(sub)-[:SOLD]->(order)
RETURN e.employeeID as employee, [x IN COLLECT(DISTINCT sub.employeeID) WHERE x <> e.employeeID] AS reportsTo, COUNT(distinct order) AS totalOrders
ORDER BY totalOrders DESC;
您的结果应该如下所示
employee | reportsTo | totalOrders |
---|---|---|
2 |
[8,1,5,6,7,9,4,3] |
830 |
5 |
[6,7,9] |
224 |
4 |
[] |
156 |
3 |
[] |
127 |
1 |
[] |
123 |
8 |
[] |
104 |
7 |
[] |
72 |
6 |
[] |
67 |
9 |
[] |
43 |
下一步是什么?
如果您按照本指南的每个步骤进行操作,那么您可能希望通过更多查询来探索数据集,并尝试回答您为数据提出的其他问题。您可能还想将这些相同的原则应用于您自己的或另一个数据集进行分析。
如果您使用此流程将其应用于另一个数据集,或者您希望接下来这样做,请随时从头开始,使用另一个域再次完成本指南。步骤和过程仍然适用(当然,数据模型、查询和业务问题需要调整)。
如果您有需要进行比本指南中涵盖的更多清理和操作的数据,APOC 库 可能会有所帮助。它包含数百个用于处理大量数据、转换值、清理凌乱的数据源等的程序和函数!
如果您有兴趣对关系数据进行一次性初始转储到 Neo4j,那么Neo4j ETL 工具 可能就是您所需要的。该应用程序设计了一个点对点用户界面,其目标是快速、简单的关系到图形加载,帮助新老用户通过查看其数据作为图形而不是 Cypher、导入过程或其他代码来更快地获取价值。
资源
-
LOAD CSV:Cypher 用于导入 CSV 文件的命令
-
APOC 库:Neo4j 的实用程序库
-
Neo4j ETL 工具:无需代码即可加载关系数据