Neo4j系列博客-Cypher-3

查询调优

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 的语句不能使用查询器提示。

后续的示例将使用一下图:
cypher-3-1

索引提示

索引提示用于告知计划器无论在什么情况下都应使用指定的索引作为开始点。在遇到特定值的查询时,索引统计信息不够准确导致计划器使用非最佳的索引,而这种情况下索引提示就非常有用。其使用在 MATCH 语句之后添加 USING INDEX variable:Label(property) 来补充索引提示。
当然也支持多个补充索引提示,但是多个开始点会在后面的查询计划中潜在地需要额外的连接。

扫描提示

如果查询匹配到一个索引的大部分,那么扫描索引可以快速的扫描标签并过滤掉不匹配的节点。通过在 MATCH 语句后面使用 USING SCAN variable:Lable 实现。其会强制 Cypher 不使用本应使用的索引,而采用标签扫描。

连接提示

连接提示是计划器提示中的高级选项。其不是用于找到查询计划的开始点,而是强制在特定的点进行连接。为了查询能够连接来自这些叶子节点的两个分支,这意味着在计划中会出现多个开始点,基于这一点 连接提示器 将强制计划器查看额外的开始点。
再查看额外的开始点时,如果没有更多好的开始点,则会使用较差的开始点,这将对查询性能产生负面的效果。但在部分情况下,强制提示器选取一些不好的开始点,但结果是好的。


执行计划

Neo4j 将执行一个查询的任务分解为一些被称为运算符,每个运算符负责整个查询中的一小部分。这些以模式(Pattern)形式连接在一起的运算符则被称为一个执行计划
每个运算符使用如下统计信息来注解:

  • Rows
  • EstimatedRows
  • DbHits

开始点运算符

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

  1. 全节点扫描
    从节点库中读取所有的节点。

    1
    MATCH (n) RETURN n
  2. 通过 id 搜索有向关系
    从关系库中通过 id 来读取一个或多个关系,结果将返回关系和两端的节点。

    1
    MATCH (n1)-[r]->() WHERE id(r) = 0 RETURN r, n1
  3. 通过 id 寻找节点
    从节点库中通过 id 读取一个或多个节点。

    1
    MATCH (n) WHERE id(n) = 0 RETURN n
  4. 通过标签扫描检索节点
    使用标签索引,从节点的标签索引中获取拥有指定标签的所有节点。

    1
    MATCH (person:Person) RETURN person
  5. 通过索引检索节点
    使用索引搜索节点,节点变量和使用的索引在运算符的实参中。

    1
    MATCH (localtion:Location {name: "Vgbh"}) RETURN localtion
  6. 通过索引范围(Range)搜索节点
    通过使用检索节点,节点的属性值满足给定的字符串前缀。

    1
    MATCH (l:Location) WHERE l.name STARTS WITH 'V' RETURN l
  7. 通过索引包含(Contains)检索节点
    一个节点的包含扫描将遍历存储在索引中的所有值,搜索实体中是否包含指定的字符串。

    1
    MATCH (l:Location) WHERE l.name CONTAINS 'V' RETURN l
  8. 通过索引扫描检索节点
    索引扫描将遍历存储在索引中的所有值,其可以找到拥有特定标签和特定属性的所有节点。

    1
    MATCH (l:Location) WHERE exists(l.name) RETURN l
  9. 通过 id 检索无方向关系
    从关系库中通过 id 读取一个或多个关系。

    1
    MATCH (n1)-[r]-() WHERE id(r) = 1 RETURN n1, r

Expand 运算符

Expand 运算符用于展开图模式来搜索图。

  1. Expand All
    给定一个关系节点 expand-all 将根据关系中的模式沿开始节点或结束节点展开。也可以处理变长模式的关系。

    1
    MATCH (p:Person {name: "Vgbh"})-[:FRIENDS_WITH]->(fof) RETURN fof
  2. Expand into
    当开始和结束节点都已经找到时 expand-into 用于找到两个节点之间连接的所有关系。

    1
    MATCH (p:Person {name: "Vgbh"})-[:FRIENDS_WITH]->(fof)-->(p) RETURN fof
  3. 可选 Expand All
    可选 expand 从一个开始节点开始遍历关系,确保在返回结果之前断言会被处理。如果没找到匹配的关系,则返回 null 并产生一个结束节点变量。

    1
    MATCH (p:Person) OPTIONAL MATCH (p)-[works_in:WORKS_IN]->(l) WHERE works_in.duration > 180 RETURN p, l

组合运算符

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

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

    1
    MATCH (p:Person) WHERE (p)-[:FRIENDS_WITH]->() RETURN p.name
  3. AntiSemiApply
    测试一个模式断言的存在性。其功能与 SemiApply 相反,进行反向过滤。

    1
    MATCH (me:Person {name: "Vgbh"}), (other:Person) WHERE NOT (me)-[:FRIENDS_WITH]->(other) RETURN other.name
  4. LetSemiApply
    测试模式断言的存在性。当一个查询包含多个模式断言时,其将用于处理其中的第一个,记录断言的评估结果,但会留下过滤器到另外一个运算符。

    1
    MATCH (other:Person) WHERE (other)-[:FRIENDS_WITH]->() OR (other)-[:WORKS_IN]->() RETURN other.name
  5. LetAntiSemiApply
    测试模式断言的存在性。

    1
    MATCH (other:Person) WHERE NOT (other)-[:FRIENDS_WITH]->() OR (other)-[:WORKS_IN]->() RETURN other.name
  6. SelectOrSemiApply
    测试一个模式断言的存在性并评估一个断言。这个运算符允许将一般的断言与检查存在性的断言放在一起,首先评估普通表达式,仅当返回 false 时模式断言才会执行。

    1
    MATCH (other:Person) WHERE other.age > 25 OR (other)-[:FRIENDS_WITH]->() RETURN other.name
  7. SelectOrAntiSemiApply
    测试一个模式断言的存在性并评估一个断言。

    1
    MATCh (other:Person) WHERE other.age > 25 OR NOT (other)-[:FRIENDS_WITH]->() RETURN other.name
  8. ConditionalApply
    检查一个变量是否不为 null,如果是则执行右边的部分。

    1
    MERGE (p:Person {name: "Vgbh"}) ON MATCH SET p.exists = TRUE
  9. AntiConditionalApply
    检查一个变量是否为 null,如果是则执行右边的部分。

    1
    MERGE (p:Person {name: "Vgbh"}) ON CREATE SET p.exists = TRUE
  10. AssertSameNode
    该运算符用于确保没有违背唯一性约束。

    1
    MERGE (t:Term {name: "Vgbh", age: 37})
  11. NodeHashJoin
    使用哈希表 NodeHashJoin 将来自左端和右端的输入连接起来。

    1
    MATCH (andy:Person {name: "Vgbh"})-[:WORKS_IN]->(loc)<-[:WORKS_IN]-(other:Person {name: "five"}) RETURN loc.name
  12. 三元(Triadic
    三元用于解决三元查询,例如常见的查询中查找我朋友的朋友还不是我朋友的人,首先将所有的朋友放入一个集合,然后检查是否与我相连。

    1
    MATCH (me:Person)-[:FRIENDS_WITH]-()-[:FRIENDS_WITH]-(other) WHERE NOT (me)-[:FRIENDS_WITH]-(other) RETURN other.name

行运算符

行运算符将其他运算符产生的行转换为一个新的行集合(Set)。

  1. Eager
    为了隔离的目的,该运算符确保在继续之前将那些影响后续操作的运算在整个数据集上被完全执行。

    1
    MATCH (a)-[r]-(b) DELETE r, a, b MERGE ()
  2. Distinct
    移除输入行流中重复的行。

    1
    MATCH (l:Location)<-[:WORKS_IN]-(p:Person) RETURN DISTINCT l
  3. Eager 聚合
    即时加载潜在的结果并存入哈希 map 中,使用分组键作为 map 的键。

    1
    MATCH (l:Location)<-[:WORKS_IN]-(p:Person) RETURN l.name AS location, collect(p.name) AS people
  4. 从计数库获取节点数量
    从计数库中获取节点数量会比 Eager 聚合的计数方式要快很多。

    1
    MATCH (p:Person) RETURN count(p) AS people
  5. 从计数库获取关系数量
    从关系库中获取节点数量会比 Eager 聚合的计数方式要快很多。

    1
    MATCH (p:Person)-[r:WORKS_IN]->() RETURN count(r) AS jobs
  6. 过滤
    过滤来自子运算符的每一行,仅仅让断言为 true 的结果通过。

    1
    MATCH (p:Person) WHERE p.name =~ '^a.*' RETURN p
  7. Limit
    返回输入的前 n 行。

    1
    MATCH (p:Person) RETURN p LIMIT 5
  8. Projection
    对于输入的每一行 projection 将评估表达式并产生一行表达式的结果。

    1
    RETURN 'hello' AS greeting
  9. Skip
    跳过输入行的前 n 行。

    1
    MATCH (p:Person) RETURN p ORDER BY p.id SKIP 1
  10. Sort
    根据给定的键进行排序。

    1
    MATCH (p:Person) RETURN p ORDER BY p.name
  11. Top
    返回根据给定键排序后的前 n 行。

    1
    MATCH (p:Person) RETURN p ORDER BY p.name TOP 2
  12. Union
    将左右两个计划的结果连接在一起。

    1
    MATCH (p:Location) RETURN p.name UNION ALL MATCH (p:Country) RETURN p.name
  13. Unwind
    将列表中的值以每行一个元素的形式返回。

    1
    UNWIND range(1, 5) AS value RETURN value
  14. 调用过程
    返回以 name 为序的所有标签。

    1
    CALL db.labels() YIELD label RETURN * ORDER BY label

更新运算符

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

  1. 约束操作
    在一对标签和属性上创建一个约束。

    1
    CREATE CONSTRAINT ON (c:Country) ASSERT c.name IS UNIQUE
  2. EmptyResult
    即时加载产生的所有结果到 EmptyResult 运算符并丢弃掉。

    1
    CREATE (:Person)
  3. 更新图
    对图进行更新操作。

    1
    CYPHER planner=rule CREATE (:Person {name: "vgbhfive"})
  4. 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
2
3
MATCH (v1:Person {name: 'Vgbh'}), (v2:Person {name: 'Five'}), p = shortestPath((v1)-[rels:ACTED_IN*]-(v2))
WHERE ALL (r IN rels WHERE exists(r.role))
RETURN p

该查询没有查看所有路径,因此可以使用快速算法。

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

  1. 使用穷举搜索算法

    1
    2
    3
    MATCH (v1:Person {name: 'Vgbh'}), (v2:Person {name: 'Five'}), p = shortestPath((v1)-[*]-(v2))
    WHERE length(p) > 1
    RETURN p

    该查询与之前的查询不同,该查询需要知道所有路径的前提下才能找到最短路径,因此适合使用穷举搜索算法。

  2. 禁止穷举搜索算法

    1
    2
    3
    4
    MATCH (v1:Person {name: 'Vgbh'}), (v2:Person {name: 'Five'}), p = shortestPath((v1)-[*]-(v2))
    WITH p
    WHERE length(p) > 1
    RETURN p

    该查询语句与上述的语句查询意图一致,但是使用 WITH 语句将使得查询计划不会使用穷举搜索算法,而使用快速搜索算法找的路径可能会导致没有结果返回。


引用


个人备注

此博客内容均为作者学习所做笔记,侵删!
若转作其他用途,请注明来源!