理解执行计划

本页面介绍了如何理解 Cypher® 规划器生成的执行计划。它首先解释了 Cypher 查询的生命周期,然后逐步剖析了一个特定查询及其使用的执行计划。接着,它解释了惰性(lazy)查询评估与积极(eager)查询评估之间的区别。

Cypher 查询的生命周期

Cypher 查询始于一个表示为字符串的声明性查询,用于描述数据库中要匹配的图模式。经过解析后,查询字符串会通过查询优化器(也称为规划器),它会生成一个指令性计划(即逻辑计划),以确定在数据库当前状态下执行查询的最有效方式。[1] 在最后阶段,该逻辑计划被转换为可执行的物理计划,由该计划真正对数据库执行查询。执行此物理计划的任务由 Cypher 运行时(Runtime)负责。

示例图

为了说明如何理解 Cypher 执行计划,我们使用了一个基于英国国家铁路网络的图。图中的数据取自公开可用的数据集

该图包含两种类型的节点:StopStation。列车服务中的每个 StopCALLS_AT(停靠)一个 Station,并具有 arrivesdeparts 属性,分别给出了列车在车站的到达和出发时间。沿着 StopNEXT 关系可以找到该服务的下一个 Stop

要重新创建该图,请在空的 Neo4j 数据库中运行以下查询

查询
CREATE (pmr:Station {name: 'Peckham Rye'}),
  (dmk:Station {name: 'Denmark Hill'}),
  (clp:Station {name: 'Clapham High Street'}),
  (wwr:Station {name: 'Wandsworth Road'}),
  (clj:Station {name: 'Clapham Junction'}),
  (s1:Stop {arrives: time('17:19'), departs: time('17:20')}),
  (s2:Stop {arrives: time('17:12'), departs: time('17:13')}),
  (s3:Stop {arrives: time('17:10'), departs: time('17:11')}),
  (s4:Stop {arrives: time('17:06'), departs: time('17:07')}),
  (s5:Stop {arrives: time('16:58'), departs: time('17:01')}),
  (s6:Stop {arrives: time('17:17'), departs: time('17:20')}),
  (s7:Stop {arrives: time('17:08'), departs: time('17:10')}),
  (clj)<-[:CALLS_AT]-(s1), (wwr)<-[:CALLS_AT]-(s2),
  (clp)<-[:CALLS_AT]-(s3), (dmk)<-[:CALLS_AT]-(s4),
  (pmr)<-[:CALLS_AT]-(s5), (clj)<-[:CALLS_AT]-(s6),
  (dmk)<-[:CALLS_AT]-(s7),
  (s5)-[:NEXT {distance: 1.2}]->(s4),(s4)-[:NEXT {distance: 0.34}]->(s3),
  (s3)-[:NEXT {distance: 0.76}]->(s2), (s2)-[:NEXT {distance: 0.3}]->(s1),
  (s7)-[:NEXT {distance: 1.4}]->(s6)

本示例查询使用量化路径模式(quantified path pattern)来统计起点 StationDenmark Hill)和终点 StationClapham Junction)之间可能的路径模式数量。

查询
MATCH (:Station { name: 'Denmark Hill' })<-[:CALLS_AT]-(d:Stop)
      ((:Stop)-[:NEXT]->(:Stop))+
      (a:Stop)-[:CALLS_AT]->(:Station { name: 'Clapham Junction' })
RETURN count(*)

从图中可以看出,存在两条这样的路径(一条是从 Denmark Hill17:07 出发,经停 Clapham High StreetWandsworth Road 车站的服务;另一条是从 Denmark Hill17:10 出发的直达服务)。

然而,为了理解 Cypher 执行计划,查询结果本身并不重要,重要的是产生该结果的规划过程。

解读执行计划

Cypher 规划器生成的逻辑计划描述了特定查询将如何执行。此执行计划本质上是一个运算符的二叉树。运算符(Operator)是一个专门的执行模块,负责对数据进行某种转换,然后将其传递给下一个运算符,直到匹配到所需的图模式。因此,规划器生成的执行计划决定了将使用哪些运算符,以及为了实现原始查询中声明的目标,以何种顺序应用这些运算符。

要查看查询的计划,请在查询前加上 EXPLAIN —— 这不会运行查询,只会显示用于查找所需结果的运算符树。

查询
EXPLAIN
MATCH (:Station { name: 'Denmark Hill' })<-[:CALLS_AT]-(d:Stop)
      ((:Stop)-[:NEXT]->(:Stop))+
      (a:Stop)-[:CALLS_AT]->(:Station { name: 'Clapham Junction' })
RETURN count(*)

这是生成的执行计划[2]

+-------------------+----+------------------------------------------------------------------------+----------------+---------------------+
| Operator          | Id | Details                                                                | Estimated Rows | Pipeline            |
+-------------------+----+------------------------------------------------------------------------+----------------+---------------------+
| +ProduceResults   |  0 | `count(*)`                                                             |              1 | In Pipeline 3       |
| |                 +----+------------------------------------------------------------------------+----------------+---------------------+
| +EagerAggregation |  1 | count(*) AS `count(*)`                                                 |              1 |                     |
| |                 +----+------------------------------------------------------------------------+----------------+                     |
| +Filter           |  2 | NOT anon_1 = anon_5 AND anon_0.name = $autostring_0 AND anon_0:Station |              0 |                     |
| |                 +----+------------------------------------------------------------------------+----------------+                     |
| +Expand(All)      |  3 | (d)-[anon_1:CALLS_AT]->(anon_0)                                        |              0 |                     |
| |                 +----+------------------------------------------------------------------------+----------------+                     |
| +Filter           |  4 | d:Stop                                                                 |              0 |                     |
| |                 +----+------------------------------------------------------------------------+----------------+                     |
| +NullifyMetadata  | 14 |                                                                        |              0 |                     |
| |                 +----+------------------------------------------------------------------------+----------------+                     |
| +Repeat(Trail)    |  5 | (a) (...){1, *} (d)                                                    |              0 | Fused in Pipeline 2 |
| |\                +----+------------------------------------------------------------------------+----------------+---------------------+
| | +Filter         |  6 | isRepeatTrailUnique(anon_8) AND anon_7:Stop                            |              6 |                     |
| | |               +----+------------------------------------------------------------------------+----------------+                     |
| | +Expand(All)    |  7 | (anon_9)<-[anon_8:NEXT]-(anon_7)                                       |              6 |                     |
| | |               +----+------------------------------------------------------------------------+----------------+                     |
| | +Filter         |  8 | anon_9:Stop                                                            |             11 |                     |
| | |               +----+------------------------------------------------------------------------+----------------+                     |
| | +Argument       |  9 | anon_9                                                                 |             13 | Fused in Pipeline 1 |
| |                 +----+------------------------------------------------------------------------+----------------+---------------------+
| +Filter           | 10 | a:Stop                                                                 |              0 |                     |
| |                 +----+------------------------------------------------------------------------+----------------+                     |
| +Expand(All)      | 11 | (anon_6)<-[anon_5:CALLS_AT]-(a)                                        |              0 |                     |
| |                 +----+------------------------------------------------------------------------+----------------+                     |
| +Filter           | 12 | anon_6.name = $autostring_1                                            |              1 |                     |
| |                 +----+------------------------------------------------------------------------+----------------+                     |
| +NodeByLabelScan  | 13 | anon_6:Station                                                         |             10 | Fused in Pipeline 0 |
+-------------------+----+------------------------------------------------------------------------+----------------+---------------------+

运算符显示在结果表格的最左侧列中。解读执行计划时要记住的最重要的一点是,它们是从底向上读取的。因此,要跟踪此查询的执行情况,需要从底部或叶子运算符 NodeByLabelScan(它从节点标签索引中获取所有具有特定标签的节点)开始,然后逐步向上遍历运算符树,观察图中的数据如何被逐步细化,直到最后的根运算符 ProduceResults 为用户生成可读的结果。

要详细了解本示例中使用的运算符及其他运算符的具体作用,请参阅运算符一节。

id 列指定了分配给每个运算符的唯一 ID。ID 的顺序没有硬性规定,虽然它们通常从根运算符的 0 开始,并随着到达运算符树顶部的叶子运算符而递增。

执行计划中间的 Details 列描述了每个运算符执行的任务。例如,执行计划中间的 Repeat 运算符(id 5)的详细信息列指定了该运算符遍历了一个无上限的量化路径模式。

最后,Estimated Rows 列详细说明了每个运算符预计产生的行数。此估算值是基于现有统计信息的近似数字,规划器使用它来选择合适的执行计划。[3]

有关不同 Cypher 运行时如何更改特定执行计划的详细信息,请参阅运行时概念

惰性(Lazy)与积极(Eager)查询评估

通常,查询评估是惰性的。这意味着大多数运算符在输出行产生后,会立即将其管道传输给父运算符。换句话说,父运算符可能在子运算符尚未完全遍历完所有行之前,就已经开始消费子运算符产生的输入行了。

然而,某些运算符(例如用于聚合和排序的运算符)需要在产生输出之前聚合所有行。这些被称为积极(eager)运算符(例如上表中 ID 为 1 的 EagerAggregation 运算符)。此类运算符必须在执行完成后才能将任何行作为输入发送给其父级,有时这是为了强制执行正确的 Cypher 语义。有关 Cypher 查询逐行处理的更多信息,请参阅子句组合一节。


1. 关于数据库当前状态的相关信息包括哪些索引和约束可用,以及数据库维护的各种统计信息。Cypher 规划器使用这些信息来确定哪种访问模式将产生最佳执行计划。
2. 本节显示的执行计划格式是在使用 Cypher Shell 时生成的。由 Neo4j Browser 生成的执行计划使用不同的格式。
3. Neo4j 维护的统计信息包括:具有特定标签的节点数、按类型划分的关系数、每个索引的选择性,以及按类型划分的、以特定标签节点开始或结束的关系数。