Neo4j实战:Cypher分析Neo4j数据不一致

翻译自:https://neo4j.com/blog/run-cypher-to-analyze-neo4j-graph-database-inconsistencies/
【阅读时间:9 分钟】

Neo4j 实战

        您以前是否想校验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 MoviePerson 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结果集

        您将看到,对于 MoviePerson,所有节点的属性名集合都不相同,需要您确定属性键之间的差异是否正确。
        有时,只是某些节点确实没有该属性的数据,因此未创建该属性,通常是正确的。而有时候,这可能意味着加载的旧数据不符合数据模型中的新更改,执行此查询至少说明可能存在问题。

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数据库的不同数据健康方面的其它查询。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容