理解执行计划

本页描述如何理解 Cypher® 规划器生成的执行计划。它首先解释 Cypher 查询的生命周期,然后逐步分解特定查询及其使用的执行计划。接着,它解释了惰性求值和急切求值查询的区别。

Cypher 查询的生命周期

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

runtimes cypher lifecycle

示例图

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

patterns qpp calling points

该图包含两种类型的节点:StopStation。列车服务的每个 StopCALLS_AT 一个 Station,并具有 arrivesdeparts 属性,用于给出列车在 Station 的时间。沿着 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)

示例查询使用量化路径模式来计算起始 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:07Denmark Hill 出发,停靠 Clapham High StreetWandsworth Road 车站,另一种是列车于 17:10Denmark 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)的详细信息列指定该操作符遍历一个没有上限的量化路径模式。

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

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

惰性求值和急切求值查询

通常,查询求值是 惰性 的。这意味着大多数操作符一旦生成输出行,就会立即将其输出行传递给它们的父操作符。换句话说,子操作符可能在父操作符开始消费子操作符生成的输入行之前尚未完全耗尽。

然而,某些操作符,例如用于聚合和排序的操作符,需要在生成输出之前聚合所有行。这些操作符被称为急切操作符(例如,请参阅上表中 EagerAggregation 操作符(id 1))。此类操作符需要在将任何行作为输入发送给其父操作符之前完全完成执行,有时是为了强制执行正确的 Cypher 语义。有关 Cypher 查询逐行处理的更多信息,请参阅子句组合部分。


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