当前位置: 代码迷 >> 综合 >> neo4j--Cypher查询调优与执行计划
  详细解决方案

neo4j--Cypher查询调优与执行计划

热度:106   发布时间:2023-11-17 22:13:21.0

1.查询调优

1.1查询如何执行

Cypher执行引擎会将每个Cypher查询都转为一个执行计划。在执行查询时,执行计划将告知Neo4j执行什么样的操作。

1.2查询性能分析

查看执行计划对查询进行分析时有两个Cypher语句可用:

1.2.1 EXPLAIN

如果只想查看查询计划,而不想运行该语句,可以在查询语句中加入EXPLAIN。此时,该语句将返回空结果,对数据库不会做出任何改变。

1.2.2 PROFILE

如果想运行查询语句并查看哪个运算符占了大部分的工作,可以使用PROFILE此时,该语句将被运行,并跟踪传递了多少行数据给每个运算符,以及每个运算符与存储层交互了多少以获取必要的数据。注意,加入PROFILE的查询语句将占用更多的资源,所以除非真正在做性能分析,否则不要使用PROFILE。

1.3查询调优举例

写一个找到'Tom Hanks'的查询语句。比较初级的做法是按如下方式写:

MATCH (p { name: 'Tom Hanks' })

RETURN p

这个查询将会找到'Tom Hanks'节点,但是随着数据库中节点数的增加,该查询将越来越慢。可以通过使用PROFILE来找到原因。

PROFILE

MATCH (p { name: 'Tom Hanks' })

RETURN p

首先需要记住的是,查看执行计划应该从底端往上看。在这个过程中,我们注意到从最后一行开始的Rows列中的数字远高于给定的name属性为'Tom Hanks'的一个节点。在Operator列中我们看到AllNodeScan被使用到了,这意味着查询计划器扫描了数据库中的所有节点。

 向上移动一行看Filter运算符,它将检查由AllNodeScan传入的每个节点的name属性。这看起来是一种非常低效的方式来查找'Tom Hanks'。

 解决这个问题的办法是,无论什么时候我们查询一个节点,都应该指定一个标签来帮助查询计划器缩小搜索空间的范围。对于这个查询,可简单地添加一个Person标签。

PROFILE

MATCH (p:Person { name: 'Tom Hanks' })

RETURN p

这次最后一行Rows的值已经降低了,这里没有扫描到之前扫描到的那些节点。NodeByLabelScan运算符表明首先在数据库中做了一个针对所有Person节点的线性扫描。一旦完成后,后续将针对所有节点执行Filter运算符,依次比较每个节点的name属性。

这在某些情况下看起来还可以接受,但是如果频繁通过name属性来查询Person,针对带有Person标签的节点的name属性创建索引将获得更好的性能。

CREATE INDEX ON :Person(name)

现在再次运行该查询将运行得更快。

PROFILE

MATCH (p:Person { name: 'Tom Hanks' })

RETURN p

查询计划下降到单一的行并使用了NodeIndexSeek运算符,它通过模式索引寻找到对应的节点。

1.4 USING语句

当执行一个查询时,Neo4j需要决定从查询图中的哪儿开始匹配。这是通过查看MATCH语句和WHERE中的条件这些信息来找到有用的索引或者其他开始节点。

然而,系统选定的索引未必总是最好的选择。

 可以通过USING来强制Neo4j使用一个特定的开始点。这个被称为计划器提示。这里有三种类型的计划器提示:索引提示,扫描提示和连接(join)提示。

1.4.1索引提示

索引提示用于告知计划器无论在什么情况下都应使用指定的索引作为开始点。对于某些特定值的查询,索引统计信息不准确,它可能导致计划器选择了非最优的索引。对于这种情况,索引提示就有它的用处。使用在MATCH语句之后添加USING INDEX variable:Label(property)来补充索引提示。

也可以补充多个索引提示,但是多个开始点会在后面的查询计划中潜在地需要额外的连接。

使用索引提示查询

上面的查询没有选择索引来生成计划。这是因为图非常小,对于小数据库标签扫描很快。然而,查询的性能通常以dbhit的值来度量。下面可以看到使用索引将获得更好的查询性能。

PROFILE

MATCH (p:Person { name: 'Tom Hanks' })

USING INDEX p: Person (name)

RETURN p.born AS column

使用多个索引提示查询

PROFILE

MATCH (p:Person { name: 'Tom Hanks' })-[r]->( m:Movie {title:"You've Got Mail"

})

USING INDEX p: Person (name)

USING INDEX m: Movie (title)

RETURN p.born AS column

使用稍微更好的计划返回'Barbara Liskov'的出生年。

1.4.2 扫描提示

如果查询匹配到一个索引的大部分,它可以更快地扫描标签并过滤掉不匹配的节点。通过在MATCH语句后面使用USING SCAN variable:Label可以做到这一点。它将强制Cypher不使用本应使用的索引,而采用标签扫描。

标签扫描提示

使用USING SCAN扫描一个标签上的所有节点并过滤结果集将获得最好的性能。

PROFILE

MATCH (s: Person)

USING SCAN s: Person

WHERE s.born < 1939

RETURN s.born AS column

1.4.3 连接(join)提示

强制在特定的点进行连接。连接和后续的连接提示将强制计划器查看额外的开始点。在那些没有更多好的开始点的情况下,可能会选取一些很差的开始点。这将对查询性能产生负面的效果。在其他情况下,这些提示会强制计划器选取一些看似不好的开始点,然后它会被证明是好的。

提示在单个节点上的连接

MATCH (n1: Person { name:'Keanu Reeves' })-[: ACTED_IN]->(m1: Movie)<-[: DIRECTED]-(n2: Person {name:'Lilly Wachowski' })-[: DIRECTED]->(m2: Movie { title:'The Matrix Reloaded' })

USING INDEX n1: Person (name)

USING INDEX n2: Person (name)

USING JOIN ON m1

RETURN m1.title AS column

提示在多个节点上的连接

MATCH (n1: Person { name:'Keanu Reeves' })-[: ACTED_IN]->(m1: Movie)<-[: DIRECTED]-(n2: Person {name:'Lilly Wachowski' })-[: DIRECTED]->(m2: Movie { title:'The Matrix Reloaded' })

USING INDEX n1: Person (name)

USING JOIN ON m1,n2

RETURN m1.title AS column

 

2.执行计划(了解)

本小节主要描述执行计划(Execution Plan)中的运算符。

Neo4j将执行一个查询的任务分解为一些被称为运算符的小块。每个运算符负责整个查询中的一小部分。这些以模式(pattern)形式连接在一起的运算符被称为一个执行计划。

每个运算符用如下统计信息来注解:

  1. Rows 运算符产生的行数。只有带有profile的查询才有。
  2. EstimatedRows由运算符所产生的预估的行数。编译器使用这个估值来选择合适的执行计划。
  3. DbHits每个运算符都会向Neo4j存储引擎请求像检索或者更新数据这样的工作。数据库命中次数就是DbHits

2.1 开始点运算符

这些运算符用于找到图的开始点。

2.1.1 全节点扫描

从节点库中读取所有节点。实参中的变量将包含所有这些节点。如果查询中使用这个运算符,在任何大点的数据库中就会遭遇性能问题。

MATCH (n)

RETURN n

2.1.2 通过id搜索有向关系

从关系库中通过id来读取一个或多个关系将返回关系和两端的节点。

MATCH (n1)-[r]->()

WHERE id(r)= 0

RETURN r, n1

2.1.3 通过id寻找节点

从节点库中通过id读取一个或多个节点。

MATCH (n)

WHERE id(n)= 0

RETURN n

2.1.4 通过标签扫描检索节点

使用标签索引 ,从节点的标签索引中获取拥有指定标签的所有节点。

MATCH (person:Person)

RETURN person

2.1.5 通过索引检索节点

使用索引搜索节点。节点变量和使用的索引在运算符的实参中。如果索引是一个唯一性索引,运算符将由一个被称为NodeUniqueIndexSeek的替代。

MATCH (location:Location { name: 'Malmo' })

RETURN location

2.1.6 通过索引范围(range)寻找节点

使用索引检索节点,节点的属性值满足给定的字符串前缀。这个运算符可用于STARTS WITH和比较符号,如<,>和>=。

MATCH (l:Location)

WHERE l.name STARTS WITH 'Lon'

RETURN l

2.1.7 通过索引包含(contains)扫描检索节点

一个节点的包含扫描将遍历存储在索引中的所有值,搜索实体中是否包含指定的字符串。这个比索引检索要慢,因为需要检查所有的实体,但也比直接通过标签扫描然后过滤属性库要更快一些。

MATCH (l:Location)

WHERE l.name CONTAINS 'al'

RETURN l

2.1.8 通过索引扫描检索节点

索引扫描将遍历存储在索引中的所有值,它可以找到拥有特定标签和特定属性的所有节点(如exists(n.prop))。

MATCH (l:Location)

WHERE exists(l.name)

RETURN l

2.1.9 通过id寻找无方向关系

从关系库中通过id读取一个或多个关系。对于每个关系将返回两行,它们分别为关系的开始节点和结束节点。

MATCH (n1)-[r]-()

WHERE id(r)= 1

RETURN r, n1

2.2 Expand运算符

这些运算符通过展开图模式来探索图。

2.2.1 Expand All

给定一个开始节点,expand-all将根据关系中的模式沿开始节点或者结束节点展开。它也能处理变长模式的关系。

MATCH (p:Person { name: 'me' })-[:FRIENDS_WITH]->(fof)

RETURN fof

2.2.2 Expand Into

当开始和结束节点都已经找到时,expand-into用于找到两个节点之间连接的所有关系

MATCH (p:Person { name: 'me' })-[:FRIENDS_WITH]->(fof)-->(p)

RETURN fof

2.2.3 可选Expand All

可选expand从一个给定节点开始遍历关系,确保在返回结果之前断言得到处理。如果没有找到匹配的关系,则返回null并产生一个结束节点变量。

MATCH (p:Person)

OPTIONAL MATCH (p)-[works_in:WORKS_IN]->(l)

WHERE works_in.duration > 180

RETURN p, l

2.3组合运算符

组合运算符用于将其他运算符拼接在一起。

2.3.1 Apply

Apply以嵌套循环的方式工作。Apply运算符左端返回的每一行作为右端运算符的输入,然后Apply将产生组合的结果。

MATCH (p:Person)-[:FRIENDS_WITH]->(f)

WITH p, count(f) AS fs

WHERE fs > 2

OPTIONAL MATCH (p)-[:WORKS_IN]->(city)

RETURN city.name

找到多于两个朋友的所有人,并返回它们所工作的城市。

2.3.2 SemiApply

测试一个模式断言的存在性。SemiApply从它的子运算符中获取一行,并将其作为右端的叶节点运算符的输入。如果右端运算符树至少产生一行结果,左端的这一行由SemiApply运算符产生。这使得SemiApply成为一个过滤运算符,可大量运用在查询的模式断言中。

MATCH (p:Person)

WHERE (p)-[:FRIENDS_WITH]->()

RETURN p.name

查到有朋友的所有人。

2.3.3 AntiSemiApply

测试一个模式断言的存在性。AntiSemiApply具有的功能与SemiApply相反,它进行反向过滤。

MATCH (me:Person { name: "me" }),(other:Person)

WHERE NOT (me)-[:FRIENDS_WITH]->(other)

RETURN other.name

查找所有不是我朋友的人的名字。

2.3.4 LetSemiApply

测试模式断言的存在性。当一个查询包含多个模式断言时,LetSemiApply将用于处理它们中的第一个。它会记录断言的评估结果,但会留下过滤器到另外一个运算符。

MATCH (other:Person)

WHERE (other)-[:FRIENDS_WITH]->() OR (other)-[:WORKS_IN]->()

RETURN other.name

找到有一个朋友或者在某地工作的所有人的名字。LetSemiApply运算符将用于检查每个人的 FRIENDS_WITH关系。

2.3.5 LetAntiSemiApply

测试模式断言的存在性。当一个查询包含多个模式断言时,LetAntiSemiApply将用于处理它们中的第一个。它会记录断言的评估结果,但会留下过滤器到另外一个运算符。下面的查询语句查找没有任何朋友或者在某地工作的所有人。

MATCH (other:Person)

WHERE NOT ((other)-[:FRIENDS_WITH]->()) OR (other)-[:WORKS_IN]->()

RETURN other.name

查找没有任何朋友或者在某地工作的所有人。LetAntiSemiApply运算符将用于检查每个人的FRIENDS_WITH关系的存在性。

2.3.6 SelectOrSemiApply

测试一个模式断言的存在性并评估一个断言。这个运算符允许将一般的断言与检查存在性的断言放在一起。首先评估普通表达式,仅当它返回false时模式断言才会执行。

MATCH (other:Person)

WHERE other.age > 25 OR (other)-[:FRIENDS_WITH]->()

RETURN other.name

查找有朋友或者年龄大于25的所有人的名字。

2.3.7 SelectOrAntiSemiApply

测试一个模式断言的存在性并评估一个断言。

MATCH (other:Person)

WHERE other.age > 25 OR NOT (other)-[:FRIENDS_WITH]->()

RETURN other.name

查找没有朋友或者年龄大于25的所有人的名字.

2.3.8 ConditionalApply

检查一个变量是否不为null,如果是,那么就执行右边的部分。

MERGE (p:Person { name: 'Andres' })

ON MATCH SET p.exists = TRUE

查看是否存在一个名为'Andres'的人。如果找到,就设置他的exists属性为true。

2.3.9 AntiConditionalApply

检查一个变量是否为null,如果是,那么就执行右边的部分。

MERGE (p:Person { name: 'Andres' })

ON CREATE SET p.exists = TRUE

查看是否存在一个名为'Andres'的人。如果没找到,则创建一个并设置其exists属性为true。

2.3.10 AssertSameNode

这个运算符用于确保没有违背唯一性约束。

MERGE (t:Team { name: 'Engineering', id: 42 })

查看Team中是否存在给定的名称 和id的成员。如果不存在,则创建一个。由于存在:Team(name)和:Team(id)两个唯一性约束,通过UniqueIndexSeek找到的任何节点必定是同一个 节点,否则就违背了唯一性约束。

2.3.11 NodeHashJoin

使用哈希表,NodeHashJoin将来自左端和右端的输入连接起来。

MATCH (andy:Person { name:'Andreas' })-[:WORKS_IN]->(loc)<-[:WORKS_IN]-(matt:Person { name:'Mattis' })

RETURN loc.name

返回匹配到的两个人同时都在那里工作的地方。

2.3.12 三元(Triadic)

三元用于解决三元查询,如很常用的查找我朋友的朋友中那些还不是我朋友的人。它先将所有的朋友放入一个集合,然后再检查他们是否已经与我相连。

 

MATCH (me:Person)-[:FRIENDS_WITH]-()-[:FRIENDS_WITH]-(other)

WHERE NOT (me)-[:FRIENDS_WITH]-(other)

RETURN other.name

        查找我朋友的朋友中那些还不是我朋友的所有人。

2.4 行运算符

这些运算符将其他运算符产生的行转换为一个不同的行集合。

2.4.1 Eager

为了隔离的目的,这个运算符确保在继续之前将那些会影响后续操作的运算在整个数据集上被完全地执行。否则,它可能触发无限循环。当导入数据或者迁移图结构时,Eager运算符会引起很高的内存消耗。在这种情况下,可将操作分解为更简单的步骤。例如,可以分别地导入节点和关系。另外,也可以先返回要更新的数据,然后再执行更新语句。

MATCH (a)-[r]-(b)

DELETE r,a,b

MERGE ()

2.4.2 Distinct

移除输入行流中重复的行。

MATCH (l:Location)<-[:WORKS_IN]-(p:Person)

RETURN DISTINCT l

2.4.3 Eager聚合

即时加载潜在的结果并存入哈希map中,使用分组键作为map的键。

MATCH (l:Location)<-[:WORKS_IN]-(p:Person)

RETURN l.name AS location, collect(p.name) AS people

2.4.4 从计数库获取节点数量

从计数库中得到节点的数量,比通过计数方式的eager聚合要快。然而,计数库中只保存了有限范围的组合,因此eager聚合对很多复杂的查询依然很有用。例如,可以从计数库中得到所有节点和拥有某个标签的节点的数量,但无法获取到超过一个标签的节点的数量。

MATCH (p:Person)

RETURN count(p) AS people

2.4.5 从计数库获取关系数量

从计数库中得到关系的数量,比通过计数方式的eager聚合要快。然而,计数库中只保存了有限范围的组合,因此eager聚合对很多复杂的查询依然很有用。例如,可以从计数库中得到所有关系,某个类型的关系的数量以及末尾节点上拥有某个标签的关系的数量,但无法获取到两端节点都有标签的关系的数量。

MATCH (p:Person)-[r:WORKS_IN]->()

RETURN count(r) AS jobs

2.4.6 过滤

过滤来自子运算符的每一行,仅仅让断言为true的结果通过。

MATCH (p:Person)

WHERE p.name =~ '^a.*'

RETURN p

2.4.7 Limit

返回输入的前n行。

MATCH (p:Person)

RETURN p

LIMIT 3

2.4.8 Projection

对于输入的每一行,projection将评估表达式并产生一行表达式的结果。

RETURN 'hello' AS greeting

2.4.9 Skip

跳过输入行的前n行。

MATCH (p:Person)

RETURN p

ORDER BY p.id

SKIP 1

2.4.10 根据给定的键进行排序。

MATCH (p:Person)

RETURN p

ORDER BY p.name

2.4.11 Top

返回根据给定键排序后的前n行。实际的运算符是Top,它仅保留前X行,而不像排序需要作用于整个输入。

MATCH (p:Person)

RETURN p

ORDER BY p.name

LIMIT 2

2.4.12 Union

Union将左右两个计划的结果连接在一起。

MATCH (p:Location)

RETURN p.name

UNION ALL MATCH (p:Country)

RETURN p.name

2.4.13 Unwind

将列表中的值以每行一个元素的形式返回。

UNWIND range(1, 5) AS value

RETURN value;

2.4.14 调用过程

返回以name为序的所有标签。

CALL db.labels() YIELD label

RETURN *

ORDER BY label

2.5 新运算符

这些运算符用于在查询中更新图。

2.5.1 约束操作

在一对标签和属性上创建一个约束。下面的查询在带有Country标签节点的name属性上创建一个唯一性约束。

CREATE CONSTRAINT ON (c:Country) ASSERT c.name IS UNIQUE

2.5.2 EmptyResult

即时加载产生的所有结果到EmptyResult运算符并丢弃掉。

CREATE (:Person)

2.5.3 更新图

对图进行更新操作。

CYPHER planner=rule

CREATE (:Person { name: 'Alistair' })

2.5.4 Merge Into

当开始和结束节点都已经找到,Merge Into用于找到这两个节点之间的所有关系或者创建一个新的关系。

CYPHER planner=rule

MATCH (p:Person { name: 'me' }),(f:Person { name: 'Andres' })

MERGE (p)-[:FRIENDS_WITH]->(f)

2.6最短路径规划

本小节讲解Cypher中的最短路径查找是如何规划的。

不同的断言在规划最短路径时可能导致Cypher中产生不同的查询计划。如果断言可以在搜索路径时处理,Neo4j将使用快速的双向广度优先搜索算法。因此,当路径上是普通断言时,这个快速算法可以一直确定地返回正确的结果。例如,当搜索有Person标签的所有节点的最短路径时,或者没有带有name属性的节点时。

如果在决定哪条路径是有效或者无效之前,断言需要检查所有路径,那么这个算法那就不能可靠地找到最短路径。Neo4j可能需要求助于比较慢的穷举深度优先搜索算法去寻找路径。

这两种算法的运行时间可能有数量级的差异,因此,对于时间敏感性查询确保使用的是快速算法很重要。

2.6.1 用快速算法检索最短路径

MATCH (ms:Person { name: 'Martin Sheen' }),(cs:Person { name: 'Charlie Sheen' }), p = shortestPath((ms)-[rels:ACTED_IN*]-(cs))

WHERE ALL (r IN rels WHERE exists(r.role))

RETURN p

这个查询可以使用快速算法——因为没有断言需要查看所有路径。

需要检查路径上额外断言的最短路径规划

2.6.2 考虑使用穷举搜索

在决定哪条是最短的匹配路径之前,WHERE语句中的断言需要应用于最短路径模式。

MATCH (cs:Person { name: 'Charlie Sheen' }),(ms:Person { name: 'Martin Sheen' }), p = shortestPath((cs)-[*]-(ms))

WHERE length(p)> 1

RETURN p

与前面那个相反,这个查询在知道哪条是最短路径之前,需要检查所有的路径。因此,查询计划将使用慢一些的穷举搜索算法。

这种更费时的穷举查询计划使用Apply/Optional来确保,当快速算法无法找到结果的时候返回一个null结果,而不是简单地停止结果流。在查询计划的顶部,查询器使用了一个AntiConditionalApply,如果路径变量指向的是null,那么它将运行穷举搜索。

2.6.3 禁止使用穷举搜索算法

这个查询与上面的查询一样,在知道哪条路径是最短路径之前需要检查所有路径。然而,使用WITH语句将使得查询计划不使用穷举搜索算法。由快速算法找到的任何路径接下来将被过滤掉,这可能会导致没有结果返回。

MATCH (cs:Person { name: 'Charlie Sheen' }),(ms:Person { name: 'Martin Sheen' }), p = shortestPath((cs)-[*]-(ms))

WITH p

WHERE length(p)> 1

RETURN p

 

  相关解决方案