理解执行计划
本页面介绍了如何理解 Cypher® 规划器生成的执行计划。它首先解释了 Cypher 查询的生命周期,然后逐步剖析了一个特定查询及其使用的执行计划。接着,它解释了惰性(lazy)查询评估与积极(eager)查询评估之间的区别。
Cypher 查询的生命周期
Cypher 查询始于一个表示为字符串的声明性查询,用于描述数据库中要匹配的图模式。经过解析后,查询字符串会通过查询优化器(也称为规划器),它会生成一个指令性计划(即逻辑计划),以确定在数据库当前状态下执行查询的最有效方式。[1] 在最后阶段,该逻辑计划被转换为可执行的物理计划,由该计划真正对数据库执行查询。执行此物理计划的任务由 Cypher 运行时(Runtime)负责。
示例图
为了说明如何理解 Cypher 执行计划,我们使用了一个基于英国国家铁路网络的图。图中的数据取自公开可用的数据集。
该图包含两种类型的节点:Stop 和 Station。列车服务中的每个 Stop 都 CALLS_AT(停靠)一个 Station,并具有 arrives 和 departs 属性,分别给出了列车在车站的到达和出发时间。沿着 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)
本示例查询使用量化路径模式(quantified path pattern)来统计起点 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(*)
从图中可以看出,存在两条这样的路径(一条是从 Denmark Hill 于 17:07 出发,经停 Clapham High Street 和 Wandsworth Road 车站的服务;另一条是从 Denmark Hill 于 17: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 查询逐行处理的更多信息,请参阅子句组合一节。