When you have completed your initial implementation, it is common to progress and look at some performance tuning to your workload.
A number of options are to be considered for operating your application on Neo4j AuraDB, and the points below summarize the essential approach to achieving the best outcome.
1 - Review queries and model
Capture the Cypher queries
You should review and list all the queries, and the best starting point is to have a good understanding of the sequence and frequency of the Cypher queries submitted.
If your queries are somehow generated by a framework, it is essential you log the Cypher as it is often the starting point to make changes and review what works best.
Profile the Cypher
PROFILE plan (see here for explanations): by just prepending the keywords to your query to get the associated plan.
Note that when using
PROFILE you may need to run it multiple times to get the optimal value: the first time the query runs, it gets a full cycle of evaluation, planning, and interpreting before making its way into the query cache. Once in the cache, the subsequent execution time will improve. Also, always use parameters instead of literal values to benefit from the cache.
See this detailed guide for the steps on How to capture the execution plans.
To best interpret the output, you should get familiar with the terms used: https://neo4j.com/docs/cypher-manual/current/execution-plans/operator-summary/
The different stages involved in an execution plan need to be understood: https://neo4j.com/docs/cypher-manual/current/execution-plans/
2 - Index what matters
Defining constraints and indexes is essential to achieve the best performance as your data volume grows and for performing optimized queries.
The runtime engine will need to evaluate the cost associated with a query and to get the best estimations, it will rely on having indexes.
You will likely have identified if/what index is missing from the execution plan.
In other circumstances, you may think an index isn't available or possible, but at times it may also make sense to reconsider the model and create an intermediate node or another relationship type just to leverage an index.
If you control well the use of the index and want to fine-tune their usage in your query, you may be able to leverage them with
3 - Review metrics during the workload and find the right instance size
With Aura, you can keep an eye on some key metrics to understand the resource constraints your instance may be experiencing.
- CPU Usage: A high load is generally expected when you have a computational heavy workload. Another common reason if the query isn't complex may be that the CPU is consumed with memory management.
- Out of Memory errors: Queries that handle or process too much data may lead to OOM. There are many built-in protections, but not everything can be constrained.
- Heap usage (Enterprise only): This helps you understand if the heap is exhausting vs the off-heap memory.
- Page cache Evictions: high values indicate that the memory is used intensely.
- Garbage Collection Time: this will indicate how hard the garbage collector works to allocate/free memory and allow the execution of your queries. When this gets high, you should also see a higher CPU load.
At this stage, if you see the key metrics being too high, you may want to reconsider the instance sizing. A resize operation doesn't cause any downtime, and you would only pay for what you use.
Note that you should always size your instance against your workload activity peaks.
4 - Consider concurrency
Sometimes individual queries on their own are optimized and running fine, but the sheer volume and concurrency of operations can overwhelm your AuraDB instance.
To review what's running at any given time (this makes particular sense if you have a long-running query), you can use these statements and list what is running.
SHOW TRANSACTIONS see the full details in the documentation
CALL dbms.listQueries() see the full details in the documentation
5 - Runtime engine / Cypher version
The execution plan should show you the runtime that is selected for the execution of your query. Usually, the planner makes the right decision, but it may be worth checking at times if the other runtimes do not perform better.
To invoke the use of a given runtime forcibly, simply prepend your cypher statement with:
- For pipelined runtime
- For slotted runtime:
- For interpreted runtime:
If you have a Cypher pattern that isn't performing without error or as well as in another prior version, you could also control the Cypher version used to interpret it using these options
6 - Network and the cost of the round-trip.
With Aura, it is essential you consider the best cloud region as the physical distance is a direct factor in the achievable network latency.
This post explains the constraints well and provides good measurements.
When some event causes any network disruption between your application and Aura, you would be affected by round-trip network latency to re-submit a query. With Aura, this is particularly important because you will need to be using transaction functions:
- Python: https://neo4j.com/docs/python-manual/current/session-api/#python-driver-simple-transaction-fn
- .NET: https://neo4j.com/docs/dotnet-manual/current/session-api/configuration/#dotnet-driver-simple-transaction-fn
- Go: https://neo4j.com/docs/go-manual/current/session-api/#go-driver-simple-transaction-fn
- Java: https://neo4j.com/docs/java-manual/current/session-api/#java-driver-simple-transaction-fn