Skip to content
Developer Edition runs on one host and does not require a product license file. Need multi-host deployment or performance testing at scale? Explore Enterprise Edition

EXPLAIN command

The EXPLAIN command displays the execution plan of a query. Unlike actually executing a query, the EXPLAIN command outlines the steps taken during query execution, which is useful for debugging and optimizing queries.

When using the ngql CLI, you can use the :planstyle command to switch between execution plan display styles (nest/table/box). The default style is nest. For more information, see Execution plan style setting in the ngql documentation.

Optimization Pipeline

The EXPLAIN command allows you to view the execution plan at different optimization stages. The full optimization pipeline is as follows:

  • RAW: The original logical plan before any optimization.
  • DECORRELATED: The logical plan after subquery decorrelation.
  • CBO: The logical plan after the CBO (Cost-Based Optimizer) stage.
  • Default (no stage specified): The final execution plan after all optimization stages are complete.

By examining plans at different stages, you can pinpoint specific issues in the optimization process, such as whether subquery rewriting took effect or whether the join order is reasonable.

Syntax

explain_command
EXPLAIN RAW CBO DECORRELATED FORMAT = " VERBOSE DEFAULT " gql_query

explain_command ::=
    'EXPLAIN' ( 'RAW' | 'CBO' | 'DECORRELATED' )? ( 'FORMAT' '=' '"' ( 'VERBOSE' | 'DEFAULT' ) '"' )? gql_query

Parameters

RAW | CBO | DECORRELATED

Specifies the optimization stage at which to view the execution plan. Each stage corresponds to a different processing node in the query optimization pipeline.

The stages are described below:

Stage Description
RAW Returns the original logical plan before any optimization.
DECORRELATED Returns the logical plan after subquery decorrelation. In this stage, correlated subqueries — those that reference outer variables and would otherwise require re-execution of the inner query for each row — are rewritten into equivalent non-nested forms (such as joins or aggregations), enabling subsequent standard optimizations.
CBO Returns the logical plan after the CBO (Cost-Based Optimizer) stage. This stage focuses on join reordering and cost-based decisions for graph pattern matching. Not all queries go through this stage; it takes effect only for queries involving eligible graph patterns or join reordering scenarios. The optimizer's ability to change the expansion order of MATCH patterns is controlled separately by enable_reorder, which defaults to false.

If this parameter is omitted, the final execution plan after all optimization stages is returned.

FORMAT = "VERBOSE" | FORMAT = "DEFAULT"

There are two output formats for the execution plan: VERBOSE and DEFAULT.

  • The VERBOSE FORMAT is more detailed and is suitable for debugging and optimizing queries. Use the VERBOSE FORMAT by specifying the FORMAT = VERBOSE option.
  • The DEFAULT FORMAT is a simplified version of the VERBOSE FORMAT. The DEFAULT FORMAT is used when both FORMAT = DEFAULT and FORMAT = "VERBOSE" options are omitted.

The information returned by both formats is as follows:

Parameter Description
plan Represents the operators in the execution plan. Operators are listed in bottom-to-top execution order.
details Provides detailed information about the operator, including the data of nodes and edges to be accessed.
columns Represents the columns processed by the operator during execution. This column is displayed only when FORMAT = VERBOSE is specified.

gql_query

The GQL query whose execution plan is to be displayed.

Examples

The following examples are based on the example dataset.

Specify DEFAULT to view the optimized execution plan

The following example explicitly specifies FORMAT = "DEFAULT", which is equivalent to omitting FORMAT = "DEFAULT" since DEFAULT is the DEFAULT FORMAT. It returns the optimized execution plan.

View nested execution plan

EXPLAIN FORMAT = "DEFAULT" USE movie MATCH (a:Person)-[e]->(b@Movie) LIMIT 1 RETURN b

Return:

• Limit
│ details: Offset:0, Limit:1, Partial:false, Approx
│          :false
│
└── • HashJoin(Inner)(DF:/_id)
    │ details:  Project: b, element_id(b)=_right_node_i
    │          d(e)
    │
    ├── • [S]NodesScan
    │     details: TableScan: {Movie: {name, id}}, b{}
    │
    └── • [S]Filter
        │ details: in(node_type_id(_left_node_id(e)), [1024
        │          ,1025,1026])
        │
        └── • [S]EdgesScan
            details: direction: outgoing: true, incoming: fal
                    se, undirected0: false, undirected1: fal
                    se, TableScan: {Watch: {}}, TableScan: {
                    Direct: {}}, TableScan: {Act: {}}, e{}

Execution Plan: [Px] means pipeline-x and [S] means storage side.

Elapsed Time:
parse time    : 624 us
build time    : 951 us
optimize time : 929 us
serialize time: 41 us
total time    : 3070 us

Each level of the nested view corresponds to an operator, and the indentation between levels indicates the dependency between operators. For example, the Limit operator depends on HashJoin.

View table execution plan

When using the ngql CLI, you can use the :planstyle command to display the execution plan in the table style. For more information, see Execution plan style setting in the ngql documentation.

:planstyle table
EXPLAIN FORMAT = "DEFAULT" USE movie MATCH (a:Person)-[e]->(b@Movie) LIMIT 1 RETURN b

Return:

+----------------------------+------------------------------------------------------------------------------------------------------+
| plan                       | details                                                                                              |
+----------------------------+------------------------------------------------------------------------------------------------------+
| Limit                      | Offset:0, Limit:1, Partial:false, Approx:false                                                       |
| └─HashJoin(Inner)(DF:/_id) |  Project: b, element_id(b)=_right_node_id(e)                                                         |
|   ├─[S]NodesScan           | TableScan: {Movie: {name, id}}, b{}                                                                  |
|   └─[S]Filter              | in(node_type_id(_left_node_id(e)), [1024,1025,1026])                                                 |
|     └─[S]EdgesScan         | direction: outgoing: true, incoming: false, undirected0: false, undirected1: false, TableScan: {W... |
+----------------------------+------------------------------------------------------------------------------------------------------+
Execution Plan: [Px] means pipeline-x and [S] means storage side.

Elapsed Time:
parse time    : 269 us
build time    : 314 us
optimize time : 350 us
serialize time: 61 us
total time    : 1272 us

Each row in the table corresponds to an operator. The indentation in the plan column indicates the dependency between operators. For example, the Limit operator depends on HashJoin.

View box execution plan

When using the ngql CLI, you can use the :planstyle command to display the execution plan in the box style. For more information, see Execution plan style setting in the ngql documentation.

:planstyle box
EXPLAIN FORMAT = "DEFAULT" USE movie MATCH (a:Person)-[e]->(b@Movie) LIMIT 1 RETURN b

Return:

┌──────────────────────────────────────┐
│                Limit                 │
│   --------------------------------   │
│   Offset:0, Limit:1, Partial:false   │
│            , Approx:false            │
└──────────────────┬───────────────────┘
┌──────────────────┴───────────────────┐
│       HashJoin(Inner)(DF:/_id)       │
│   --------------------------------   │
│    Project: b, element_id(b)=_righ   │────────────────────┐
│             t_node_id(e)             │                    │
└──────────────────┬───────────────────┘                    │
┌──────────────────┴───────────────────┐ ┌──────────────────┴───────────────────┐
│             [S]NodesScan             │ │              [S]Filter               │
│   --------------------------------   │ │   --------------------------------   │
│   TableScan: {Movie: {name, id}},    │ │   in(node_type_id(_left_node_id(e)   │
│                 b{}                  │ │         ), [1024,1025,1026])         │
└──────────────────────────────────────┘ └──────────────────┬───────────────────┘
                                        ┌──────────────────┴───────────────────┐
                                        │             [S]EdgesScan             │
                                        │   --------------------------------   │
                                        │   direction: outgoing: true, incom   │
                                        │   ing: false, undirected0: false,    │
                                        │   undirected1: false, TableScan: {   │
                                        │   Direct: {}}, TableScan: {Watch:    │
                                        │    {}}, TableScan: {Act: {}}, e{}    │
                                        └──────────────────────────────────────┘

Execution Plan: [Px] means pipeline-x and [S] means storage side.

Elapsed Time:
parse time    : 820 us
build time    : 1154 us
optimize time : 1116 us
serialize time: 92 us
total time    : 3963 us

Each box represents an operator, and the connections between boxes indicate that the upper operator depends on the output of the lower operator. For example, the Limit operator depends on HashJoin.

Specify RAW to view the unoptimized logical plan

EXPLAIN RAW USE movie MATCH (a:Person)-[e]->(b@Movie) LIMIT 1 RETURN b

The above example specifies the RAW option and returns the unoptimized logical plan for the query MATCH (a:Person)-[e]->(b@Movie) LIMIT 1 RETURN b.

Return:

• Project
│ details: b
│
└── • Limit
    │ details: Offset:0, Limit:1, Partial:false, Approx
    │          :false
    │
    └── • LogicalJoin(Cross)
        │
        ├── • Values(UNIT)
        │     details: UNIT
        │
        └── • Project
            │ details: b, e, a
            │
            └── • LogicalJoin(Inner)
                │ details: element_id(b)=_right_node_id(e)
                │
                ├── • GetNodes
                │     details: Movie,, b{_GraphId,_id,id,name}
                │
                └── • LogicalJoin(Inner)
                    │ details: _left_node_id(e)=element_id(a)
                    │
                    ├── • [S]GetEdges
                    │     details: Act,Direct,Watch,, e{rate}
                    │
                    └── • GetNodes
                          details: Actor,Director,User,, a{_GraphId,_id,bir
                                   thDate,id,name}

Execution Plan: [Px] means pipeline-x and [S] means storage side.

Elapsed Time:
 parse time    : 173 us
 build time    : 0 us
 optimize time : 0 us
 serialize time: 25 us
 total time    : 582 us

Each level of the nested view corresponds to an operator, and the indentation between levels indicates the dependency between operators.

Specify DECORRELATED to view the logical plan after decorrelation

EXPLAIN DECORRELATED returns the execution plan after subquery decorrelation, showing the logical plan after correlated subqueries have been rewritten into equivalent non-nested forms.

'''
EXPLAIN DECORRELATED 
USE movie MATCH (a@Actor)
WHERE EXISTS {USE movie MATCH (v@Actor) RETURN v}
RETURN a.name
LIMIT 5
'''  

Return:

• Project
│ details: a.name
│
└── • Limit
    │ details: Offset:0, Limit:5, Partial:false, Approx
    │          :false
    │
    └── • LogicalJoin(Cross)
        │
        ├── • GetNodes
        │     details: Actor,, a{_GraphId,_id,birthDate,id,name
        │              }
        │
        └── • Project
            │ details: CASE WHEN @anon2 = 1 THEN true ELSE fals
            │          e END AS @anon2
            │
            └── • Filter
                │ details: CASE WHEN @anon2 = 1 THEN true ELSE fals
                │          e END
                │
                └── • Aggregate
                    │ details: Partial: false, Single: true, Memory: tr
                    │          ue, AGGREGATE count(true) AS @anon2
                    │
                    └── • Project
                        │ details: v
                        │
                        └── • Limit
                            │ details: Offset:0, Limit:1, Partial:false, Approx
                            │          :false
                            │
                            └── • LogicalJoin(Cross)
                                │
                                ├── • Values(UNIT)
                                │     details: UNIT
                                │
                                └── • GetNodes
                                      details: Actor,, v{_GraphId,_id,birthDate,id,name
                                               }

Execution Plan: [Px] means pipeline-x and [S] means storage side.

Elapsed Time:
 parse time    : 497 us
 build time    : 572 us
 optimize time : 614 us
 serialize time: 55 us
 total time    : 2147 us

Note

The DECORRELATED stage has a significant effect primarily on queries that contain correlated subqueries. For queries without subqueries, the output of the DECORRELATED stage may be identical or very similar to that of the RAW stage.

Specify CBO to view the logical plan after CBO reordering

EXPLAIN CBO returns the execution plan after the Cost-Based Optimizer (CBO) stage, showing the logical plan after that stage finishes and before final plan generation. If you want to observe how the optimizer changes the expansion order of MATCH patterns, enable enable_reorder explicitly.

'''
EXPLAIN CBO /*+ SET_VAR(enable_reorder=true) */
USE movie MATCH (a:Person)-[e]->(b@Movie) LIMIT 1 RETURN b
'''

Return:

• Limit
│ details: Offset:0, Limit:1, Partial:false, Approx
│          :false
│
└── • Project
    │ details: b
    │
    └── • Extend
        │ details: Emit: from=true, edge=true, to=true, Dir
        │          ection: outgoing: false, incoming: true,
        │           undirected0: false, undirected1: false,
        │           ToNode: a, EdgeField: e, FromNode: b, M
        │          ode: All, Graph: movie(1024)
        │
        └── • GetNodes
              details: Movie,, b{_GraphId,_id,id,name}

Execution Plan: [Px] means pipeline-x and [S] means storage side.

Elapsed Time:
 parse time    : 860 us
 build time    : 812 us
 optimize time : 708 us
 serialize time: 38 us
 total time    : 2987 us

Note

Not all queries go through the CBO stage. EXPLAIN CBO produces output that differs from the DEFAULT plan only for queries involving eligible graph patterns or join reordering scenarios. For queries that do not involve CBO optimization, the output may be the same as the DECORRELATED stage.

enable_reorder defaults to false. If you want to compare a reordered MATCH plan with the default textual-order plan, enable it explicitly at the query, session, or global level.

If the plan is unexpected or you want to understand why CBO chose a specific plan, enable enable_cbo_trace=true and inspect the nebula-graphd.INFO log.

enable_cbo_trace is a session-level configuration option and its default value is false.

Example:

'''
EXPLAIN /*+ set_var(enable_cbo_trace=true) */
USE movie MATCH (a:Person)-[e]->(b@Movie) LIMIT 1 RETURN b
'''

If the CBO optimizer chose a specific plan, you can find the following information in the nebula-graphd.INFO log:

I20260326 14:45:11.947098 1439136 CboTrace.cpp:31] [CBO] ===== Trace Summary =====
Pattern: nodes=2, edges=1, predicates=0
Leaf Plans
...
[CBO] ===== End Trace Summary =====

Specify FORMAT = "VERBOSE" to view the detailed optimized execution plan

EXPLAIN FORMAT = "VERBOSE" USE movie MATCH (a:Person)-[e]->(b@Movie) LIMIT 1 RETURN b

The above example uses the VERBOSE FORMAT by specifying FORMAT = "VERBOSE" to display the optimized execution plan for the query USE movie MATCH (a:Person)-[e]->(b@Movie) LIMIT 1 RETURN b.

Return:

• Limit
│ details: Offset:0, Limit:1, Partial:false, Approx
│          :false
│ columns: [b]
│
└── • HashJoin(Inner)(DF:/_id)
    │ details:  Project: b:NodeRef<(Movie)>, element_id
    │          (b::NodeRef<(Movie)>)::INT64=(_right_nod
    │          e_id(e::EdgeRef<(Actor)-[Act]->(Movie),
    │          (Director)-[Direct]->(Movie), (User)-[Wa
    │          tch]->(Movie)>))::INT64
    │ columns: [b]
    │
    ├── • [S]NodesScan
    │     details: TableScan: {Movie: {name, id}}, b{}
    │     columns: [b]
    │
    └── • [S]Filter
        │ details: (in((node_type_id((_left_node_id(e::Edge
        │          Ref<(Actor)-[Act]->(Movie), (Director)-[
        │          Direct]->(Movie), (User)-[Watch]->(Movie
        │          )>))::INT64))::INT16, [1024,1025,1026]::
        │          LIST<INT16>))::BOOL
        │ columns: [e]
        │
        └── • [S]EdgesScan
              details: direction: outgoing: true, incoming: fal
                       se, undirected0: false, undirected1: fal
                       se, TableScan: {Watch: {}}, TableScan: {
                       Direct: {}}, TableScan: {Act: {}}, e{}
              columns: [e]

Execution Plan: [Px] means pipeline-x and [S] means storage side.

Elapsed Time:
 parse time    : 234 us
 build time    : 258 us
 optimize time : 387 us
 serialize time: 39 us
 total time    : 1184 us

Each level of the nested view corresponds to an operator, and the indentation between levels indicates the dependency between operators. For example, the Limit operator depends on HashJoin.

Specify RAW and FORMAT = "VERBOSE" to view the detailed unoptimized logical plan

EXPLAIN RAW FORMAT = "VERBOSE" USE movie MATCH (a:Person)-[e]->(b@Movie) LIMIT 1 RETURN b

Return:

• Project
│ details: b::NodeRef<(Movie)>
│ columns: [b]
│
└── • Limit
    │ details: Offset:0, Limit:1, Partial:false, Approx
    │          :false
    │ columns: [b, e, a]
    │
    └── • LogicalJoin(Cross)
        │ columns: [b, e, a]
        │
        ├── • Values(UNIT)
        │     details: UNIT
        │     columns: []
        │
        └── • Project
            │ details: b::NodeRef<(Movie)>, e::EdgeRef<(Actor)-
            │          [Act]->(Movie), (Director)-[Direct]->(Mo
            │          vie), (User)-[Watch]->(Movie)>, a::NodeR
            │          ef<(Actor), (Director), (User)>
            │ columns: [b, e, a]
            │
            └── • LogicalJoin(Inner)
                │ details: element_id(b::NodeRef<(Movie)>)::INT64=(
                │          _right_node_id(e::EdgeRef<(Actor)-[Act]-
                │          >(Movie), (Director)-[Direct]->(Movie),
                │          (User)-[Watch]->(Movie)>))::INT64
                │ columns: [b, e, a]
                │
                ├── • GetNodes
                │     details: Movie,, b{_GraphId,_id,id,name}
                │     columns: [b]
                │
                └── • LogicalJoin(Inner)
                    │ details: (_left_node_id(e::EdgeRef<(Actor)-[Act]-
                    │          >(Movie), (Director)-[Direct]->(Movie),
                    │          (User)-[Watch]->(Movie)>))::INT64=(eleme
                    │          nt_id(a::NodeRef<(Actor), (Director), (U
                    │          ser)>))::INT64
                    │ columns: [e, a]
                    │
                    ├── • [S]GetEdges
                    │     details: Act,Direct,Watch,, e{rate}
                    │     columns: [e]
                    │
                    └── • GetNodes
                          details: Actor,Director,User,, a{_GraphId,_id,bir
                                   thDate,id,name}
                          columns: [a]

Execution Plan: [Px] means pipeline-x and [S] means storage side.

Elapsed Time:
 parse time    : 281 us
 build time    : 0 us
 optimize time : 0 us
 serialize time: 39 us
 total time    : 775 us

Each level of the nested view corresponds to an operator, and the indentation between levels indicates the dependency between operators.