运行时概念

在 Cypher® 中,有三种类型的运行时:Slotted(插槽式)、Pipelined(流水线式)和 Parallel(并行式)。通常,默认运行时(企业版中的 Pipelined 运行时)能提供最佳的查询性能。然而,每种运行时都有各自的优缺点,在某些场景下,决定使用哪种运行时是最大化查询效率的重要一步。

本指南将逐步介绍这三种 Cypher 运行时背后的概念。对于不熟悉 Cypher 查询执行计划的读者,建议先阅读理解执行计划一节。

示例图

本页面的查询将使用以下图数据

该图包含两种类型的节点:Stop(站点)和 Station(车站)。每个火车服务的 Stop 都会 CALLS_AT 一个 Station,并具有 arrives(到达)和 departs(出发)属性,表示火车在车站的时间。通过 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)

Slotted 运行时

Slotted 运行时是 Neo4j 社区版的默认运行时。Neo4j 企业版用户必须在查询前加上 CYPHER runtime = slotted,才能使用该运行时。例如:

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

此查询将生成以下执行计划

Planner COST

Runtime SLOTTED

Runtime version 2026.03

+-------------------+----+------------------------------------------------------------------------+----------------+
| Operator          | Id | Details                                                                | Estimated Rows |
+-------------------+----+------------------------------------------------------------------------+----------------+
| +ProduceResults   |  0 | `count(*)`                                                             |              1 |
| |                 +----+------------------------------------------------------------------------+----------------+
| +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 |
| |                 +----+------------------------------------------------------------------------+----------------+
| +Repeat(Trail)    |  5 | (a) (...){1, *} (d)                                                    |              0 |
| |\                +----+------------------------------------------------------------------------+----------------+
| | +Filter         |  6 | isRepeatTrailUnique(anon_7) AND anon_2:Stop                            |              6 |
| | |               +----+------------------------------------------------------------------------+----------------+
| | +Expand(All)    |  7 | (anon_4)<-[anon_7:NEXT]-(anon_2)                                       |              6 |
| | |               +----+------------------------------------------------------------------------+----------------+
| | +Filter         |  8 | anon_4:Stop                                                            |             11 |
| | |               +----+------------------------------------------------------------------------+----------------+
| | +Argument       |  9 | anon_4                                                                 |             13 |
| |                 +----+------------------------------------------------------------------------+----------------+
| +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 |
+-------------------+----+------------------------------------------------------------------------+----------------+

Slotted 运行时生成的物理计划与逻辑计划是一一对应的,每个逻辑运算符映射到一个对应的物理运算符,并且运算符逐行处理数据。使用 Slotted 运行时,查询中的每个变量都有一个专用的“插槽(slot)”,运行时使用它来访问映射到该变量的数据,因此得名“Slotted”。

Slotted 运行时使用了大多数数据库采用的传统执行模型,即迭代器模型或“火山(Volcano)”模型。这是一个基于拉取(pull-based)的过程,树中的每个运算符通过虚函数调用从其子运算符“拉取”数据行。通过这种方式,数据从执行计划的底部被拉取到顶部,从而产生类似火山喷发的数据流。

注意事项

Slotted 运行时是 Neo4j 引入的第一个高性能运行时,取代了现已退役的原始(且较慢的)解释型运行时。

Slotted 运行时是一种解释型运行时,意味着它会逐个运算符地解释规划器发送的逻辑计划。通常,这是一种方便且灵活的方法,能够处理所有运算符和查询。Slotted 运行时在概念上类似于解释型编程语言,其规划阶段较短,因为它不需要在执行前生成查询的所有代码(与编译型运行时不同——详见下文)。[1]

通常,Neo4j 企业版用户不需要使用 Slotted 运行时。但在某些场景下,Slotted 运行时快速的规划阶段可能很有用。例如,如果您使用的应用程序会生成大量不被缓存的短查询(即从不或极少重复执行),那么由于其更快的规划时间,Slotted 运行时可能更合适。

然而,Slotted 运行时存在局限性。每个运算符之间持续调用虚函数会消耗 CPU 周期,导致查询执行变慢。此外,迭代器模型可能导致数据局部性差,从而降低查询速度。这是因为从不同运算符逐行拉取数据的过程使得 CPU 缓存难以得到高效利用。

Pipelined 运行时

Pipelined 运行时是 Neo4j 企业版和所有 Aura 层的默认运行时。这意味着除非 Neo4j Aura 或企业版用户指定不同的运行时,否则查询将使用 Pipelined 运行时执行。

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

生成的执行计划与 Slotted 运行时产生的计划有显著差异

Planner COST

Runtime PIPELINED

Runtime version 2026.03

Batch size 128

+-------------------+----+------------------------------------------------------------------------+----------------+---------------------+
| 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_7) AND anon_2:Stop                            |              6 |                     |
| | |               +----+------------------------------------------------------------------------+----------------+                     |
| | +Expand(All)    |  7 | (anon_4)<-[anon_7:NEXT]-(anon_2)                                       |              6 |                     |
| | |               +----+------------------------------------------------------------------------+----------------+                     |
| | +Filter         |  8 | anon_4:Stop                                                            |             11 |                     |
| | |               +----+------------------------------------------------------------------------+----------------+                     |
| | +Argument       |  9 | anon_4                                                                 |             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 |
+-------------------+----+------------------------------------------------------------------------+----------------+---------------------+

计划的最右侧一列显示它已被划分为四个不同的流水线(pipelines)。要理解什么是流水线,首先需要明白使用 Pipelined 运行时的查询与 Slotted 运行时不同,它们不是一次执行一行。相反,Pipelined 运行时允许物理运算符消耗和产生批次(batches)(每批约 100 到 1000 行,称为 morsels),这些批次被写入包含流水线数据和任务的缓冲区(buffers)中。流水线可以定义为一系列已融合在一起的运算符,以便运行时可以在同一个任务中一起执行它们。

因此,使用 Pipelined 运行时,逻辑运算符不会一一映射到对应的物理运算符。相反,逻辑运算符树被转换为包含流水线和缓冲区的执行图。

在此执行图中,查询从 pipeline 0 开始,最终产生一个 morsel 并写入 pipeline 1 的缓冲区。一旦 pipeline 1 有了要处理的数据,它就可以开始执行,进而为下一个流水线写入数据,依此类推。通过这种方式,数据沿着执行图被推送。

注意事项

Pipelined 运行时是一种基于推送(push-based)的执行模型,数据从叶运算符推送到其父运算符。与拉取模型(Slotted 使用的模型)不同,基于推送的执行模型可以将数据保留在局部变量中,这有几个好处:它能直接利用 CPU 寄存器,改善 CPU 缓存的使用,并避免了拉取模型中代价高昂的虚函数调用。

Pipelined 运行时非常适合事务性用例,即系统中并行运行大量查询的场景。这涵盖了大多数使用场景,因此它是 Neo4j 的默认运行时。

Pipelined 运行时是一个混合模型,可以使用解释型或编译型运行时。然而,因为它主要使用后者,所以它被视为一种编译型运行时。与解释型运行时不同,编译型运行时有一个代码生成阶段,随后是执行阶段,这通常导致较长的查询规划时间,但执行时间较短。

上文所述,极少数情况下 Neo4j 企业版用户可能不希望使用 Pipelined 运行时。但对于大多数查询而言,Pipelined 运行时是一种更高效的、能够处理所有运算符和查询的运行时。

Parallel 运行时

Slotted 和 Pipelined 运行时都在分配给单个 CPU 核心的单个线程中执行查询。在使用这两个运行时的情况下,通过在不同的 CPU 线程中并发运行多个查询(这是 OLTP 在线事务处理用例中的典型场景),仍然可以实现并行(广义上定义为在单个数据库环境中同时处理两组或多组操作)。另一个选择是使用 CALL {…​} IN CONCURRENT TRANSACTIONS 在同一个查询中并发运行多个事务。

然而,在某些场景下,特别是进行图分析时,单个查询使用多个核心来提高性能是有利的。这可以通过使用 Parallel 运行时来实现,它是多线程的,并允许查询利用运行 Neo4j 的服务器上的所有可用核心。

要指定查询使用 Parallel 运行时,请在查询前加上 CYPHER runtime = parallel。例如:

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

这是生成的执行计划

Planner COST

Runtime PARALLEL

Runtime version 2026.03

Batch size 128

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

Parallel 运行时生成的物理计划与 Pipelined 运行时的一个关键区别在于,通常情况下,使用 Parallel 运行时会产生更多的流水线(在此例中为 7 个,而 Pipelined 运行时仅产生 4 个)。这是因为在 Parallel 运行时中执行查询时,拥有更多可以并行运行的任务更有效;而在 Pipelined 运行时进行单线程执行时,将多个流水线融合在一起则更高效。

另一个重要的区别是 Parallel 运行时使用分区运算符(在此例中为 PartitionedNodeByLabelScan)。这些运算符首先对检索到的数据进行分段,然后并行地对每个段进行操作。

Parallel 运行时与 Pipelined 运行时共享相同的架构,这意味着它会将逻辑计划转换为上述类型的执行图。然而,使用 Parallel 运行时,每个流水线任务可以在单独的线程中执行。与 Pipelined 运行时的另一个相似之处是,Parallel 运行时的查询会先生成第一个流水线,最终在后续流水线的输入缓冲区中产生 morsel。但与 Pipelined 运行时一次只能推进一个流水线不同,Parallel 运行时允许流水线并发产生 morsels。因此,随着每个任务的完成,会有越来越多的输入 morsels 可供后续任务使用,这意味着可以使用更多的 worker 来执行查询。

为了进一步解释 Parallel 运行时的工作原理,需要定义一些新术语:

  • Worker(工作线程):执行工作单元以评估传入查询的线程。

  • Task(任务):工作单元。任务在输入 morsel 上执行一条流水线并产生一个输出 morsel。如果任何条件阻止了任务完成,它可以被重新调度为 Continuation(延续)以便稍后恢复。

  • Continuation(延续):未完成执行并需要再次调度的任务。

  • Scheduler(调度器):负责决定接下来处理哪个工作单元。调度是分散的,每个 worker 都有自己的调度器实例。

考虑下方的执行图,基于同样的示例查询:

执行图显示执行始于 pipeline 0,它包含 PartitionedNodeByLabelScan 运算符,可以在所有可用线程上同时对不同的数据 morsels 进行执行。一旦 pipeline 0 产生至少一个完整的 morsel 数据,任何线程都可以开始执行 pipeline 1,而其他线程可以继续执行 pipeline 0。更具体地说,一旦有来自某个流水线的数据,调度器就可以在并发执行早期流水线的同时推进到下一个流水线。在此例中,pipeline 5 以聚合结束(由 EagerAggregation 运算符执行),这意味着最后一个流水线(6)在所有前面的 morsel 数据处理完之前无法开始。

注意事项

何时使用 Parallel 运行时

在大多数拥有多个 CPU 核心的情况下,长时间运行的查询在 Parallel 运行时上通常会快得多。虽然无法定义查询在多长时才适合使用 Parallel 运行时(因为这取决于数据模型、查询结构、系统负载和可用核心数),但通常可以认为任何耗时超过约 500 毫秒的查询都是不错的候选者。

这意味着 Parallel 运行时适用于分析型图全局(graph-global)查询。这些查询通常不锚定在特定的起始节点,而是处理图的很大一部分以获取有价值的见解。

然而,对于从锚定特定节点开始的查询,如果满足以下任一条件,也可能受益于使用 Parallel 运行时:

  • 锚定的起始节点是一个连接密集的节点或超级节点。

  • 查询从锚定节点扩展到图的很大一部分。

因此,对于何时应使用 Parallel 运行时没有固定规则,但这些指导原则提供了一些有用的信息,说明了用户很可能会受益的场景。

何时不使用 Parallel 运行时

与被设计为大多数查询最有效规划方法的 Pipelined 运行时不同,Parallel 运行时的使用场景更具体,有些情况下它是不可能或无益的。最显著的是,Parallel 运行时仅支持读查询。它也不支持被认为非线程安全的存储过程函数(即在多个线程中运行不安全)。

此外,并非所有查询使用 Parallel 运行时都会更快。例如,一个从锚定节点开始且仅匹配图的一小部分的图局部(graph-local)查询,在 Parallel 运行时上可能不会运行得更快(由于调度和在多线程上执行查询所需的额外记录,甚至可能变慢)。经验法则通常是,对于完成时间少于半秒的查询,Parallel 运行时可能没有好处。

对于包含使用 ORDER BY 对索引属性进行排序的子句的查询,Parallel 运行时可能比 Pipelined 运行时性能更差。这是因为 Parallel 运行时无法利用属性索引进行排序,因此必须在返回结果前对聚合结果重新排序。

最后,虽然单个查询在运行 Parallel 运行时可能会更快,但由于运行许多并发查询,数据库的整体吞吐量可能会下降。

因此,Parallel 运行时不适用于高吞吐量的工作负载中的事务处理查询。然而,它非常适合数据库仅运行少量但高需求的读查询的分析场景。

概述

通常,如果满足以下条件,应考虑使用 Parallel 运行时:

  • 图全局(Graph-global)读查询旨在覆盖图的很大一部分。

  • 查询速度至关重要。

  • 服务器拥有多个 CPU 和足够的内存。

  • 数据库上的并发负载较低。

有关 Parallel 运行时的更多信息(包括关于查询、过程、函数、配置设置的详细信息,以及在 Aura 上使用 Parallel 运行时),请参阅并行运行时:参考页面。

总结

下表总结了 Cypher 中三种不同运行时之间最重要的区别:

Slotted Pipelined Parallel

执行模型

拉取 (Pull)

推送 (Push)

推送 (Push)

物理运算符消耗

逐行处理

批处理

批处理

处理器线程

单线程

单线程

多线程

运行时类型

解释型

编译型或解释型

编译型或解释型

支持的查询类型

读和写

读和写

只读


1. 将运行时分类为“解释型”或“编译型”并不完全准确。大多数运行时实现并非完全解释或完全编译,而是两者的结合。例如,当 Slotted 运行时在 Neo4j 企业版中运行时,会为查询中包含的表达式生成代码。尽管如此,Slotted 运行时仍被视为解释型,因为这是其实现的主要方法。
© . This site is unofficial and not affiliated with Neo4j, Inc.