理解执行计划

本页面介绍了如何理解 Cypher® 规划器生成的执行计划。首先解释 Cypher 查询的生命周期,然后逐步分解一个特定的查询及其使用的执行计划。然后解释延迟查询评估和急切查询评估之间的区别。

Cypher 查询的生命周期

Cypher 查询从一个声明式查询开始,它以字符串的形式表示,描述要匹配的数据库中的图模式。在解析之后,查询字符串会经过查询优化器(也称为规划器),它会生成一个命令式计划,称为逻辑计划,以确定在给定数据库当前状态下执行查询的最有效方式。[1] 在最后阶段,此逻辑计划将转换为可执行的物理计划,该计划实际在数据库上运行查询。执行此物理计划是 Cypher 运行时 的任务。

runtimes cypher lifecycle

示例图

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

patterns qpp calling points

该图包含两种类型的节点:`Stop` 和 `Station`。火车服务上的每个 `Stop` 都 `CALLS_AT` 一个 `Station`,并且具有属性 `arrives` 和 `departs`,它们给出火车在 `Station` 处的时间。沿着 `Stop` 的 `NEXT` 关系将给出服务的下一个 `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)

此示例查询使用 量化路径模式 来计算起始 `Station` `Denmark Hill` 和结束 `Station` `Clapham Junction` 之间可能的路径模式数量

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

从图中可以看出,存在两种模式(一种是服务在 `17:07` 从 `Denmark Hill` 出发,并在 `Clapham High Street` 和 `Wandsworth Road` 车站停靠,另一种是直接服务在 `17:10` 从 `Denmark Hill` 出发)

patterns qpp solutions

但是,为了理解 Cypher 执行计划,查询结果不如产生它的规划有趣。

读取执行计划

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

要查看查询的计划,请在查询前面添加 `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(Trail) 运算符(`id 5`)的 `Details` 列指定运算符遍历没有向上限制的量化路径模式。

最后,预计行数 列详细说明了每个操作符预期产生的行数。这个估计值是基于可用统计信息得出的近似值,规划器使用它来选择合适的执行计划。[3]

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

延迟和急切查询评估

一般来说,查询评估是延迟的。这意味着大多数操作符在产生输出行后立即将其传递给父操作符。换句话说,子操作符可能在父操作符开始使用子操作符产生的输入行之前不会完全耗尽。

但是,某些操作符(例如用于聚合和排序的操作符)需要在产生输出之前聚合所有行。这些操作符称为急切操作符(参见上表中 EagerAggregation 操作符(id 1)示例)。这种操作符需要完整地执行,然后才能将任何行作为输入发送到其父级,并且有时需要强制执行正确的 Cypher 语义。有关 Cypher 查询逐行处理的更多信息,请参阅关于 子句组合 的部分。


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