教程:将数据从关系数据库导入 Neo4j
简介
本教程展示了如何从关系数据库(PostgreSQL)导出数据并导入图数据库(Neo4j)的过程。你将学习如何通过转换模式和使用导入工具将数据从关系系统导入图数据库。
或者,你可以
-
创建 AuraDB 云实例。
-
启动一个空白的 Neo4j Sandbox。
-
下载并安装 Neo4j Desktop。
本指南使用了一个特定的数据集,但其原理可以应用于任何数据领域并重复使用。
你应该对属性图模型有基本的了解,并知道如何将数据建模为图。
关于数据领域
在本指南中,我们将使用 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
的关系(表示员工有经理)。 -
Orders
和Products
之间与连接表(Order Details
)的连接成为一个名为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 文件,你可以使用命令 psql -d northwind < export_csv.sql
对你的 RDBMS 运行此脚本。
注意:除非你想在自己的 Northwind RDBMS 上执行此脚本,否则无需运行此脚本。
使用 Cypher 导入数据
你可以使用 Cypher® 的 LOAD CSV
命令将 CSV 文件的内容转换为图结构。
当你使用 LOAD CSV
在数据库中创建节点和关系时,CSV 文件的存放位置有两种选择:
-
在你可管理的 Neo4j 实例的 import 文件夹中。
-
从公开可用的位置,例如 S3 存储桶或 GitHub 位置。如果你正在使用 Neo4j AuraDB 或 Neo4j Sandbox,则必须使用此选项。
如果你想将 CSV 文件用于你管理的 Neo4j 实例,你可以从 GitHub 上的 Northwind 文件 复制 CSV 文件,并将其放置到你的 Neo4j 数据库管理系统的 import 文件夹中。
你使用 Cypher 的 LOAD CSV
语句读取每个文件,并在其后添加 Cypher 子句,将行/列数据转换为图。
接下来你将运行 Cypher 代码来:
-
从 CSV 文件加载节点。
-
为图中的数据创建索引和约束。
-
创建节点之间的关系。
创建 Order 节点
执行此 Cypher 代码块以在数据库中创建 Order 节点
// 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 个 Order
节点。
你可以通过执行此代码来查看数据库中的一些节点
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} |
创建 Product 节点
执行此代码以在数据库中创建 Product 节点
// 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 个 Product
节点。
你可以通过执行此代码来查看数据库中的一些节点
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} |
创建 Supplier 节点
执行此代码以在数据库中创建 Supplier 节点
// 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 个 Supplier
节点。
你可以通过执行此代码来查看数据库中的一些节点
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'} |
创建 Employee 节点
执行此代码以在数据库中创建 Supplier 节点
// 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 个 Employee
节点。
你可以通过执行此代码来查看数据库中的一些节点
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} |
创建 Category 节点
// 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 个 Category
节点。
你可以通过执行此代码来查看数据库中的一些节点
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、导入过程或其他代码即可更快地获得价值。
资源
-
Northwind SQL、CSV 和 Cypher 数据文件,也可作为 zip 文件
-
LOAD CSV
:Cypher 导入 CSV 文件的命令 -
APOC 库:Neo4j 的实用工具库
-
Neo4j ETL 工具:无需代码加载关系数据