查询调优
Neo4j
会尽可能地执行查询,然而利用行业的专业知识重新组织语句以获取更好的性能是很有用处的。然而手动优化的总目标是只从图中检索必要的数据,不必要的数据尽可能早地被过滤掉,以减少查询后期需要处理的数据量。同时也要避免返回整个节点和关系,尽量返回节点和关系中需要的数据。
Cypher
执行引擎会将每一个 Cypher
查询都转为一个执行计划。为了减少使用的资源,应尽可能地使用参数代替字面值,这会使得 Cypher
重用查询,而不必解析并构造新的执行计划。
查询如何执行
每个查询都被查询计划器转为一个执行计划,在执行查询时,执行计划将告知 Neo4j
执行什么操作。Neo4j
包含有两种不同的执行计划策略:
- 规则
查询计划器有用于产生查询计划的规则。他会考虑所有可用的索引,但不使用统计信息去指导查询编译。 - 成本
计划器使用统计服务为所有可选的查询赋予一个成本,然后选择耗费最少的那个。
默认情况下,Neo4j
会对所有的查询使用成本计划器,可以通过使用cypher.planner
配置来使用指定的计划器,当然也可以通过查看执行计划来查看使用的是哪个计划器。
查询性能分析
查看执行计划对查询进行分析时有两个选项:
EXPLAIN
如果仅查看执行计划而不运行该语句,则可以在查询语句中加入EXPLAIN
,执行该语句后会返回空结果,对数据库不会做任何操作。PROFILE
如果想运行查询语句并查看哪个运算符占据了大部分的工作,则可以使用PROFILE
。该语句执行时会跟踪传递了多少行数据给每个运算符,以及每个运算符与存储层交互以获取必要的数据。
使用PROFILE
的语句会使用更多的资源,因此除非在做性能分析,尽量不要使用PROFILE
。
在查询中尽可能地指出返回的关系类型和节点标签,这有助于 Neo4j
使用最合适的统计信息,进而产生更好的执行计划。
USING
USING
语句用于为一个查询构建执行计划时影响计划器的决定。
强制影响计划器的决定可能会导致查询性能的降低,因此谨慎使用。
当执行一个查询时,Neo4j
需要决定从查询图中的哪里开始匹配,取决于 MATCH
语句和 WHERE
中的条件这些信息来寻找有用的索引或者其他开始节点。那么可以使用 USING
来强制 Neo4j
使用一个特定的开始点,这个被称为计划器提示。
计划器提示可以分为三种:
- 索引提示
- 扫描提示
- 连接(
join
)提示
含有 START
的语句不能使用查询器提示。
后续的示例将使用一下图:
索引提示
索引提示用于告知计划器无论在什么情况下都应使用指定的索引作为开始点。在遇到特定值的查询时,索引统计信息不够准确导致计划器使用非最佳的索引,而这种情况下索引提示就非常有用。其使用在 MATCH
语句之后添加 USING INDEX variable:Label(property)
来补充索引提示。
当然也支持多个补充索引提示,但是多个开始点会在后面的查询计划中潜在地需要额外的连接。
扫描提示
如果查询匹配到一个索引的大部分,那么扫描索引可以快速的扫描标签并过滤掉不匹配的节点。通过在 MATCH
语句后面使用 USING SCAN variable:Lable
实现。其会强制 Cypher
不使用本应使用的索引,而采用标签扫描。
连接提示
连接提示是计划器提示中的高级选项。其不是用于找到查询计划的开始点,而是强制在特定的点进行连接。为了查询能够连接来自这些叶子节点的两个分支,这意味着在计划中会出现多个开始点,基于这一点 连接提示器 将强制计划器查看额外的开始点。
再查看额外的开始点时,如果没有更多好的开始点,则会使用较差的开始点,这将对查询性能产生负面的效果。但在部分情况下,强制提示器选取一些不好的开始点,但结果是好的。
执行计划
Neo4j
将执行一个查询的任务分解为一些被称为运算符,每个运算符负责整个查询中的一小部分。这些以模式(Pattern
)形式连接在一起的运算符则被称为一个执行计划。
每个运算符使用如下统计信息来注解:
Rows
EstimatedRows
DbHits
开始点运算符
开始点运算符用于找到图的开始点。
全节点扫描
从节点库中读取所有的节点。1
MATCH (n) RETURN n
通过
id
搜索有向关系
从关系库中通过id
来读取一个或多个关系,结果将返回关系和两端的节点。1
MATCH (n1)-[r]->() WHERE id(r) = 0 RETURN r, n1
通过
id
寻找节点
从节点库中通过id
读取一个或多个节点。1
MATCH (n) WHERE id(n) = 0 RETURN n
通过标签扫描检索节点
使用标签索引,从节点的标签索引中获取拥有指定标签的所有节点。1
MATCH (person:Person) RETURN person
通过索引检索节点
使用索引搜索节点,节点变量和使用的索引在运算符的实参中。1
MATCH (localtion:Location {name: "Vgbh"}) RETURN localtion
通过索引范围(
Range
)搜索节点
通过使用检索节点,节点的属性值满足给定的字符串前缀。1
MATCH (l:Location) WHERE l.name STARTS WITH 'V' RETURN l
通过索引包含(
Contains
)检索节点
一个节点的包含扫描将遍历存储在索引中的所有值,搜索实体中是否包含指定的字符串。1
MATCH (l:Location) WHERE l.name CONTAINS 'V' RETURN l
通过索引扫描检索节点
索引扫描将遍历存储在索引中的所有值,其可以找到拥有特定标签和特定属性的所有节点。1
MATCH (l:Location) WHERE exists(l.name) RETURN l
通过
id
检索无方向关系
从关系库中通过id
读取一个或多个关系。1
MATCH (n1)-[r]-() WHERE id(r) = 1 RETURN n1, r
Expand
运算符
Expand
运算符用于展开图模式来搜索图。
Expand All
给定一个关系节点expand-all
将根据关系中的模式沿开始节点或结束节点展开。也可以处理变长模式的关系。1
MATCH (p:Person {name: "Vgbh"})-[:FRIENDS_WITH]->(fof) RETURN fof
Expand into
当开始和结束节点都已经找到时expand-into
用于找到两个节点之间连接的所有关系。1
MATCH (p:Person {name: "Vgbh"})-[:FRIENDS_WITH]->(fof)-->(p) RETURN fof
可选
Expand All
可选expand
从一个开始节点开始遍历关系,确保在返回结果之前断言会被处理。如果没找到匹配的关系,则返回null
并产生一个结束节点变量。1
MATCH (p:Person) OPTIONAL MATCH (p)-[works_in:WORKS_IN]->(l) WHERE works_in.duration > 180 RETURN p, l
组合运算符
组合运算符用于将其他运算符拼接在一起。
Apply
Apply
以循环嵌套的方式工作。Apply
运算符左端返回的每一行作为右端运算符的输入,然后Apply
将产生组合的结果。1
MATCH (p:Person)-[:FRIENDS_WITH]->(f) WITH p, count(f) AS fs WHERE fs > 2 OPTIONAL MATCH (p)-[:WORKDS_IN]->(city) RETURN city.name
SemiApply
测试一个模式断言的存在性。SemiApply
从它的子运算符中获取一行,并将其作为右端的叶节点运算符的输入。如果右端运算符至少产生一行结果,左端的这一行由SemoApply
运算符产生。这使得SemiApply
成为一个过滤运算符,可大量运用在查询的模式断言中。1
MATCH (p:Person) WHERE (p)-[:FRIENDS_WITH]->() RETURN p.name
AntiSemiApply
测试一个模式断言的存在性。其功能与SemiApply
相反,进行反向过滤。1
MATCH (me:Person {name: "Vgbh"}), (other:Person) WHERE NOT (me)-[:FRIENDS_WITH]->(other) RETURN other.name
LetSemiApply
测试模式断言的存在性。当一个查询包含多个模式断言时,其将用于处理其中的第一个,记录断言的评估结果,但会留下过滤器到另外一个运算符。1
MATCH (other:Person) WHERE (other)-[:FRIENDS_WITH]->() OR (other)-[:WORKS_IN]->() RETURN other.name
LetAntiSemiApply
测试模式断言的存在性。1
MATCH (other:Person) WHERE NOT (other)-[:FRIENDS_WITH]->() OR (other)-[:WORKS_IN]->() RETURN other.name
SelectOrSemiApply
测试一个模式断言的存在性并评估一个断言。这个运算符允许将一般的断言与检查存在性的断言放在一起,首先评估普通表达式,仅当返回false
时模式断言才会执行。1
MATCH (other:Person) WHERE other.age > 25 OR (other)-[:FRIENDS_WITH]->() RETURN other.name
SelectOrAntiSemiApply
测试一个模式断言的存在性并评估一个断言。1
MATCh (other:Person) WHERE other.age > 25 OR NOT (other)-[:FRIENDS_WITH]->() RETURN other.name
ConditionalApply
检查一个变量是否不为null
,如果是则执行右边的部分。1
MERGE (p:Person {name: "Vgbh"}) ON MATCH SET p.exists = TRUE
AntiConditionalApply
检查一个变量是否为null
,如果是则执行右边的部分。1
MERGE (p:Person {name: "Vgbh"}) ON CREATE SET p.exists = TRUE
AssertSameNode
该运算符用于确保没有违背唯一性约束。1
MERGE (t:Term {name: "Vgbh", age: 37})
NodeHashJoin
使用哈希表NodeHashJoin
将来自左端和右端的输入连接起来。1
MATCH (andy:Person {name: "Vgbh"})-[:WORKS_IN]->(loc)<-[:WORKS_IN]-(other:Person {name: "five"}) RETURN loc.name
三元(
Triadic
)
三元用于解决三元查询,例如常见的查询中查找我朋友的朋友还不是我朋友的人,首先将所有的朋友放入一个集合,然后检查是否与我相连。1
MATCH (me:Person)-[:FRIENDS_WITH]-()-[:FRIENDS_WITH]-(other) WHERE NOT (me)-[:FRIENDS_WITH]-(other) RETURN other.name
行运算符
行运算符将其他运算符产生的行转换为一个新的行集合(Set
)。
Eager
为了隔离的目的,该运算符确保在继续之前将那些影响后续操作的运算在整个数据集上被完全执行。1
MATCH (a)-[r]-(b) DELETE r, a, b MERGE ()
Distinct
移除输入行流中重复的行。1
MATCH (l:Location)<-[:WORKS_IN]-(p:Person) RETURN DISTINCT l
Eager
聚合
即时加载潜在的结果并存入哈希map
中,使用分组键作为map
的键。1
MATCH (l:Location)<-[:WORKS_IN]-(p:Person) RETURN l.name AS location, collect(p.name) AS people
从计数库获取节点数量
从计数库中获取节点数量会比Eager
聚合的计数方式要快很多。1
MATCH (p:Person) RETURN count(p) AS people
从计数库获取关系数量
从关系库中获取节点数量会比Eager
聚合的计数方式要快很多。1
MATCH (p:Person)-[r:WORKS_IN]->() RETURN count(r) AS jobs
过滤
过滤来自子运算符的每一行,仅仅让断言为true
的结果通过。1
MATCH (p:Person) WHERE p.name =~ '^a.*' RETURN p
Limit
返回输入的前n
行。1
MATCH (p:Person) RETURN p LIMIT 5
Projection
对于输入的每一行projection
将评估表达式并产生一行表达式的结果。1
RETURN 'hello' AS greeting
Skip
跳过输入行的前n
行。1
MATCH (p:Person) RETURN p ORDER BY p.id SKIP 1
Sort
根据给定的键进行排序。1
MATCH (p:Person) RETURN p ORDER BY p.name
Top
返回根据给定键排序后的前n
行。1
MATCH (p:Person) RETURN p ORDER BY p.name TOP 2
Union
将左右两个计划的结果连接在一起。1
MATCH (p:Location) RETURN p.name UNION ALL MATCH (p:Country) RETURN p.name
Unwind
将列表中的值以每行一个元素的形式返回。1
UNWIND range(1, 5) AS value RETURN value
调用过程
返回以name
为序的所有标签。1
CALL db.labels() YIELD label RETURN * ORDER BY label
更新运算符
更新运算符用于在查询中更新图。
约束操作
在一对标签和属性上创建一个约束。1
CREATE CONSTRAINT ON (c:Country) ASSERT c.name IS UNIQUE
EmptyResult
即时加载产生的所有结果到EmptyResult
运算符并丢弃掉。1
CREATE (:Person)
更新图
对图进行更新操作。1
CYPHER planner=rule CREATE (:Person {name: "vgbhfive"})
Merge Into
当开始节点和结束节点已经找到时,Merge Into
用于找到这两个节点之间的所有关系或者创建一个新的关系。1
CYPHER planner=rule MATCH (p:Person {name: 'Vgbh'}), (other:Person {name: "five"}) MERGE (p)-[:FRIENDS_WITH]-(other)
Cypher
最短路径优化
不同的断言在规划最短路径时可能导致 Cypher
中产生不同的查询计划。如果断言可以在搜索路径时处理,Neo4j
将使用快速的 双向广度优先搜索算法,因此当路径上时普通断言时该算法可以一直正确地返回正确的结果。
若在决定哪条路径有效或无效之前,断言需要检查所有路径,那么就要使用 穷举深度优先算法 来寻找路径。
快速算法检索最短路径
1 | MATCH (v1:Person {name: 'Vgbh'}), (v2:Person {name: 'Five'}), p = shortestPath((v1)-[rels:ACTED_IN*]-(v2)) |
该查询没有查看所有路径,因此可以使用快速算法。
检查路径上额外断言的最短路径规划
使用穷举搜索算法
1
2
3MATCH (v1:Person {name: 'Vgbh'}), (v2:Person {name: 'Five'}), p = shortestPath((v1)-[*]-(v2))
WHERE length(p) > 1
RETURN p该查询与之前的查询不同,该查询需要知道所有路径的前提下才能找到最短路径,因此适合使用穷举搜索算法。
禁止穷举搜索算法
1
2
3
4MATCH (v1:Person {name: 'Vgbh'}), (v2:Person {name: 'Five'}), p = shortestPath((v1)-[*]-(v2))
WITH p
WHERE length(p) > 1
RETURN p该查询语句与上述的语句查询意图一致,但是使用
WITH
语句将使得查询计划不会使用穷举搜索算法,而使用快速搜索算法找的路径可能会导致没有结果返回。
引用
个人备注
此博客内容均为作者学习所做笔记,侵删!
若转作其他用途,请注明来源!