Understanding query plans

This page explains how Cypher® queries are planned, and how to understand the query plans shown when using the EXPLAIN or PROFILE keywords.

The lifecycle of a Cypher query

A Cypher query begins as a declarative query represented as a string, describing the graph pattern to match in a database. After parsing, the query string goes through the query optimizer (also known as the planner), which produces a logical plan, determining the most efficient way to execute the query given the current state of the database.[1] In the final phase, this logical plan is turned into an executable physical plan, which actually runs the query against the database. Executing this physical plan is the task of the Cypher runtime.

EXPLAIN and PROFILE query plans

Query plans can be shown by prepending a query with either EXPLAIN or PROFILE.

  • EXPLAIN: plans the query but does not run it. Only returns estimated rows.

  • PROFILE: runs the query, and the plan shows real measurements (for example, the number of DB hits the plan involved). Unlike EXPLAIN, queries prepended with PROFILE can write data to the database. Note that profiling your query uses more resources, so you should not profile unless you are actively working on a query.

Example query plan

Find all Person nodes not FRIENDS_WITH me
PROFILE
MATCH (me:Person {name: 'me'}), (other:Person)
WHERE NOT (me)-[:FRIENDS_WITH]->(other)
RETURN other.name
Query plan
Cypher 25

Planner COST

Runtime PIPELINED

Runtime version 2026.05.0

Batch size 128

+--------------------+----+--------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+---------------------+
| Operator           | Id | Details                                                | Estimated Rows | Rows | DB Hits | Memory (Bytes) | Page Cache Hits/Misses | Time (ms) | Pipeline            | Indexes Used        |
+--------------------+----+--------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+---------------------+
| +ProduceResults    |  0 | `other.name`                                           |             13 |   12 |       0 |              0 |                    0/0 |     0,086 |                     |                     |
| |                  +----+--------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+                     +---------------------+
| +Projection        |  1 | other.name AS `other.name`                             |             13 |   12 |      24 |                |                    0/0 |     0,216 | In Pipeline 3       |                     |
| |                  +----+--------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+---------------------+
| +Apply             |  2 |                                                        |             13 |   12 |       0 |                |                    0/0 |     0,008 |                     |                     |
| |\                 +----+--------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+---------------------+
| | +Apply           |  3 |                                                        |             13 |   12 |       0 |                |                    0/0 |           |                     |                     |
| | |\               +----+--------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+---------------------+
| | | +Anti          |  9 |                                                        |             13 |   12 |       0 |           1256 |                    0/0 |     0,224 | In Pipeline 3       |                     |
| | | |              +----+--------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+---------------------+
| | | +Limit         |  8 | 1                                                      |              1 |    2 |       0 |            752 |                        |           |                     |                     |
| | | |              +----+--------------------------------------------------------+----------------+------+---------+----------------+                        |           |                     +---------------------+
| | | +Expand(Into)  |  4 | (me)-[:FRIENDS_WITH]->(other)                          |              1 |    2 |      16 |            632 |                        |           |                     |                     |
| | | |              +----+--------------------------------------------------------+----------------+------+---------+----------------+                        |           |                     +---------------------+
| | | +Argument      |  5 | me, other                                              |             14 |   14 |       0 |           4472 |                    1/0 |     6,150 | Fused in Pipeline 2 |                     |
| | |                +----+--------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+---------------------+
| | +NodeByLabelScan |  6 | other:Person                                           |             14 |   14 |      15 |           2424 |                    1/0 |     0,310 | In Pipeline 1       |                     |
| |                  +----+--------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+---------------------+
| +NodeIndexSeek     |  7 | RANGE INDEX me:Person(name) WHERE name = $autostring_0 |              1 |    1 |       2 |            376 |                    0/1 |     3,368 | In Pipeline 0       | range_index_name: 1 |
+--------------------+----+--------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+---------------------+

Total database accesses: 57, total allocated memory: 7024

12 rows
ready to start consuming query after 75 ms, results consumed after another 17 ms

Query plan parts

A query plan contains three parts:

  • Header: Cypher version, planner, runtime, and batch size (not included when using slotted runtime).

  • Table: binary tree of operators showing how the query is planned. The operators are listed in the leftmost column, and each operator is assigned one row in the plan.

  • Footer: total database accesses (sum of DB hits across all operators), plus optional totals for memory and parallel-runtime workers.

    • If EXPLAIN is used, ? will be shown instead of the number of total database accesses. Does not show the total allocated memory.

    • If PROFILE is used, the number of actual total database accesses will be shown. If some operators cannot be measured, the output appears as <n> + ?, where <n> indicates a lower bound. The total allocated memory represents the query-wide peak memory (not equivalent to the sum of each operator’s peak memory).

Query plan table columns

  • Columns only appear if at least one operator has a value for them.

  • Empty cells indicate that no data is available for the corresponding operator.

  • When a query uses either pipelined or parallel runtime, some operator profile data gets merged due to fusing and so can only be displayed per-pipeline rather than per-operator.

Column When Description Considerations

Operator

Always

Name of the operator (e.g. NodeByLabelScan, Expand(All), and Filter).

Id

Always

A stable id for the operator within this plan. Only useful for cross-referencing.

Details

Always

Operator-specific arguments. For example:

Estimated Rows

Always

The planner’s estimate of how many rows this operator will produce. Used for plan choice.

Derived from index/label statistics and a selectivity model, with no runtime feedback and can, therefore, be incorrect. Treat it as a hint, not a measurement, to compare with the Rows column.

Rows

PROFILE only

The number of rows the operator actually produced when the query executed.

DB Hits

PROFILE only

A count of low-level database access operations, including entity reads, property reads, and index entry reads. This metric reflects the amount of work performed by the storage layer. Lower values generally indicate more efficient query execution and improved performance.

DB Hits is not the same as Rows. A single matched row can produce many DB hits. For more information, see Database hits below.

Memory (Bytes)

PROFILE only

Per-operator peak heap memory used. Blank for operators that do not allocate memory.

Memory (Bytes) is per-operator peak, not cumulative. Two operators each reporting 100 MB does not mean 200 MB was used in total; they may have been active at different times. The query-wide peak is the total allocated memory line in the query plan footer.

Page Cache Hits/Misses

PROFILE only, enterprise edition

Page cache stats.

Measures how often data is found in memory versus requiring a read from disk. A warm cache means most data is already stored in memory, resulting in mostly hits. A cold cache means data must be loaded from disk, resulting in more misses, especially on the first execution. Results should not be compared across runs with different cache states.

Time (ms)

PROFILE only, pipelined / parallel runtime

Wall-clock time spent in this operator. For parallel runtime this gives the cumulative time spent by all workers.

Time (ms) in pipelined / parallel runtimes is per pipeline, not per operator. Operators that are fused together share a single time figure. A large Time (ms) on one row represents the whole fused pipeline.

Ordered by

When results are ordered using ORDER BY.

The provided order this operator guarantees, e.g. n.name ASC.

Pipeline

Pipelined / Parallel runtime

Which runtime pipeline the operator belongs to, e.g. Fused in Pipeline 3. Operators in the same fused pipeline are compiled together.

Indexes Used

PROFILE only

Which indexes the operator used and how many times each was accessed (not the number of rows that were read from the index). Useful in particular when debugging dynamic lookups.

Reading query plan tables

The Cypher planner produces logical plans that describe how a particular query is going to be executed. This query plan is a binary tree of operators. An operator is a specialized execution module that is responsible for some type of transformation to the data before passing it on to the next operator until the desired graph pattern has been matched. The plans produced by the planner thus decide which operators will be used and in what order they are applied to achieve the aim declared in the original query.

The most important thing to remember when reading execution plans is that they are read from the bottom up. The root operator (the top row of the query plan table) is ProduceResults, the operator responsible for producing results, and data flows upward from the leaf operator (the bottom row of the query plan table). The relationships between operators are described in terms of parent and child: an operator’s inputs are its child operators, and the operator receiving those inputs is its parent operator.

+ProduceResults <- Root operator
|
+Projection     <- Parent operator of Apply
|
+Apply          <- Child operator of Projection
|
| ...           (intermediate operators omitted)
|
+NodeIndexSeek  <- Leaf operator

Each operator can have 0, 1, or 2 child operators:

  • 0 children = leaf operator

  • 1 children = unary operator

  • 2 children = binary operator

For binary operators the convention is:

The right-hand input (RHS) is shown first, indented one level deeper. The left-hand input (LHS) is shown second, at the same indentation level as the operator itself.

+Apply         <- the binary operator
|\
| +RHS-subtree <- right-hand input (evaluated first / indented)
|
+LHS-subtree   <- left-hand input (same level as the binary operator)

For an Apply-style (i.e. performing nested loops) binary operator, the LHS is run once, then the RHS is run once per LHS row, with the LHS bindings available. For a Join or CartesianProduct, both sides are full inputs combined by the operator.

Full example

+--------------------+----+--------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+---------------------+
| Operator           | Id | Details                                                | Estimated Rows | Rows | DB Hits | Memory (Bytes) | Page Cache Hits/Misses | Time (ms) | Pipeline            | Indexes Used        |
+--------------------+----+--------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+---------------------+
| +ProduceResults    |  0 | `other.name`                                           |             13 |   12 |       0 |              0 |                    0/0 |     0,086 |                     |                     |
| |                  +----+--------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+                     +---------------------+
| +Projection        |  1 | other.name AS `other.name`                             |             13 |   12 |      24 |                |                    0/0 |     0,216 | In Pipeline 3       |                     |
| |                  +----+--------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+---------------------+
| +Apply             |  2 |                                                        |             13 |   12 |       0 |                |                    0/0 |     0,008 |                     |                     |
| |\                 +----+--------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+---------------------+
| | +Apply           |  3 |                                                        |             13 |   12 |       0 |                |                    0/0 |           |                     |                     |
| | |\               +----+--------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+---------------------+
| | | +Anti          |  9 |                                                        |             13 |   12 |       0 |           1256 |                    0/0 |     0,224 | In Pipeline 3       |                     |
| | | |              +----+--------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+---------------------+
| | | +Limit         |  8 | 1                                                      |              1 |    2 |       0 |            752 |                        |           |                     |                     |
| | | |              +----+--------------------------------------------------------+----------------+------+---------+----------------+                        |           |                     +---------------------+
| | | +Expand(Into)  |  4 | (me)-[:FRIENDS_WITH]->(other)                          |              1 |    2 |      16 |            632 |                        |           |                     |                     |
| | | |              +----+--------------------------------------------------------+----------------+------+---------+----------------+                        |           |                     +---------------------+
| | | +Argument      |  5 | me, other                                              |             14 |   14 |       0 |           4472 |                    1/0 |     6,150 | Fused in Pipeline 2 |                     |
| | |                +----+--------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+---------------------+
| | +NodeByLabelScan |  6 | other:Person                                           |             14 |   14 |      15 |           2424 |                    1/0 |     0,310 | In Pipeline 1       |                     |
| |                  +----+--------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+---------------------+
| +NodeIndexSeek     |  7 | RANGE INDEX me:Person(name) WHERE name = $autostring_0 |              1 |    1 |       2 |            376 |                    0/1 |     3,368 | In Pipeline 0       | range_index_name: 1 |
+--------------------+----+--------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+---------------------+
  1. NodeIndexSeek (id 7) — leaf operator at the bottom. Uses the range index on Person(name) to find the single node where name = 'me'. Produces 1 row.

  2. NodeByLabelScan (id 6) — leaf on the RHS branch. Scans all Person nodes, producing 14 rows (one per Person). This forms the LHS input to Apply (id 3), providing the other rows that are then tested by the Anti (id 9) operator.

  3. Argument (id 5) — leaf inside the RHS of the Anti subtree. Carries the already-bound variables me and other through. Produces 14 rows (no computation, just variable propagation).

  4. Expand(Into) (id 4) — for each (me, other) pair, checks whether a (me)-[:FRIENDS_WITH]→(other) pattern exists. This is executed once per input row (14 invocations), producing rows when matches are found.

  5. Limit (id 8) — restricts the expansion input to 1 row per evaluation context, controlling how many expansions are attempted per bound row in the anti pattern evaluation.

  6. Anti (id 9) — binary operator implementing NOT EXISTS. For each (me, other) pair from its LHS, it runs the RHS (Limit -> Expand) and only emits the LHS row if the RHS produces zero rows. In this case, 12 of 14 (me, other) pairs survive because no friendship edge exists.

  7. Apply (id 3) — binary operator. Its LHS is the NodeByLabelScan (providing other rows), and its RHS is the Anti subtree. For each LHS row, the RHS is executed with bindings available, so the anti-check is evaluated per other.

  8. Apply (id 2) — outer binary operator combining LHS: NodeIndexSeek (binding me) and RHS: Apply (id 3). The RHS runs once per me row, with me bound, producing the filtered other rows.

  9. Projection (id 1) — computes other.name for each remaining row. Produces 12 rows.

  10. ProduceResults (id 0) — root operator. Emits the final 12 rows to the client.

Lazy and eager query evaluation

In general, query evaluation is lazy. This means that most operators pipe their output rows to their parent operators as soon as they are produced. In other words, a child operator may not be fully exhausted before the parent operator starts consuming the input rows produced by the child.

However, some operators, such as those used for aggregation and sorting, need to aggregate all their rows before they can produce output. These operators are called eager operators. Such operators need to complete execution in its entirety before any rows are sent to their parents as input in order for the planner to preserve correctness (e.g. when reads and writes touch the same properties). Eager operators have a memory cost — the entire upstream output is buffered. The reason for an eager operator is shown in its Details column.

Database hits

Each operator will send a request to the storage engine to do work such as retrieving or updating data. A database hit (DBHits) is an abstract unit of this storage engine work.

These are all the actions that trigger one or more database hits:

  • Create actions

    • Create a node.

    • Create a relationship.

    • Create a new node label.

    • Create a new relationship type.

    • Create a new ID for property keys with the same name.

  • Delete actions

    • Delete a node.

    • Delete a relationship.

  • Update actions

    • Set one or more labels on a node.

    • Remove one or more labels from a node.

  • Node-specific actions

    • Get a node by its ID.

    • Get the degree of a node.

    • Determine whether a node is dense.

    • Determine whether a label is set on a node.

    • Get the labels of a node.

    • Get a property of a node.

    • Get an existing node label.

    • Get the name of a label by its ID, or its ID by its name.

  • Relationship-specific actions

    • Get a relationship by its ID.

    • Get a property of a relationship.

    • Get an existing relationship type.

    • Get a relationship type name by its ID, or its ID by its name.

  • General actions

    • Get the name of a property key by its ID, or its ID by the key name.

    • Find a node or relationship through an index seek or index scan.

    • Find a path in a variable-length expand.

    • Find a shortest path.

    • Ask the count store for a value.

  • Schema actions

    • Add an index.

    • Drop an index.

    • Get the reference of an index.

    • Create a constraint.

    • Drop a constraint.

  • Call a procedure.

  • Call a user-defined function.


1. The relevant information about the current state of the database includes which indexes and constraints are available, as well as various statistics maintained by the database. The Cypher planner uses this information to determine which access patterns will produce the best execution plan.