【翻译自:https://neo4j.com/blog/run-cypher-to-analyze-neo4j-graph-database-inconsistencies/】
【阅读时间:9 分钟】
您以前是否想校验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语句将返回以下结果:
您将看到,对于 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数据库的不同数据健康方面的其它查询。