Neo4j和mysql强一致性_Neo4j Cypher 实战(一):数据一致性分析

您以前是否想校验Neo4j图数据库中的数据不一致?

也许您正在合并来自不同来源的数据,或者项目迭代过程中变更数据模型而没有重新加载数据。或者检查Neo4j图数据库并查找问题。这篇博客将探讨一种校验数据不一致的方法。

Neo4j在存储数据时非常灵活,属性图模型使具有相同标签的节点具有不同的节点属性。让我用一个简短的例子进一步解释。

假设您有一个项目来记录著名演员的历史,您会立即想到在 Neo4j 浏览器前端输入:play movies并加载 Cypher,快速入门。

以下是来自 Movies 例子中的某段Cypher:CREATE (TheMatrix:Movie {title:'The Matrix', released:1999, tagline:'Welcome to the Real World'})

CREATE (Keanu:Person {name:'Keanu Reeves', born:1964})

CREATE (Carrie:Person {name:'Carrie-Anne Moss', born:1967})

CREATE (Laurence:Person {name:'Laurence Fishburne', born:1961})

CREATE (Hugo:Person {name:'Hugo Weaving', born:1960})

CREATE (LillyW:Person {name:'Lilly Wachowski', born:1967})

CREATE (LanaW:Person {name:'Lana Wachowski', born:1965})

CREATE (JoelS:Person {name:'Joel Silver', born:1952})

CREATE (Keanu)-[:ACTED_IN {roles:['Neo']}]-> (TheMatrix),

(Carrie)-[:ACTED_IN {roles:['Trinity']}]-> (TheMatrix),

(Laurence)-[:ACTED_IN {roles:['Morpheus']}]-> (TheMatrix),

(Hugo)-[:ACTED_IN {roles:['Agent Smith']}]-> (TheMatrix),

(LillyW)-[:DIRECTED]-> (TheMatrix),

(LanaW)-[:DIRECTED]-> (TheMatrix),

(JoelS)-[:PRODUCED]-> (TheMatrix)

当前数据模型如下:

原数据模型

数据模型具有两个节点标签:Person和 Movie。Person和 Movie之间有两种关系:DIRECTED和 ACTED_IN,本文仅关注节点标签。

标签为 Person 的节点有两个属性:born、name,标签为 Movie 的节点,具有三个属性:released、agline、title。

现在,团队负责人对您说,当演员开始担任主角时,人们会更感兴趣。此外,客户还要求知道某个 Person 是否赢得了奥斯卡奖。最后,您还要把 Movie 节点的 tagline 属性删除。

要实现这些需求,您可以修改Cypher:CREATE (ToyStory4:Movie {title:'Toy Story 4', released:2019})

MERGE (Keanu:Person {name:'Keanu Reeves', born:1964})

SET Keanu.wonOscar = false, Keanu.filmDebut = 1985

MERGE (TomH:Person {name:'Tom Hanks', born:1956})

SET TomH.wonOscar = true, TomH.filmDebut = 1980

MERGE (TimA:Person {name:'Tim Allen', born:1953})

SET TimA.wonOscar = false, TimA.filmDebut = '1988 maybe?'

MERGE (AnnieP:Person {name:'Annie Potts', born:1952})

SET AnnieP.wonOscar = false, AnnieP.filmDebut = 1978

CREATE (Keanu)-[:ACTED_IN {roles:['Duke Caboom (voice)']}]-> (ToyStory4),

(TomH)-[:ACTED_IN {roles:['Woody (voice)']}]-> (ToyStory4),

(TimA)-[:ACTED_IN {roles:['Buzz Lightyear (voice)']}]-> (ToyStory4),

(AnnieP)-[:ACTED_IN {roles:['Bo Peep (voice)']}]-> (ToyStory4)

在新的Cypher中,Movie 删除了'tagline' ,并为 Person 添加了两个新属性 :wonOscar、filmDebut。

另外,请注意,为了避免创建重复节点,不使用 CREATE,而用 MERGE 查找和更新现有数据。

现在,我们的新模型如下所示:

新数据模型

通过原数据模型和新数据模型的图片对比,我们可以看出模型差异。但是无法看出数据库中加载的数据符合哪种模型,以及需要更新的节点数。

1、Cypher查找属性名的变化

实际上,我们可以编写Cypher来检查数据是否不一致。

下面的Cypher查询将通过Neo4j图数据库的节点标签遍历节点,并返回不同属性名的节点标签。/* Looks for Node Labels that have different sets of property keys */

WITH "MATCH (n:`$nodeLabel`)

WITH n

LIMIT 10000

UNWIND keys(n) as key

WITH n, key

ORDER BY key

WITH n, collect(key) as keys

RETURN '$nodeLabel' as nodeLabel, count(n) as nodeCount, length(keys) as keyLen, keys

ORDER BY keyLen" as cypherQuery

CALL db.labels()

YIELD label AS nodeLabel

WITH replace(cypherQuery, '$nodeLabel', nodeLabel) as newCypherQuery

CALL apoc.cypher.run(newCypherQuery, {})

YIELD value

WITH value.nodeLabel as nodeLabel, collect({ nodeCount: value.nodeCount, keyLen: value.keyLen, keys: value.keys}) as nodeInfoList

WHERE size(nodeInfoList) > 1

UNWIND nodeInfoList as nodeInfo

RETURN nodeLabel, nodeInfo.nodeCount as nodeCount, nodeInfo.keyLen as keyLen, nodeInfo.keys as keys

ORDER BY nodeLabel, keyLen

如果您使用 :play movies从Neo4j示例数据集中加载的 Movie 数据 ,然后执行ToyStory4 Cypher语句,则执行上面的Cypher语句将返回以下结果:

Cypher结果集

您将看到,对于 Movie 和 Person,所有节点的属性名集合都不相同,需要您确定属性键之间的差异是否正确。

有时,只是某些节点确实没有该属性的数据,因此未创建该属性,通常是正确的。而有时候,这可能意味着加载的旧数据不符合数据模型中的新更改,执行此查询至少说明可能存在问题。

2、建立查询

上面的Cypher语句相当复杂,为了能解释其工作方式,我们将一句一句的依次创建Cypher语句并做相关说明。

首先,让我们仅关注带有 Movie 标签的节点。

我们想在数据库中的所有 Movie 节点上查找,并列出它们具有的属性键:MATCH (n:Movie) RETURN keys(n)

这将返回如下内容:

结果集

我们使用 keys() 返回节点的属性键。请注意,节点的属性键集的存储顺序不同,返回的结果也将不同。我们首先需要对键进行排序,以确保 [title,tagline,released] 与 [released,tagline,title] 相同:MATCH (n:Movie)

UNWIND keys(n) as key

RETURN key

ORDER BY key

UNWIND keys(n) 将集合成员拆分成单独行返回,使用 ORDER BY 按字母顺序对键进行排序。检查输出结果,必须找出消除重复键名的方法。

结果集

为了去除重复键,我们使用了两个 WITH 语句。第一个 WITH 能够使用 ORDER BY 排序,因此可以将有序键集传递给第二个 WITH,第二个 WITH 用 collect(key) 将属性键集聚合为列表。MATCH (n:Movie)

UNWIND keys(n) as key

WITH n, key// need this to run ORDER BY

ORDER BY key

WITH n, collect(key) as keys

RETURN keys

注意,在每个 WITH 语句中都必须包含 WITH n 。在Cypher中,WITH用于传递中间结果,并在使用时创建新的变量范围。您要保留的任何变量都必须在 WITH 中声明,以便后面语句可以继续使用它们。

第一个 WITH n仅用于传递 n 到下一部分,以确保变量仍在范围内。

第二个 WITH n 用作分组,对于每个节点,n 将其属性键聚合为列表。

运行此查询语句返回的结果是:

结果集

现在我们可以看到 keys 是有序的,并且每个 Movie节点都获得了一组 keys 。我们要做的最后一件事是计算每个唯一 keys 集有多少个节点。

以下查询在 RETURN 子句中执行,Cypher 隐式地对 keys 和 keyLen分组,并使用 count(n) 对每个唯一键集的节点计数。MATCH (n:Movie)

UNWIND keys(n) as key

WITH n, key

ORDER BY key

WITH n, collect(key) as keys

RETURN count(n) as nodeCount, length(keys) as keyLen, keys

ORDER BY keyLen

通过返回结果,可以看到 2 个带有 ['released','title'] 的节点和 37 个带有 ['released','tagline','title'] 的节点。

结果集

3、执行所有节点标签的查询

现在,上面的查询语句适用于单个节点标签 Movie。我们的目标是使它适用于数据库中存在的所有节点标签,并且在不知道已经存在哪些节点标签的情况下执行。

实现此目标的第一步是执行下面的语句:CALL db.labels()

YIELD label AS nodeLabel

RETURN nodeLabel

这将列出数据库中的所有节点标签。

结果集

接下来是使用这些节点标签为每种节点标签创建单独的Cypher语句,我们可以使用db.labels() 返回的 nodeLabel 来创建每种标签的查询语句。WITH "some cypher statement" as cypherQuery

CALL db.labels()

YIELD label AS nodeLabel

RETURN cypherQuery + " for " + nodeLabel

现在,我们必须用 Movie Cypher语句代替“some cypher statement”,并进行以下修改:

1)Movie 替换为 $nodeLabel

2)添加 $nodeLabel 为 nodeLabel to RETURN

3)CALL db.labels() 后添加 WITH replace(cypherQuery, '$nodeLabel', nodeLabel)为newCypherQuerydb.labels()

进行这些替换将得到以下查询。

执行查询会为每个查询生成单独的Cypher语句 NodeLabel。请注意,即使使用 $nodeLabel,这也不是实际的参数化Cypher调用,目前您无法参数化节点标签。

$nodeLabel 用作字符串替换的占位符,调用 replace(…)会将 $nodeLabel 占位符更改为 db.labels() 返回的 nodeLabel 实际值 。WITH "MATCH (n:`$nodeLabel`)

UNWIND keys(n) as key

WITH n, key

ORDER BY key

WITH n, collect(key) as keys

RETURN '$nodeLabel' as nodeLabel, count(n) as nodeCount, length(keys) as keyLen, keys

ORDER BY keyLen" as cypherQuery

CALL db.labels()

YIELD label AS nodeLabel

WITH replace(cypherQuery, '$nodeLabel', nodeLabel) as newCypherQuery

RETURN newCypherQuery

执行上面的Cypher语句会返回以下结果:

结果集

现在,我们对每个节点标签都有一个Cypher查询,我们可以使用 apoc.cypher.run() 来执行每个查询,同时需要您的数据库中安装APOC。如果尚未安装APOC,请阅读以下说明以安装APOC。CALL apoc.cypher.run(newCypherQuery, {}) YIELD value

返回的值 apoc.cypher.run 包含已执行查询的结果。

对于Cypher查询中返回的每一行,value 都会生成一个映射,其中映射键是返回变量名称,而映射值是返回值,下面是示例的返回结果:{

"nodeCount": 2,

"keyLen": 2,

"nodeLabel": "Movie",

"keys": [

"released",

"title"]

}

为了完成我们的查询,我们必须处理这些结果以确定哪些节点标签可能具有不同的属性键。

首先,我们将其 nodeLabel 用作分组 key 并聚合返回值,对 nodeLabel 使用 collect() ,最终每个Cypher查询会返回一行,该 nodeInfoList 变量包含从Cypher查询返回的所有数据。WITH value.nodeLabel as nodeLabel, collect({

nodeCount: value.nodeCount,

keyLen: value.keyLen,

keys: value.keys}) as nodeInfoList

WHERE size(nodeInfoList) > 1

接下来,在 WHERE 子句中使用 size(nodeInfoList) > 1 来检查每个Cypher查询中是否超过1行。如果只有1行,我们不返回。单行意味着对于带有该节点标签的所有节点都具有相同的属性键集。这意味着数据没问题,我们只想返回不同属性键集的节点标签。

该查询的最后一部分使用 UNWIND来将 nodeInfoList 集合转换回单独的行 。我们还使用 ORDER BY nodeLabel和 keyLen 按字母顺序对节点标签排序。UNWIND nodeInfoList as nodeInfo

RETURN nodeLabel, nodeInfo.nodeCount as nodeCount, nodeInfo.keyLen as keyLen, nodeInfo.keys as keys

ORDER BY nodeLabel, keyLen

执行完成的查询,将产生以下结果(如前所示):

结果集

整个查询中最后一个部分,我添加了以下限制:WITH n LIMIT 10000

当对大型数据库执行此查询时,这提供了一种保护措施。

对于给定的节点标签,它将仅查看前 10,000 行。如果没有这种保护措施,则对于大型数据库,它很可能会耗尽内存。如果您不想只查看前 10,000 行,可以随意调整此限制,使用 SKIP添加或尝试一些不同的采样技术。

4、结论

通过执行Cypher查询来检查Neo4j数据库中的数据不一致,我们只研究了相同节点标签的属性键之间的差异。根据您的特定数据和数据模型,这可能是正常的,也可能不是,您需要根据项目需求做出判断。

您可以创建此查询的其它一些变体:一个变体可以收回可能具有不一致数据的特定行,另一个变体可以检查特定属性键中是否存在不一致的数据值。尝试看看是否可以使用Neo4j存储过程的强大功能以及动态生成的Cypher查询的执行来自己生成变体。这些变体使用与本文中描述的完全相同的技术,只需要一点额外的逻辑。

在以后的博客文章中,将继续探讨检查Neo4j数据库的不同数据健康方面的其它查询。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值