教程:重构图数据模型

重构是更改数据模型和图的过程。需要重构数据模型的主要原因包括

  • 所建模的图未涵盖所有用例。

  • 出现了新的用例。

  • 针对这些用例的 Cypher® 性能不佳,尤其是在图扩展时。

为了满足这些需求,本教程将指导您设计、实现和测试一个使用更新后的 Cypher 进行重构的数据模型。

先决条件

本教程是《教程:创建图数据模型》的后续内容。在继续之前,您需要已创建的数据模型。

或者,您现在也可以从头开始创建。选择您偏好的部署方法,并使用此代码添加数据

CREATE (Apollo13:Movie {title: 'Apollo 13', tmdbID: 568, released: '1995-06-30', imdbRating: 7.6, genres: ['Drama', 'Adventure', 'IMAX']})
CREATE (TomH:Person {name: 'Tom Hanks', tmdbID: 31, born: '1956-07-09'})
CREATE (MegR:Person {name: 'Meg Ryan', tmdbID: 5344, born: '1961-11-19'})
CREATE (DannyD:Person {name: 'Danny DeVito', tmdbID: 518, born: '1944-11-17'})
CREATE (JackN:Person {name: 'Jack Nicholson', tmdbID: 514, born: '1937-04-22'})
CREATE (SleeplessInSeattle:Movie {title: 'Sleepless in Seattle', tmdbID: 858, released: '1993-06-25', imdbRating: 6.8, genres: ['Comedy', 'Drama', 'Romance']})
CREATE (Hoffa:Movie {title: 'Hoffa', tmdbID: 10410, released: '1992-12-25', imdbRating: 6.6, genres: ['Crime', 'Drama']})

MERGE (TomH)-[:ACTED_IN {roles:'Jim Lovell'}]->(Apollo13)
MERGE (TomH)-[:ACTED_IN {roles:'Sam Baldwin'}]->(SleeplessInSeattle)
MERGE (MegR)-[:ACTED_IN {roles:'Annie Reed'}]->(SleeplessInSeattle)
MERGE (DannyD)-[:DIRECTED]->(Hoffa)
MERGE (DannyD)-[:ACTED_IN {roles:'Robert "Bobby" Ciaro'}]->(Hoffa)
MERGE (JackN)-[:ACTED_IN {roles:'Hoffa'}]->(Hoffa)

CREATE (Sandy:User {name: 'Sandy Jones', userID: 1})
CREATE (Clinton:User {name: 'Clinton Spencer', userID: 2})

MERGE (Sandy)-[:RATED {rating:5}]->(Apollo13)
MERGE (Sandy)-[:RATED {rating:4}]->(SleeplessInSeattle)
MERGE (Clinton)-[:RATED {rating:3}]->(Apollo13)
MERGE (Clinton)-[:RATED {rating:3}]->(SleeplessInSeattle)
MERGE (Clinton)-[:RATED {rating:3}]->(Hoffa)

剩余或新增用例

假设您想知道特定语言有哪些电影可用。

要回答这个问题,您需要首先将这些信息添加到图中,就像在《教程:创建图数据模型》中添加用户信息一样。然而,添加新数据存在重复的风险,这反过来会影响图的性能

为了说明这种情况,请将新属性 languages 添加到 'Movie' 节点及其对应值

MATCH (Apollo13:Movie {title:'Apollo 13'})
MATCH (SleeplessInSeattle:Movie {title:'Sleepless in Seattle'})
MATCH (Hoffa:Movie {title:'Hoffa'})
SET Apollo13.languages = ['English']
SET SleeplessInSeattle.languages = ['English']
SET Hoffa.languages = ['English', 'Italian', 'Latin']

您更新后的图应如下所示

如果您想检索所有英文电影,请执行此查询

MATCH (m:Movie)
WHERE 'English' IN m.languages
RETURN m.title

此查询的结果回答了问题,并返回电影“阿波罗 13 号”、“西雅图夜未眠”和“霍法”

表 1. 结果
m.title

“阿波罗 13 号”

“西雅图夜未眠”

“霍法”

此查询检索所有 Movie 节点,然后测试 languages 属性是否包含值 English。这并非错误,但随着图的扩展,您可能会遇到两个问题

  • 为了执行查询,必须检索所有 Movie 节点 → 随着图的扩展,以这种方式建模数据会降低此类查询的性能。避免此问题的替代方法是创建索引

  • language 属性的相同属性值在许多 Movie 节点中重复(在本例中,所有节点都重复) → 如果许多节点共享相同的属性值,这表明此属性值可以转换为新实体,例如节点或关系。

解决这些问题的方案是将属性 languages 重构为一个节点,并使用新关系将其连接到 Movie 节点。

当前 重构后
Movie node with a language property
Movie node connected to a language node via an in language property.

消除重复数据

为了将节点属性 languages 重构为一个节点,您可以使用以下查询

MATCH (m:Movie)
WITH m, m.languages AS languages
UNWIND languages AS language
MERGE (l:Language {name: language})
MERGE (m)-[:IN_LANGUAGE]->(l)
REMOVE m.languages

通过分解查询,您应该执行以下操作

  1. UNWIND Movie 节点中的 languages 属性,并将其条目转换为新的 Language 节点。

  2. 创建 IN_LANGUAGE 关系,将 Movie 节点连接到其对应的 Language 节点

  3. Movie 节点中删除 languages 属性。

您的图现在应如下所示

重构后,您应该只有一个值为“English”的 Language 节点,并且等效电影连接到该节点。这消除了图中大量的重复数据,并在图扩展时提高了性能。

处理复杂数据

假设出现了一个新的用例,需要了解每部电影制片人的信息。关于制片人的部分数据包括他们的实际地址,这可以被认为是复杂数据。

您可以通过创建 ProductionCompany 节点和 address 属性将此信息添加到图中

CREATE (p:ProductionCompany {name:'Imagine Entertainment', country:'US', postalCode:90212, state:'CA', city:'Beverly Hills', address1:'10351 Santa Monica Blvd'})
MERGE (Apollo13:Movie {title:'Apollo 13'})
CREATE (p)-[:PRODUCED]->(Apollo13)
CREATE (jerseyFilms:ProductionCompany {name:'Jersey Films', country:'US', postalCode:90049, state:'CA', city:'Los Angeles', address1:'10351 Santa Monica Blvd'})
MERGE (hoffa:Movie {title:'Hoffa'})
CREATE (jerseyFilms)-[:PRODUCED]->(hoffa)

然而,以这种方式在节点上存储复杂数据可能没有益处,原因包括

  • 重复数据:同一地点可能存在多家制作公司,相同信息会在多个节点上重复。

    • 示例:在上一步中,您将属性“languages”重构为一个节点,以避免“English”条目在所有 Movie 节点上重复。

  • 过度获取:与节点信息相关的查询要求不必要地检索某个类别中的更多节点。

    • 示例:如果您想返回位于加利福尼亚州的制作公司,则需要扫描 ProductionCompany 节点的所有属性,以从 state 键中检索属性值 California。相反,一个代表 California 的节点可能是一条更短的路径,您也无需检索超出所需的信息。

    • 或者,您也可以创建索引

数据建模的目标是减少查询所触及的图的大小。 如果图包含大量重复数据,或者您的查询仍然过度获取数据,您可能需要再次重构您的模型。

在当前模型中,您以新的节点标签 ProductionCompany 形式添加了更多信息,其中包含多个地址属性。属性值包含大量重复数据,这是不可取的。为了使模型更高效,请检查重复的键值,看看是否可以将其转换为另一个实体,例如节点或关系。

在这种情况下,两家制作公司都位于加利福尼亚州,因此可以将州转换为 State 节点,并通过新的关系 LOCATED_AT 连接到制作公司

重构后,按州检索制作公司的查询现在可以根据 State.name 值进行过滤,而不是评估所有 ProductionCompany 节点的 ProductionCompany.state 属性。

如何重构您的图以处理复杂数据取决于您想要回答的问题以及图扩展时查询的性能。下一步是通过测试来衡量图中的性能

使用特定关系

特定关系是一种重构策略,当您的项目有需要不断检索特定信息的重复用例时,您可以使用它。使用它们的好处包括

  • 减少需要检索的节点数量。

  • 提高查询性能。

假设您经常需要检索关于演员在 1995 年的信息。查询可以是

MATCH (p:Person)-[:ACTED_IN]-(m:Movie)
WHERE p.name = 'Tom Hanks' AND m.released STARTS WITH '1995'
RETURN DISTINCT m.title AS Movie

但是,如果您创建了一个特定关系,例如 ACTED_IN_1995,当您查询相同信息时,您将改写成这样

MATCH (p:Person)-[:ACTED_IN_1995]-(m:Movie)
WHERE p.name = 'Tom Hanks'
RETURN m.title AS Movie

这样,查询将不需要检索所有连接到汤姆·汉克斯的 Movie 节点并读取它们的所有 m.released 属性,而只需检索那些通过特定关系 ACTED_IN_1995 与汤姆·汉克斯连接的电影的标题。因此,您可以避免过度获取并提高查询性能。

重新测试图

重构图后,您应该重新审视所有用例的查询,并确定其中是否有任何查询可以重写以利用重构。以下是列表

用例 查询示例

哪些人参演了电影?

MATCH (p:Person)-[:ACTED_IN]->(m:Movie {title:'Hoffa'})
RETURN p

谁执导了电影?

MATCH (p:Person)-[:DIRECTED]->(m:Movie {title:'Hoffa'})
RETURN p

一个人参演了哪些电影?

MATCH (p:Person {name:'Tom Hanks'})-[:ACTED_IN]->(m:Movie)
RETURN m

有多少用户评价了一部电影?

MATCH (m:Movie {title: 'Apollo 13'})
RETURN COUNT {(:User)-[:RATED]->(m)} AS `Number of reviewers`

谁是参演电影的最年轻的人?

MATCH (p:Person)-[:ACTED_IN]-(m:Movie)
WHERE m.title = 'Hoffa'
RETURN  p.name AS Actor, p.born as `Year Born` ORDER BY p.born DESC LIMIT 1

一个人在电影中扮演了什么角色?

MATCH (p:Person {name:'Tom Hanks'})-[a:ACTED_IN]->(m:Movie {title: 'Apollo 13'})
RETURN a.roles

根据 IMDb,特定年份评分最高的电影是哪部?

MATCH (m:Movie)
WHERE m.released STARTS WITH '1995'
RETURN  m.title as Movie, m.imdbRating as Rating ORDER BY m.imdbRating DESC LIMIT 1

一位演员参演了哪些剧情片?

MATCH (p:Person)-[:ACTED_IN]-(m:Movie)
WHERE p.name = 'Tom Hanks' AND
'Drama' IN m.genres
RETURN m.title AS Movie

哪些用户给电影评了 5 分?

MATCH (u:User)-[r:RATED]-(m:Movie)
WHERE m.title = 'Apollo 13' AND
r.rating = 5
RETURN u.name as Reviewer

哪些电影是英文的?

MATCH (m:Movie)
WHERE m.languages = 'English'
RETURN m.title as Movie in English

考虑到这一点,您现在应该确定是否有任何查询需要重写以利用重构,并在适用时重写它们。例如,对于用例“哪些电影是英文的?”

旧查询 重构后的查询
MATCH (m:Movie)
WHERE m.languages = 'English'
RETURN m.title as Movie in English
MATCH (m:Movie)-[:IN_LANGUAGE]->(l:Language)
WHERE l.name = 'English'
RETURN m.title as Movie in English

性能检查

在实际应用程序上测试时,尤其是在完全扩展的图上,您将看不到显著的改进,但您可能会看到检索行数的差异。

例如,如果您想查看查询所有 Person 节点的数据库命中次数,您需要在查询前添加 PROFILE 子句

PROFILE MATCH (n:Person)
RETURN n

这应该是结果

您可以在Cypher 手册 → 执行计划和查询优化中找到有关查询优化和计划的更多详细信息。

持续学习

您可以对模型进行的大部分重构都是关于重新利用或向图中添加更多信息。

您可以通过在 GraphAcademy 上学习互动课程《图数据建模基础》来查看更多示例,了解如何将 Person 节点拆分为 ActorDirector 节点,如何将 Movie 节点属性 genre 转换为节点,以及其他重构策略。

© . All rights reserved.