Article DetailsDetailsHelp DetailsIntroduction Query plan caching is governed by three configuration settings, defined in the neo4j.conf file. The three settings that govern whether a Cypher statement is planned/replanned are: server.memory.query_cache.per_db_cache_num_entries (being dbms.query_cache_size in v4.4)dbms.cypher.min_replan_intervaldbms.cypher.statistics_divergence_threshold Refer to "Configure the replanning of execution plans" for more details on how each setting works, and additional options such as manually clearing the plan cache, and automatic or manual replanning. As a note, the statistics used by dbms.cypher.statistics_divergence_threshold depend on which statistics the planner accessed during planning, which can be multiple for a single query. These statistics could be total node count, node count with a given label, relationship count, index size, or number of unique values in an index and so on. While we aim to give as clear a picture of how the query plan cache works, there are some additional complexities in relation to the statistics that we cannot cover here. Also, relative to the query cache, you should be aware of the following: If there are any schema changes, such as the addition/removal of indexes or constraints, all query plans are immediately invalidatedCypher supports querying with parameters. This means developers don’t have to resort to string building to create a queryIn addition, it makes caching execution plans much easier for Cypher More details about parameters are described here. Understanding the Query Plan Cache When a Cypher statement is first submitted, Neo4j will attempt to determine if the query is in the plan cache before planning it. By default, Neo4j will keep 1000 query plans in the cache based on the neo4j.conf parameter server.memory.query_cache.per_db_cache_num_entries. This represents 2 query plan caches. The String Cache When Cypher is initially submitted, the Cypher statement will have a hash computed on the string as-is. Using this resultant hash value we will attempt to determine if the statement already exists in the plan cache and if it does, then re-planning may not be necessary. It is important to understand that hashes are computed as it, and statements that are logically the same but differing in case will produce different hashes. The following 2 statements, though semantically equivalent, will produce a different hash and a replan may be necessary: match (n) return count(n); MATCH (n) return COUNT(n); Additionally, statements that are logically identical but differ only in whitespace/carriage returns will produce different hash values. The following 3 statements will produce a different hash, and a replan may be necessary: Match (n) return count(n); Match (n) return COUNT(n); Match (n) return COUNT(n); Cypher statements prefaced by PROFILE/EXPLAIN will have their PROFILE/EXPLAIN removed before the statement is hashed. The following 2 statements will hash to the same value: Match (n) return COUNT(n); PROFILE Match (n) return COUNT(n); If the Cypher statement hash value is not found in the string cache, Neo4j will then attempt to determine if it is in the 2nd cache, called The Abstract Syntax Tree Cache (AST). The AST Cache The Neo4j compiler parses the query from a string to an abstract syntax tree (AST). This is an object representation of the query. The optimizer then normalizes the statement to make planning easier. For example: Match (n:Person {id:101}) return n; will be normalized to: Match (n:Person) where n.id= $param1 return n; {param1: 101} In the above example, the normalisation has moved the predicate {id:101} from the MATCH pattern to the WHERE clause and has a parameterized 101 value into a parameter, e.g. n.id= $param1. The usage of parameters is further detailed here. The AST cache doesn’t store information such as white space and keyword casing, and since it's parameterized, literal values can change while still producing the same AST. This second query cache is keyed on this normalized AST. i.e., these queries will reuse the same query plan. Match (n:Person) where n.id=101 return n; Match (n:Person {id:101}) return n; Match ( n:Person { id : 101 } ) return n; If the statement is found in either the string or AST cache, the query may still be subject to being replanned based on the configuration of: dbms.cypher.min_replan_interval with below parameter description: The minimum time between possible Cypher query replanning events. After this time, the graph statistics will be evaluated, and if they have changed by more than the value set by dbms.cypher.statistics_divergence_threshold, the query will be replanned. If the statistics have not changed sufficiently, the same interval will need to pass before the statistics will be evaluated again. Each time they are evaluated, the divergence threshold will be reduced slightly until it reaches 10% after 7h, so that even moderately changing databases will see query replanning after a sufficiently long time interval. dbms.cypher.statistics_divergence_threshold with below parameter description: The threshold for statistics above which a plan is considered stale. If any of the underlying statistics used to create the plan have changed more than this value, the plan will be considered stale and will be replanned. Change is calculated as abs(a-b)/max(a,b). This means that a value of 0.75 requires the database to quadruple in size before query replanning. A value of 0 means that the query will be replanned as soon as there is any change in statistics and the replan interval has elapsed. This interval is defined by dbms.cypher.min_replan_interval and defaults to 10s. After this interval, the divergence threshold will slowly start to decline, reaching 10% after about 7h. This will ensure that long running databases will still get query replanning on even modest changes, while not replanning frequently unless the changes are very large. For example, running: // remove all :Person nodes Match (n:Person) detach delete n; // create 10 :Person nodes foreach (x in range (1,10) | Create (n:Person {id:x})); // list the 10 :Person nodes created Match (n:Person) return n.id order by n.id desc; // create 8 new :Person nodes foreach (x in range (11,18) | Create (n:Person {id:x})); // list the 18 :Person nodes Match (n:Person) return n.id order by n.id desc; The MATCH that appears two times: Match (n:Person) return n.id order by n.id desc; Would be planned for each case, specifically the 2nd instance, although both have the same hash value. The statistics on :Person changed from 10 nodes to 18 nodes and thus exceeded the 75% change threshold. If an existing plan needs to be replanned as a result of the above 2 parameters, then the debug.log will log it as such: 2026-03-06 00:21:01.321+1100 INFO [o.n.c.i.ExecutionEngine] [neo4j/xxxxxxxx] Discarded stale query from the query cache after 20303 seconds. Additionally, it should be noted that when a query plan is removed from the cache to make room for a new plan, a least frequently used (LFU) algorithm is used. So if the first query added to the plan cache is run every 1 second, and the 2nd query added to the query plan cache is run every 2 minutes, then when we need to remove a query plan from the cache to make room for a new query, we will remove the second query before the first, since the first is more frequently called upon. Finally, it should be noted that any schema changes, for example, index/constraint creation/removal will flush the entire query plan cache. InformationTitleUnderstanding the Query Plan CacheArticle Created Date3/15/2024, 10:56 PMURL Name115013558147-Understanding-the-Query-Plan-CacheLast Modified Date3/9/2026, 12:40 PM