agtype performance optimizations - #2510
Open
AntufeevMaksim wants to merge 5 commits into
Open
Conversation
find_agtype_value_from_container() previously used binary search for objects of any size. For small objects this was inefficient due to repeated get_agtype_offset() calls on each random-access probe and poor cache locality. Introduce a hybrid search strategy: - Use sequential linear scan with running key offset tracking via AGTE_ADVANCE_OFFSET when pair count <= AGTYPE_OBJECT_LINEAR_SEARCH_THRESHOLD. This eliminates backward walks through the children array during key comparisons. - Leverage sorted key order for early termination when the current key exceeds the search target. - Retain binary search for larger objects where O(log N) dominates. The threshold is set to 8 pairs. Key length is now derived from the difference between consecutive offsets instead of calling get_agtype_length(), which is safe for the current storage format and further reduces CPU overhead.
- Add a non-variadic fast path for agtype_access_operator with direct argument passing, avoiding variadic array construction and extraction. - Update Cypher expression transformation to build non-variadic agtype_access_operator calls for map and property access. - Update regression tests and expected query plans for the new non-variadic function calls.
…cked by dynamically allocated chunks. Small traversals now complete without any heap allocations, while deeper traversals transparently fall back to chunk allocation.
…_from_container_no_copy in execute_map_access_operator and agtype arithmetic operations functions.
Add a per-function type cache backed by a hash table to avoid repeated type categorization for previously seen OIDs. Add a fast path for non-variadic calls that processes arguments directly from FunctionCallInfo, avoiding extract_variadic_args() and its temporary allocations in the common case. Add a temporary MemoryContext in agtype_build_map() for agtype_value allocations.
AntufeevMaksim
marked this pull request as ready for review
August 12, 2026 15:13
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
agtype performance optimizations
replace binary search in find_agtype_value_from_container() with linear for small objects.
Motivation / Problem:
In the current implementation of find_agtype_value_from_container, searching for a key in an agtype object always uses binary search. For each probe during the binary search, we must call get_agtype_offset(). This function walks backwards through the container's entry array to find the nearest preceding element with a stored absolute offset (AGTE_HAS_OFF), summing lengths along the way.
Solution
Linear search is now used for objects with pair counts <= AGTYPE_OBJECT_LINEAR_SEARCH_THRESHOLD (default: 8).
During linear search, key offsets are calculated incrementally by adding the size of previous elements via AGTE_ADVANCE_OFFSET, completely eliminating backward walks and repeated get_agtype_offset() calls.
Result: TPS increase of up to 5%
CreateKnowsMutation [before]

CreateKnowsMutation [after]

Optimize agtype_access_operator by avoiding variadic array construction
Motivation / Problem:
extract_variadic_args() took around 50% of agtype_access_operator execution time.
ExecEvalArrayExpr(), which is used to construct the variadic array, also accounted
for a significant part of transaction execution time.
Solution
To avoid these overheads, we replace the original
with
This allows the function to be called in a non-variadic way. In
particular, Cypher property and map access expressions now pass their
arguments directly instead of constructing an agtype array.
Note
There is an important difference between VARIADIC agtype[] and
VARIADIC "any". With VARIADIC agtype[], PostgreSQL automatically packs
arguments passed using the regular function-call syntax into an
agtype[] array. With VARIADIC "any", this automatic conversion to an
agtype array is no longer available for the non-variadic call path.
Therefore, agtype_access_operator now performs the required type
conversion itself for non-agtype arguments.
The original behavior is preserved: arguments are still converted to
agtype before being processed, and the variadic path remains available
for calls where the number of arguments is not known in advance.
Regression tests were also updated because the function signature and
the generated expression trees have changed. Expected EXPLAIN output
now contains non-variadic agtype_access_operator calls instead of
VARIADIC ARRAY expressions.
Result: TPS increase of up to 30%
latency reduction of up to 23%
CreateForumWithTagsMutationWhere [before]

CreateForumWithTagsMutationWhere [after]

Replace per-iterator heap allocation with an inline iterator stack backed by dynamically allocated chunks.
Motivation / Problem:
agtype_iterator objects were previously allocated individually with palloc() during traversal.
Solution
The first AGI_INLINE_ITERS (default 8) iterators are stored directly in agtype_traversal using stack memory
When the traversal depth exceeds the inline capacity, additional iterators are stored in dynamically allocated chunks.
The heap-based part of the stack is organized as a linked list of chunks with exponentially increasing capacity. Once a chunk is allocated, it is kept for the lifetime of the traversal and reused when the traversal reaches the same depth again. This avoids repeated palloc()/pfree() calls when moving between nested containers.
Result: TPS increase of up to 18%
latency reduction of up to 15%
MessageAutorQuery [before]

MessageAutorQuery [after]

agtype_build_map()
Motivation / Problem:
agtype_categorize_type() accounted for around 31% of agtype_build_map() execution time. Type categorization is based on the argument type OID, which is obtained from the function expression stored in FmgrInfo. The expression tree associated with a particular FmgrInfo does not change between its executions, so the resulting type metadata can be safely cached for subsequent calls using the same FmgrInfo.
extract_variadic_agrs() accounted for around 13% of agtype_build_map() execution time.
pfree_agtype_value() accounted for around 4% of agtype_build_map() execution time.
Solution
Add a per-function type cache backed by a hash table. The cache stores the category and output function for each encountered type OID, avoiding repeated calls to agtype_categorize_type() for the same types.
The change also adds a fast path for non-variadic calls. Previously, extract_variadic_args() was used for both ordinary and VARIADIC calls. For a non-variadic call, the arguments are already available in FunctionCallInfo, so allocating and populating separate args, nulls, and types arrays is unnecessary.
Add a temporary memory context which give us a possibility to remove pfree_agtype_value() calls.
Result: TPS increase of up to 35%
latency reduction of up to 25%
CreateForumWithTagsMutation [before]

CreateForumWithTagsMutation [after]

Replace get_ith_agtype_value_from_container with get_ith_agtype_value_from_container_no_copy
Motivation / Problem:
get_ith_agtype_value_from_container performs a copy of strings, numerics, vertices, edges, and paths. However, in some functions like agtype_access_operator and agtype_add, we don't change the agtype_value received from get_ith_agtype_value_from_container, which results in unnecessary copy overhead.
Solution
Create a get_ith_agtype_value_from_container_no_copy function that uses fill_agtype_value_no_copy, and replace calls to get_ith_agtype_value_from_container with get_ith_agtype_value_from_container_no_copy where possible.
Result: TPS increase of up to 4%
latency reduction of up to 4%
CreateForumWithTagsMutationWhere [before]

CreateForumWithTagsMutationWhere [after]

final version
(Build including all changes)
Result: TPS increase of up to 51%
latency reduction of up to 34%
Benchmarking
Benchmarks were conducted on a laptop with 4 performance and 4 power efficient cores. 16 CB RAM.
How to run tests:
Graph creation and population:
The sf parameter here defines the scale factor of the generated graph.
psql -d your_database -f generate_graph.sql -v sf=1
Creating wrapper functions for the workload:
This script sets up the environment and creates functions with the correct parameter mapping for Apache AGE.
psql -d your_database -f setup_AddForumMemberMutation.sql
Running the load test:
Running 16 clients for 30 seconds.
pgbench -d your_database -f workload_AddForumMemberMutation.sql -D sf=1 -c 16 -j 12 -T 30 -n
Test scenarious:
generate_graph.sql
setup_AddForumMemberMutation.sql
setup_CreateCommentMutation.sql
setup_CreateCommentMutationWhere.sql
setup_CreateForumWithTagsMutation.sql
setup_CreateForumWithTagsMutationWhere.sql
setup_CreateKnowsMutation.sql
setup_CreatePersonMutation.sql
setup_LikeCommentMutation.sql
setup_LikePostMutation.sql
setup_MessageAuthorQuery.sql
setup_MessageContentQuery.sql
setup_MessageForumAndModeratorsQuery.sql
setup_MessageRepliesQuery.sql
setup_PersonFriendsQuery.sql
setup_PersonProfileQuery.sql
setup_PersonRecentMessagesQuery.sql
workload_AddForumMemberMutation.sql
workload_CreateCommentMutation.sql
workload_CreateCommentMutationWhere.sql
workload_CreateForumWithTagsMutation.sql
workload_CreateForumWithTagsMutationWhere.sql
workload_CreateKnowsMutation.sql
workload_CreatePersonMutation.sql
workload_LikeCommentMutation.sql
workload_LikePostMutation.sql
workload_MessageAuthorQuery.sql
workload_MessageContentQuery.sql
workload_MessageForumAndModeratorsQuery.sql
workload_MessageRepliesQuery.sql
workload_PersonFriendsQuery.sql
workload_PersonProfileQuery.sql
workload_PersonRecentMessagesQuery.sql
Special thanks to @sandy-bes for pointing out the agtype performance issues, suggesting the initial direction for the optimization, and providing a thorough code review. Your feedback was very helpful in shaping this work.