图数据库Neo4j数据建模系列(五) — 建模技巧及模型重构

图数据库Neo4j数据建模系列(五) — 建模技巧及模型重构

为了找到满足您需求的最佳数据模型,通常使用一些技巧来进行分析,并根据该分析做出数据模型决策。本文将讨论一些建模技巧,并重构模型。

构思查询

在建模前,首先要知道业务场景对数据提出的各种问题,而写出查询是确定数据模型结构的好方法。如果您知道查询需要在特定日期范围内返回结果,则应确保该日期不是节点上的属性,而是存储为单独的节点或关系。相反,对于大学课程而言,想找到与当前课程相似的课程产品,可以由课程上层类别入手,使搜索结果的所有课程更为有效。

即使您还不知道确切的查询语法,了解正在构建的系统或应用程序,围绕业务需求构建模型都将帮助您以更准确的方式设计。

查询优先级

为每个查询或功能找到理想的模型非常困难,正如我们在之前几篇文章中所讨论的那样,在选择模型时,需要权衡取舍。尽管您可能会改进某些方面,但无法获得一种万能的解决方案。

相反,您应该确定哪种模型适合您的需求。您可能无法最大程度地发挥每个查询的性能,但是您可以通过某些资源、时间和代码来充分利用系统。

为此,您需要确定哪些查询必须绝对具有最佳性能,以及哪些功能对于提供价值至关重要。这可能是一个艰难的决定,Neo4j更具价值的是,数据模型具有灵活性,并且能够根据您的优先级随时间变化而更改。

验证

我们可能会遇到在设计阶段未意识到的方案,找到这些的最佳方法之一是实际测试模型。
导入部分数据并在系统上执行测试和查询,这将确定您查询的结果是否适合您的需求或预期的性能。同样,Neo4j非常灵活,因此您可以调整模型或优化查询以优化输出。

在选择一个或多个模型时遇到困难?尝试为每个模型以及两者一起创建概念验证测试,并查看它们如何工作。什么是复杂的,什么不值得解决?是否有一种在现实生活中实际表现更好的方法,或者是多数据模型方法确实能为您带来最佳结果?有时,找出答案的最佳方法是使用实​真实数据对其进行测试。

模型重构

如上所述,Neo4j 模型始终可以进行更改。因此,数据模型具有一定的灵活性,并且易于调整,业务需求和优先级往往会波动,用户还可能改变其行为并引起业务变化。

Cypher 允许编写查询,执行批量更新、添加或删除属性,以及在结构中插入其他节点和关系。还提供了APOC标准库来帮助批处理查询和执行对集群实例的更新。

数据集的大小也会影响查询和性能。如果您的数据集较小,那么在更复杂的查询中可能看不到太多的性能影响。只有当数据量增加时,您才可能看到增加的影响。在这里,数据模型和查询优化对于最大化系统价值至关重要。

机场数据集

在本文中,我们将继续使用机场数据集,其中包含2008年1月美国各机场之间的连接。我们将数据存储在CSV文件中,这是将要导入到其中的图模型:


在导入任何数据之前,我们将在Airport标签和code属性上创建唯一约束,以确保我们不会意外导入重复的机场。以下Cypher 创建此约束:

CREATE CONSTRAINT ON (airport:Airport)
ASSERT airport.code IS UNIQUE

以下查询使用LOAD CSV工具,从 CSV 文件导入数据:

LOAD CSV WITH HEADERS FROM "raw.githubusercontent.com" AS row
MERGE (origin:Airport {code: row.Origin})
MERGE (destination:Airport {code: row.Dest})
MERGE (origin)-[connection:CONNECTED_TO {
airline: row.UniqueCarrier,
flightNumber: row.FlightNum,
date: date({year: toInteger(row.Year), month: toInteger(row.Month), day:toInteger(row.DayofMonth)}),
cancelled: row.Cancelled,
diverted: row.Diverted}]->(destination)
ON CREATE SET connection.departure = localtime(apoc.text.lpad(row.CRSDepTime, 4, "0")),
connection.arrival = localtime(apoc.text.lpad(row.CRSArrTime, 4, "0"))

该查询:

  • 创建带有Airport标签的节点,该标签的code属性的值来自OriginCSV文件中的列
  • 创建带有Airport标签的节点,该标签的code属性的值来自DestCSV文件中的列
  • 根据 CSV 文件中的列,创建具有几个属性的 CONNECTED_TO 类型关系。

运行此查询,我们将看到以下输出:

该模型是一个很好的入门模型,但是我们可以进行一些改进。

将属性转换为布尔值

CONNECTED_TO关系的属性divertedcancelled包含字符串值10。由于这些值表示布尔值,因此我们可以使用调用apoc.refactor.normalizeAsBoolean过程将值从字符串转换为布尔值。


以下查询对diverted属性进行转换:

MATCH (:Airport)-[connectedTo:CONNECTED_TO]->(:Airport)
CALL apoc.refactor.normalizeAsBoolean(connectedTo, "diverted", ["1"], ["0"])
RETURN count(*)

然后以下查询对cancelled属性进行转换:

MATCH (origin:Airport)-[connectedTo:CONNECTED_TO]->(departure)
CALL apoc.refactor.normalizeAsBoolean(connectedTo, "cancelled", ["1"], ["0"])
RETURN count(*)

如果有很多关系要更新,则尝试在一个事务中将它们全部提交,则可能会收到 OutOfMemory异常。因此,我们可以使用该apoc.periodic.iterate过程批量处理它们。以下查询针对同一查询中的cancelledreverted属性执行此操作:

UNWIND ["cancelled", "reverted"] AS propertyToDelete
CALL apoc.periodic.iterate(
"MATCH (:Airport)-[connectedTo:CONNECTED_TO]->(:Airport) RETURN connectedTo",
"CALL apoc.refactor.normalizeAsBoolean(connectedTo, $propertyToDelete, ['1'], ['0'])
RETURN count(*)",
{params: {propertyToDelete: propertyToDelete}, batchSize: 100})
YIELD batches
RETURN propertyToDelete, batches

apoc.periodic.iterate此查询过程需要3个参数:

  • 外部Cypher查询,用于查找并返回CONNECTED_TO要处理的关系数据。
  • 内部Cypher查询,用于处理这些CONNECTED_TO关系,并将这些关系上指定属性的任何值转换为布尔值。它使用apoc.refactor.normalizeAsBoolean过程来执行此操作,该过程本身具有几个参数:
    • 属性存在的实体
    • 属性名称的初始化
    • 应当考虑的值列表 true
    • 应当考虑的值列表 false
  • 该过程的配置值,包括:
    • params – 传递给那些Cypher查询的参数
    • batchSize– 控制在单个事务中运行的内部语句的批次大小

运行此查询时,将看到以下输出:

完成此操作后,我们可以编写以下查询以返回所有已取消的连接:

MATCH (origin:Airport)-[connectedTo:CONNECTED_TO]->(destination)
WHERE connectedTo.cancelled
RETURN origin.code AS origin,
destination.code AS destination,
connectedTo.date AS date,
connectedTo.departure AS departure,
connectedTo.arrival AS arrival

变关系为节点

接下来,假设我们要编写一个查询来查找特定的航班。对于我们现有的模型,这是非常困难的,因为航班是用关系表示的。我们可以对模型进行演化,根据CONNECTED_TO关系中存储的属性创建Flight节点。


以下查询执行此重构:

CALL apoc.periodic.iterate(
"MATCH (origin:Airport)-[connected:CONNECTED_TO]->(destination:Airport) RETURN origin, connected, destination",
"CREATE (flight:Flight {
date: connected.date,
airline: connected.airline,
number: connected.flightNumber,
departure: connected.departure,
arrival: connected.arrival,
cancelled: connected.cancelled,
diverted: connected.diverted
})
MERGE (origin)<-[:ORIGIN]-(flight)
MERGE (flight)-[:DESTINATION]->(destination)
DELETE connected",
{batchSize: 100})

与先前的查询一样,该查询使用该apoc.periodic.iterate过程,以便我们可以分批进行重构,而不是在单个事务中进行重构。该程序接受三个参数:

  • 外部Cypher查询,用于查找并返回CONNECTED_TO关系数据以及需要处理的起点和目的地机场。
  • 内部Cypher查询,用于处理这些实体,创建带有标签的节点Flight以及创建从该节点到始发地和目的地机场的关系。
  • batchSize配置,它设置为100,在单个事务中运行的内部语句的数量。

如果执行查询,我们将看到以下输出:

我们还可以使用apoc.refactor.extractNode过程进行重构。

CALL apoc.periodic.iterate(
"MATCH (origin:Airport)-[connected:CONNECTED_TO]->(destination:Airport)
RETURN origin, connected, destination",
"CALL apoc.refactor.extractNode([connected], ['Flight'], 'DESTINATION', 'ORIGIN')
YIELD input, output, error
RETURN input, output, error",
{batchSize: 100});

此操作与上一个查询相同,但是外部Cypher查询使用该apoc.refactor.extractNode过程创建Flight节点,并创建与始发和目的地机场的关系。如果运行此查询,我们将看到以下输出:

变属性为节点

目前,我们航班的航空公司已存储在节点airline上的属性Flight中。这意味着,如果我们想返回所有航空公司的信息数据,则需要扫描每个航班并检查每个航班的airline属性。

通过Airline为每个航空公司创建一个带有标签的节点,我们可以更轻松、更高效地编写此查询:


首先,我们在Airline标签和name属性上创建一个约束,以免创建重复的航空公司节点:

CREATE CONSTRAINT ON (airline:Airline)
ASSERT airline.name IS UNIQUE

我们可以执行以下查询来进行重构:

CALL apoc.periodic.iterate(
'MATCH (flight:Flight) RETURN flight',
'MERGE (airline:Airline {name:flight.airline})
MERGE (flight)-[:AIRLINE]->(airline)
REMOVE flight.airline',
{batchSize:10000, iterateList:true, parallel:false}
)

再次使用apoc.periodic.iterate具有以下参数的过程:

  • 一个外部Cypher语句,该语句返回Flight要处理的节点数据
  • 内部Cypher语句,用于处理这些飞行节点,Airline根据飞行airline属性创建节点,并创建AIRLINEFlightAirline节点的关系。然后,我们airlineFlight节点中删除该属性。

如果运行此查询,我们将看到以下输出:


然后,我们可以编写以下查询来查找航空公司和涉及每个航空公司的航班数量:

MATCH (airline:Airline)<-[:AIRLINE]-(:Flight)
RETURN airline.name AS airline, count(*) AS numberOfFlights

此操作与上一个查询相同,但是外部Cypher查询使用该apoc.refactor.extractNode过程创建Flight节点并创建与始发和目的地机场的关系。如果运行此查询,我们将看到以下输出:

本文介绍了如何在APOC库中的过程帮助下,重构图模型,期望对大家有帮助。有问题,请留言,一起交流学习。

图数据库Neo4j数据建模系列:

图数据库Neo4j数据建模系列(一) — 数据建模基础
图数据库Neo4j数据建模系列(二) — 数据建模准则
图数据库Neo4j数据建模系列(三) — 关系与图模型的转变
图数据库Neo4j数据建模系列(四) — 图数据建模
图数据库Neo4j数据建模系列(五) — 建模技巧及模型重构

至此,数据建模的相关概念及技巧就介绍完了,后续将基于一些实际的业务场景共同探讨图数据建模,敬请期待。

近期也在尝试 OrientDB 数据迁移到Neo4j,有时间的话,我也会更新一些迁移细节。

编辑于 05-27

文章被以下专栏收录