Explore the query execution summary

After all results coming from a query have been processed, the server ends the transaction by returning a summary of execution. It comes as a ResultSummary object, and it contains information among which:

  • Query counters — What changes the query triggered on the server

  • Query execution plan — How the database would execute (or executed) the query

  • Notifications — Extra information raised by the server while running the query

  • Timing information and query request summary

Retrieve the execution summary

When running queries with ExecuteQuery(), the execution summary is part of the default return object, under the Summary key.

result, _ := neo4j.ExecuteQuery(ctx, driver, `
    UNWIND ["Alice", "Bob"] AS name
    MERGE (p:Person {name: name})
    `, nil,
    neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("neo4j"))
summary := result.Summary

If you are using transaction functions, you can retrieve the query execution summary with the method Result.Consume(). Notice that once you ask for the execution summary, the result stream is exhausted. This means that any record which has not yet been processed is discarded.

session := driver.NewSession(ctx, neo4j.SessionConfig{DatabaseName: "neo4j"})
defer session.Close(ctx)
summary, _ := session.ExecuteWrite(ctx,
    func(tx neo4j.ManagedTransaction) (any, error) {
        result, err := tx.Run(ctx, `
            UNWIND ["Alice", "Bob"] AS name
            MERGE (p:Person {name: name})
            `, nil)
        summary, _ := result.Consume(ctx)
        return summary, err
    })

Query counters

The method ResultSummary.Counters() returns counters for the operations that a query triggered (as a Counters object).

Insert some data and display the query counters
result, _ := neo4j.ExecuteQuery(ctx, driver, `
    MERGE (p:Person {name: $name})
    MERGE (p)-[:KNOWS]->(:Person {name: $friend})
    `, map[string]any{
        "name": "Mark",
        "friend": "Bob",
    }, neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("neo4j"))
summary := result.Summary
counters := summary.Counters()
fmt.Println("Nodes created:", counters.NodesCreated())
fmt.Println("Labels added:", counters.LabelsAdded())
fmt.Println("Properties set:", counters.PropertiesSet())
fmt.Println("Relationships created:", counters.RelationshipsCreated())

// Nodes created: 2
// Labels added: 2
// Properties set: 2
// Relationships created: 1

There are two additional boolean methods which act as meta-counters:

  • .ContainsUpdates() — whether the query triggered any write operation on the database on which it ran

  • .ContainsSystemUpdates() — whether the query updated the system database

Query execution plan

If you prefix a query with EXPLAIN, the server will return the plan it would use to run the query, but will not actually run it. You can retrieve the plan by calling ResultSummary.Plan(), which contains the list of Cypher operators that would be used to retrieve the result set. You may use this information to locate potential bottlenecks or room for performance improvements (for example through the creation of indexes).

result, _ := neo4j.ExecuteQuery(ctx, driver,
    "EXPLAIN MATCH (p {name: $name}) RETURN p",
    map[string]any{
        "name": "Alice",
    },
    neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("neo4j"))
fmt.Println(result.Summary.Plan().Arguments()["string-representation"])
/*
Planner COST
Runtime PIPELINED
Runtime version 5.0
Batch size 128

+-----------------+----------------+----------------+---------------------+
| Operator        | Details        | Estimated Rows | Pipeline            |
+-----------------+----------------+----------------+---------------------+
| +ProduceResults | p              |              1 |                     |
| |               +----------------+----------------+                     |
| +Filter         | p.name = $name |              1 |                     |
| |               +----------------+----------------+                     |
| +AllNodesScan   | p              |             10 | Fused in Pipeline 0 |
+-----------------+----------------+----------------+---------------------+

Total database accesses: ?
*/

If you instead prefix a query with the keyword PROFILE, the server will return the execution plan it has used to run the query, together with profiler statistics. This includes the list of operators that were used and additional profiling information about each intermediate step. You can access the plan by calling ResultSummary.Profile(). Notice that the query is also run, so the result object also contains any result records.

result, _ := neo4j.ExecuteQuery(ctx, driver,
    "PROFILE MATCH (p {name: $name}) RETURN p",
    map[string]any{
        "name": "Alice",
    },
    neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("neo4j"))
fmt.Println(result.Summary.Profile().Arguments()["string-representation"])
/*
Planner COST
Runtime PIPELINED
Runtime version 5.0
Batch size 128

+-----------------+----------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+
| Operator        | Details        | Estimated Rows | Rows | DB Hits | Memory (Bytes) | Page Cache Hits/Misses | Time (ms) | Pipeline            |
+-----------------+----------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+
| +ProduceResults | p              |              1 |    1 |       3 |                |                        |           |                     |
| |               +----------------+----------------+------+---------+----------------+                        |           |                     |
| +Filter         | p.name = $name |              1 |    1 |       4 |                |                        |           |                     |
| |               +----------------+----------------+------+---------+----------------+                        |           |                     |
| +AllNodesScan   | p              |             10 |    4 |       5 |            120 |                 9160/0 |   108.923 | Fused in Pipeline 0 |
+-----------------+----------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+

Total database accesses: 12, total allocated memory: 184
*/

For more information and examples, see Basic query tuning.

Notifications

After executing a query, the server can return notifications alongside the query result. Notifications contain recommendations for performance improvements, warnings about the usage of deprecated features, and other hints about sub-optimal usage of Neo4j.

For driver version >= 5.25 and server version >= 5.23, two forms of notifications are available (Neo4j status codes and GQL status codes). For earlier versions, only Neo4j status codes are available.
GQL status codes are planned to supersede Neo4j status codes.
Example 1. An unbounded shortest path raises a performance notification

The method Summary.Notifications() returns a list of Notification objects.

result, _ := neo4j.ExecuteQuery(ctx, driver, `
    MATCH p=shortestPath((:Person {name: 'Alice'})-[*]->(:Person {name: 'Bob'}))
    RETURN p
    `, nil,
    neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("neo4j"))

for _, notification := range result.Summary.Notifications() {
    fmt.Println("Code:", notification.Code())
    fmt.Println("Title:", notification.Title())
    fmt.Println("Description:", notification.Description())
    fmt.Println("Severity:", notification.SeverityLevel())
    fmt.Println("Category:", notification.Category(), "\n")
}
/*
Code: Neo.ClientNotification.Statement.UnboundedVariableLengthPattern
Title: The provided pattern is unbounded, consider adding an upper limit to the number of node hops.
Description: Using shortest path with an unbounded pattern will likely result in long execution times. It is recommended to use an upper limit to the number of node hops in your pattern.
Severity: INFORMATION
Category: PERFORMANCE
*/

With version >= 5.25, the method Summary.GqlStatusObjects() returns a list of GqlStatusObjects. These are GQL-compliant status objects.

Some (but not all) GqlStatusObjects are notifications, whereas some report an outcome status: 00000 for "success", 02000 for "no data", and 00001 for "omitted result". Summary.GqlStatusObjects() always contains at least one entry, containing the outcome status.

result, _ := neo4j.ExecuteQuery(ctx, driver, `
    MATCH p=shortestPath((:Person {name: 'Alice'})-[*]->(:Person {name: 'Bob'}))
    RETURN p
    `, nil,
    neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("neo4j"))

for _, status := range result.Summary.GqlStatusObjects() {
    fmt.Println("GQLSTATUS:", status.GqlStatus())
    fmt.Println("Description:", status.StatusDescription())
    // Not all statuses are notifications.
    fmt.Println("Is notification:", status.IsNotification())

    // Notification and thus vendor-specific fields.
    // These fields are only meaningful for notifications.
    if status.IsNotification() == true {
        fmt.Println("Position (offset, line, column):", status.Position().Offset(), status.Position().Line(), status.Position().Column())
        fmt.Println("Classification:", status.Classification())
        fmt.Println("Unparsed classification:", status.RawClassification())

        fmt.Println("Severity:", status.Severity())
        fmt.Println("Unparsed severity:", status.RawSeverity())
    }
    // Any raw extra information provided by the server
    fmt.Println("Diagnostic record:", status.DiagnosticRecord())
    fmt.Println(strings.Repeat("=", 80))
}
/*
GQLSTATUS: 02000
Description: note: no data
Is notification: false
Diagnostic record: map[CURRENT_SCHEMA:/ OPERATION: OPERATION_CODE:0]
================================================================================
GQLSTATUS: 03N91
Description: info: unbounded variable length pattern. The provided pattern `(:Person {name: 'Alice'})-[*]->(:Person {name: 'Bob'})` is unbounded. Shortest path with an unbounded pattern may result in long execution times. Use an upper limit (e.g. `[*..5]`) on the number of node hops in your pattern.
Is notification: true
Position (offset, line, column): 26 2 26
Classification: PERFORMANCE
Unparsed classification: PERFORMANCE
Severity: INFORMATION
Unparsed severity: INFORMATION
Diagnostic record: map[CURRENT_SCHEMA:/ OPERATION: OPERATION_CODE:0 _classification:PERFORMANCE _position:map[column:26 line:2 offset:26] _severity:INFORMATION _status_parameters:map[pat:(:Person {name: 'Alice'})-[*]->(:Person {name: 'Bob'})]]
================================================================================
*/

Filter notifications

By default, the server analyses each query for all categories and severity of notifications. Starting from version 5.7, you can use the parameters NotificationsMinSeverity and/or NotificationsDisabledCategories/NotificationsDisabledClassifications to restrict the severity and/or category/classification of notifications that you are interested into. There is a slight performance gain in restricting the amount of notifications the server is allowed to raise.

The severity filter applies to both Neo4j and GQL notifications. Category and classification filters exist separately only due to the discrepancy in lexicon between GQL and Neo4j; both filters affect either form of notification though, so you should use only one of them. You can use any of those parameters either when creating a Driver instance, or when creating a session.

You can disable notifications altogether by setting the minimum severity to "OFF".

Allow only Warning notifications, but not of Hint or Generic category
// import (
//     "github.com/neo4j/neo4j-go-driver/v5/neo4j/notifications"
//     "github.com/neo4j/neo4j-go-driver/v5/neo4j/config"
// )

// At driver level
driverNot, _ := neo4j.NewDriverWithContext(
    dbUri,
    neo4j.BasicAuth(dbUser, dbPassword, ""),
    func (conf *config.Config) {
        conf.NotificationsMinSeverity = notifications.WarningLevel  // or "OFF" to disable entirely
        conf.NotificationsDisabledClassifications = notifications.DisableClassifications(notifications.Hint, notifications.Generic)  // filters categories as well
    })


// At session level
sessionNot := driver.NewSession(ctx, neo4j.SessionConfig{
    NotificationsMinSeverity: notifications.WarningLevel,  // or "OFF" to disable entirely
    NotificationsDisabledClassifications: notifications.DisableClassifications(notifications.Hint, notifications.Generic),  // filters categories as well
    DatabaseName: "neo4j",  // always provide the database name
})