From 74dd20423ddeff1ceba02048371e9893a1d644a2 Mon Sep 17 00:00:00 2001 From: John Gemignani Date: Sat, 20 Dec 2025 08:49:04 -0800 Subject: [PATCH] Add index-based lookups and oid caching NOTE: This work was done with an AI coding tool and a human. Replace sequential table scans with index-based lookups for single-row vertex operations in the unified vertex table architecture. This improves performance from O(n) to O(log n) for vertex existence checks, retrievals, updates, and deletions. Changes: * vertex_exists() in cypher_utils.c: Use systable_beginscan() with the primary key index instead of table_beginscan() * get_vertex() in agtype.c: Use index scan for startNode()/endNode() vertex retrieval * process_delete_list() in cypher_delete.c: Use index scan for vertex lookups; fix scan key comparison from F_GRAPHIDEQ to F_INT8EQ since unified vertex table stores id as bigint, not graphid * process_update_list() in cypher_set.c: Use index scan for SET/REMOVE operations Add cache-first lookup optimization: * _label_name_from_table_oid() in ag_label.c: Check label relation cache before falling back to syscache lookup, reducing catalog overhead for repeated label name lookups All changes use RelationGetIndexList() with rd_pkindex to obtain the primary key index OID for systable_beginscan(). Added regression tests. modified: regress/expected/unified_vertex_table.out modified: regress/sql/unified_vertex_table.sql modified: src/backend/catalog/ag_label.c modified: src/backend/executor/cypher_delete.c modified: src/backend/executor/cypher_set.c modified: src/backend/executor/cypher_utils.c modified: src/backend/utils/adt/agtype.c --- regress/expected/unified_vertex_table.out | 406 +++++++++++++++++++++- regress/sql/unified_vertex_table.sql | 224 ++++++++++++ src/backend/catalog/ag_label.c | 21 +- src/backend/executor/cypher_delete.c | 25 +- src/backend/executor/cypher_set.c | 25 +- src/backend/executor/cypher_utils.c | 24 +- src/backend/utils/adt/agtype.c | 23 +- 7 files changed, 724 insertions(+), 24 deletions(-) diff --git a/regress/expected/unified_vertex_table.out b/regress/expected/unified_vertex_table.out index 38fac52f4..b6037b610 100644 --- a/regress/expected/unified_vertex_table.out +++ b/regress/expected/unified_vertex_table.out @@ -392,11 +392,406 @@ WHERE properties::text LIKE '%val%'; 3 (1 row) +-- +-- Test 12: Index scan optimization for vertex_exists() +-- This exercises the systable_beginscan path in vertex_exists() +-- +SELECT * FROM cypher('unified_test', $$ + CREATE (:IndexTest {id: 100}) +$$) AS (v agtype); + v +--- +(0 rows) + +SELECT * FROM cypher('unified_test', $$ + CREATE (:IndexTest {id: 101}) +$$) AS (v agtype); + v +--- +(0 rows) + +SELECT * FROM cypher('unified_test', $$ + CREATE (:IndexTest {id: 102}) +$$) AS (v agtype); + v +--- +(0 rows) + +-- DETACH DELETE exercises vertex_exists() to check vertex validity +SELECT * FROM cypher('unified_test', $$ + MATCH (n:IndexTest {id: 100}) + DETACH DELETE n +$$) AS (v agtype); + v +--- +(0 rows) + +-- Verify vertex was deleted and others remain +SELECT * FROM cypher('unified_test', $$ + MATCH (n:IndexTest) + RETURN n.id ORDER BY n.id +$$) AS (id agtype); + id +----- + 101 + 102 +(2 rows) + +-- Multiple deletes to exercise index scan repeatedly +SELECT * FROM cypher('unified_test', $$ + MATCH (n:IndexTest) + DELETE n +$$) AS (v agtype); + v +--- +(0 rows) + +-- Verify all IndexTest vertices are gone +SELECT * FROM cypher('unified_test', $$ + MATCH (n:IndexTest) + RETURN count(n) +$$) AS (cnt agtype); + cnt +----- + 0 +(1 row) + +-- +-- Test 13: Index scan optimization for get_vertex() via startNode/endNode +-- This exercises the systable_beginscan path in get_vertex() +-- +SELECT * FROM cypher('unified_test', $$ + CREATE (a:GetVertexTest {name: 'source1'})-[:LINK]->(b:GetVertexTest {name: 'target1'}) +$$) AS (v agtype); + v +--- +(0 rows) + +SELECT * FROM cypher('unified_test', $$ + CREATE (a:GetVertexTest {name: 'source2'})-[:LINK]->(b:GetVertexTest {name: 'target2'}) +$$) AS (v agtype); + v +--- +(0 rows) + +SELECT * FROM cypher('unified_test', $$ + CREATE (a:GetVertexTest {name: 'source3'})-[:LINK]->(b:GetVertexTest {name: 'target3'}) +$$) AS (v agtype); + v +--- +(0 rows) + +-- Multiple startNode/endNode calls exercise get_vertex() with index scans +SELECT * FROM cypher('unified_test', $$ + MATCH ()-[e:LINK]->() + RETURN startNode(e).name AS src, endNode(e).name AS tgt, + label(startNode(e)) AS src_label, label(endNode(e)) AS tgt_label + ORDER BY src +$$) AS (src agtype, tgt agtype, src_label agtype, tgt_label agtype); + src | tgt | src_label | tgt_label +-----------+-----------+-----------------+----------------- + "source1" | "target1" | "GetVertexTest" | "GetVertexTest" + "source2" | "target2" | "GetVertexTest" | "GetVertexTest" + "source3" | "target3" | "GetVertexTest" | "GetVertexTest" +(3 rows) + +-- Chain of edges to test repeated get_vertex calls +SELECT * FROM cypher('unified_test', $$ + MATCH (a:GetVertexTest {name: 'target1'}) + CREATE (a)-[:CHAIN]->(:GetVertexTest {name: 'chain1'})-[:CHAIN]->(:GetVertexTest {name: 'chain2'}) +$$) AS (v agtype); + v +--- +(0 rows) + +SELECT * FROM cypher('unified_test', $$ + MATCH ()-[e:CHAIN]->() + RETURN startNode(e).name, endNode(e).name + ORDER BY startNode(e).name +$$) AS (src agtype, tgt agtype); + src | tgt +-----------+---------- + "chain1" | "chain2" + "target1" | "chain1" +(2 rows) + +-- +-- Test 14: Index scan optimization for process_delete_list() +-- This exercises the F_INT8EQ fix and systable_beginscan in DELETE +-- +SELECT * FROM cypher('unified_test', $$ + CREATE (:DeleteTest {seq: 1}), (:DeleteTest {seq: 2}), (:DeleteTest {seq: 3}), + (:DeleteTest {seq: 4}), (:DeleteTest {seq: 5}) +$$) AS (v agtype); + v +--- +(0 rows) + +-- Verify vertices exist +SELECT * FROM cypher('unified_test', $$ + MATCH (n:DeleteTest) + RETURN n.seq ORDER BY n.seq +$$) AS (seq agtype); + seq +----- + 1 + 2 + 3 + 4 + 5 +(5 rows) + +-- Delete specific vertex by property (exercises index lookup) +SELECT * FROM cypher('unified_test', $$ + MATCH (n:DeleteTest {seq: 3}) + DELETE n +$$) AS (v agtype); + v +--- +(0 rows) + +-- Verify correct vertex was deleted +SELECT * FROM cypher('unified_test', $$ + MATCH (n:DeleteTest) + RETURN n.seq ORDER BY n.seq +$$) AS (seq agtype); + seq +----- + 1 + 2 + 4 + 5 +(4 rows) + +-- Delete with edges (exercises process_delete_list with edge cleanup) +SELECT * FROM cypher('unified_test', $$ + MATCH (a:DeleteTest {seq: 1}) + CREATE (a)-[:DEL_EDGE]->(:DeleteTest {seq: 10}) +$$) AS (v agtype); + v +--- +(0 rows) + +SELECT * FROM cypher('unified_test', $$ + MATCH (n:DeleteTest {seq: 1}) + DETACH DELETE n +$$) AS (v agtype); + v +--- +(0 rows) + +-- Verify vertex and edge were deleted +SELECT * FROM cypher('unified_test', $$ + MATCH (n:DeleteTest) + RETURN n.seq ORDER BY n.seq +$$) AS (seq agtype); + seq +----- + 2 + 4 + 5 + 10 +(4 rows) + +-- +-- Test 15: Index scan optimization for process_update_list() +-- This exercises the systable_beginscan in SET/REMOVE operations +-- +SELECT * FROM cypher('unified_test', $$ + CREATE (:UpdateTest {id: 1, val: 'original1'}), + (:UpdateTest {id: 2, val: 'original2'}), + (:UpdateTest {id: 3, val: 'original3'}) +$$) AS (v agtype); + v +--- +(0 rows) + +-- Single SET operation +SELECT * FROM cypher('unified_test', $$ + MATCH (n:UpdateTest {id: 1}) + SET n.val = 'updated1' + RETURN n.id, n.val +$$) AS (id agtype, val agtype); + id | val +----+------------ + 1 | "updated1" +(1 row) + +-- Multiple SET operations in one query (exercises repeated index lookups) +SELECT * FROM cypher('unified_test', $$ + MATCH (n:UpdateTest) + SET n.modified = true + RETURN n.id, n.val, n.modified ORDER BY n.id +$$) AS (id agtype, val agtype, modified agtype); + id | val | modified +----+-------------+---------- + 1 | "updated1" | true + 2 | "original2" | true + 3 | "original3" | true +(3 rows) + +-- SET with property addition +SELECT * FROM cypher('unified_test', $$ + MATCH (n:UpdateTest {id: 2}) + SET n.extra = 'new_property', n.val = 'updated2' + RETURN n.id, n.val, n.extra +$$) AS (id agtype, val agtype, extra agtype); + id | val | extra +----+------------+---------------- + 2 | "updated2" | "new_property" +(1 row) + +-- REMOVE property operation +SELECT * FROM cypher('unified_test', $$ + MATCH (n:UpdateTest {id: 3}) + REMOVE n.val + RETURN n.id, n.val, n.modified +$$) AS (id agtype, val agtype, modified agtype); + id | val | modified +----+-----+---------- + 3 | | true +(1 row) + +-- Verify final state of all UpdateTest vertices +SELECT * FROM cypher('unified_test', $$ + MATCH (n:UpdateTest) + RETURN n ORDER BY n.id +$$) AS (n agtype); + n +------------------------------------------------------------------------------------------------------------------------------------------------ + {"id": 5910974510923777, "label": "UpdateTest", "properties": {"id": 1, "val": "updated1", "modified": true}}::vertex + {"id": 5910974510923778, "label": "UpdateTest", "properties": {"id": 2, "val": "updated2", "extra": "new_property", "modified": true}}::vertex + {"id": 5910974510923779, "label": "UpdateTest", "properties": {"id": 3, "modified": true}}::vertex +(3 rows) + +-- +-- Test 16: OID caching in _label_name_from_table_oid() +-- Repeated calls should use cache after first lookup +-- +-- Call multiple times to exercise cache hit path +SELECT ag_catalog._label_name_from_table_oid('unified_test."Person"'::regclass::oid); + _label_name_from_table_oid +---------------------------- + Person +(1 row) + +SELECT ag_catalog._label_name_from_table_oid('unified_test."Person"'::regclass::oid); + _label_name_from_table_oid +---------------------------- + Person +(1 row) + +SELECT ag_catalog._label_name_from_table_oid('unified_test."Company"'::regclass::oid); + _label_name_from_table_oid +---------------------------- + Company +(1 row) + +SELECT ag_catalog._label_name_from_table_oid('unified_test."Company"'::regclass::oid); + _label_name_from_table_oid +---------------------------- + Company +(1 row) + +SELECT ag_catalog._label_name_from_table_oid('unified_test."Location"'::regclass::oid); + _label_name_from_table_oid +---------------------------- + Location +(1 row) + +SELECT ag_catalog._label_name_from_table_oid('unified_test."Location"'::regclass::oid); + _label_name_from_table_oid +---------------------------- + Location +(1 row) + +-- Call with unified table OID (default vertex label case) +SELECT ag_catalog._label_name_from_table_oid('unified_test._ag_label_vertex'::regclass::oid); + _label_name_from_table_oid +---------------------------- + +(1 row) + +-- Verify label function also benefits from caching (exercises full path) +SELECT * FROM cypher('unified_test', $$ + MATCH (p:Person) + RETURN label(p), label(p), label(p) +$$) AS (l1 agtype, l2 agtype, l3 agtype); + l1 | l2 | l3 +----------+----------+---------- + "Person" | "Person" | "Person" + "Person" | "Person" | "Person" + "Person" | "Person" | "Person" +(3 rows) + +-- +-- Test 17: Combined operations stress test +-- Multiple operations in sequence to verify optimizations work together +-- +SELECT * FROM cypher('unified_test', $$ + CREATE (a:StressTest {id: 1})-[:ST_EDGE]->(b:StressTest {id: 2}) +$$) AS (v agtype); + v +--- +(0 rows) + +-- startNode/endNode (get_vertex index scan) +SELECT * FROM cypher('unified_test', $$ + MATCH ()-[e:ST_EDGE]->() + RETURN startNode(e).id, endNode(e).id +$$) AS (start_id agtype, end_id agtype); + start_id | end_id +----------+-------- + 1 | 2 +(1 row) + +-- SET (process_update_list index scan) +SELECT * FROM cypher('unified_test', $$ + MATCH (n:StressTest) + SET n.updated = true + RETURN n.id, n.updated ORDER BY n.id +$$) AS (id agtype, updated agtype); + id | updated +----+--------- + 1 | true + 2 | true +(2 rows) + +-- label() calls (OID cache) +SELECT * FROM cypher('unified_test', $$ + MATCH (n:StressTest) + RETURN n.id, label(n) ORDER BY n.id +$$) AS (id agtype, lbl agtype); + id | lbl +----+-------------- + 1 | "StressTest" + 2 | "StressTest" +(2 rows) + +-- DETACH DELETE (vertex_exists + process_delete_list index scans) +SELECT * FROM cypher('unified_test', $$ + MATCH (n:StressTest) + DETACH DELETE n +$$) AS (v agtype); + v +--- +(0 rows) + +-- Verify cleanup +SELECT * FROM cypher('unified_test', $$ + MATCH (n:StressTest) + RETURN count(n) +$$) AS (cnt agtype); + cnt +----- + 0 +(1 row) + -- -- Cleanup -- SELECT drop_graph('unified_test', true); -NOTICE: drop cascades to 14 other objects +NOTICE: drop cascades to 23 other objects DETAIL: drop cascades to table unified_test._ag_label_vertex drop cascades to table unified_test._ag_label_edge drop cascades to table unified_test."Person" @@ -411,6 +806,15 @@ drop cascades to table unified_test."CONNECTED" drop cascades to table unified_test."LabelA" drop cascades to table unified_test."LabelB" drop cascades to table unified_test."LabelC" +drop cascades to table unified_test."IndexTest" +drop cascades to table unified_test."GetVertexTest" +drop cascades to table unified_test."LINK" +drop cascades to table unified_test."CHAIN" +drop cascades to table unified_test."DeleteTest" +drop cascades to table unified_test."DEL_EDGE" +drop cascades to table unified_test."UpdateTest" +drop cascades to table unified_test."StressTest" +drop cascades to table unified_test."ST_EDGE" NOTICE: graph "unified_test" has been dropped drop_graph ------------ diff --git a/regress/sql/unified_vertex_table.sql b/regress/sql/unified_vertex_table.sql index 84145e384..f9f30f667 100644 --- a/regress/sql/unified_vertex_table.sql +++ b/regress/sql/unified_vertex_table.sql @@ -238,6 +238,230 @@ $$) AS (v agtype); SELECT COUNT(DISTINCT labels) AS distinct_labels FROM unified_test._ag_label_vertex WHERE properties::text LIKE '%val%'; +-- +-- Test 12: Index scan optimization for vertex_exists() +-- This exercises the systable_beginscan path in vertex_exists() +-- +SELECT * FROM cypher('unified_test', $$ + CREATE (:IndexTest {id: 100}) +$$) AS (v agtype); + +SELECT * FROM cypher('unified_test', $$ + CREATE (:IndexTest {id: 101}) +$$) AS (v agtype); + +SELECT * FROM cypher('unified_test', $$ + CREATE (:IndexTest {id: 102}) +$$) AS (v agtype); + +-- DETACH DELETE exercises vertex_exists() to check vertex validity +SELECT * FROM cypher('unified_test', $$ + MATCH (n:IndexTest {id: 100}) + DETACH DELETE n +$$) AS (v agtype); + +-- Verify vertex was deleted and others remain +SELECT * FROM cypher('unified_test', $$ + MATCH (n:IndexTest) + RETURN n.id ORDER BY n.id +$$) AS (id agtype); + +-- Multiple deletes to exercise index scan repeatedly +SELECT * FROM cypher('unified_test', $$ + MATCH (n:IndexTest) + DELETE n +$$) AS (v agtype); + +-- Verify all IndexTest vertices are gone +SELECT * FROM cypher('unified_test', $$ + MATCH (n:IndexTest) + RETURN count(n) +$$) AS (cnt agtype); + +-- +-- Test 13: Index scan optimization for get_vertex() via startNode/endNode +-- This exercises the systable_beginscan path in get_vertex() +-- +SELECT * FROM cypher('unified_test', $$ + CREATE (a:GetVertexTest {name: 'source1'})-[:LINK]->(b:GetVertexTest {name: 'target1'}) +$$) AS (v agtype); + +SELECT * FROM cypher('unified_test', $$ + CREATE (a:GetVertexTest {name: 'source2'})-[:LINK]->(b:GetVertexTest {name: 'target2'}) +$$) AS (v agtype); + +SELECT * FROM cypher('unified_test', $$ + CREATE (a:GetVertexTest {name: 'source3'})-[:LINK]->(b:GetVertexTest {name: 'target3'}) +$$) AS (v agtype); + +-- Multiple startNode/endNode calls exercise get_vertex() with index scans +SELECT * FROM cypher('unified_test', $$ + MATCH ()-[e:LINK]->() + RETURN startNode(e).name AS src, endNode(e).name AS tgt, + label(startNode(e)) AS src_label, label(endNode(e)) AS tgt_label + ORDER BY src +$$) AS (src agtype, tgt agtype, src_label agtype, tgt_label agtype); + +-- Chain of edges to test repeated get_vertex calls +SELECT * FROM cypher('unified_test', $$ + MATCH (a:GetVertexTest {name: 'target1'}) + CREATE (a)-[:CHAIN]->(:GetVertexTest {name: 'chain1'})-[:CHAIN]->(:GetVertexTest {name: 'chain2'}) +$$) AS (v agtype); + +SELECT * FROM cypher('unified_test', $$ + MATCH ()-[e:CHAIN]->() + RETURN startNode(e).name, endNode(e).name + ORDER BY startNode(e).name +$$) AS (src agtype, tgt agtype); + +-- +-- Test 14: Index scan optimization for process_delete_list() +-- This exercises the F_INT8EQ fix and systable_beginscan in DELETE +-- +SELECT * FROM cypher('unified_test', $$ + CREATE (:DeleteTest {seq: 1}), (:DeleteTest {seq: 2}), (:DeleteTest {seq: 3}), + (:DeleteTest {seq: 4}), (:DeleteTest {seq: 5}) +$$) AS (v agtype); + +-- Verify vertices exist +SELECT * FROM cypher('unified_test', $$ + MATCH (n:DeleteTest) + RETURN n.seq ORDER BY n.seq +$$) AS (seq agtype); + +-- Delete specific vertex by property (exercises index lookup) +SELECT * FROM cypher('unified_test', $$ + MATCH (n:DeleteTest {seq: 3}) + DELETE n +$$) AS (v agtype); + +-- Verify correct vertex was deleted +SELECT * FROM cypher('unified_test', $$ + MATCH (n:DeleteTest) + RETURN n.seq ORDER BY n.seq +$$) AS (seq agtype); + +-- Delete with edges (exercises process_delete_list with edge cleanup) +SELECT * FROM cypher('unified_test', $$ + MATCH (a:DeleteTest {seq: 1}) + CREATE (a)-[:DEL_EDGE]->(:DeleteTest {seq: 10}) +$$) AS (v agtype); + +SELECT * FROM cypher('unified_test', $$ + MATCH (n:DeleteTest {seq: 1}) + DETACH DELETE n +$$) AS (v agtype); + +-- Verify vertex and edge were deleted +SELECT * FROM cypher('unified_test', $$ + MATCH (n:DeleteTest) + RETURN n.seq ORDER BY n.seq +$$) AS (seq agtype); + +-- +-- Test 15: Index scan optimization for process_update_list() +-- This exercises the systable_beginscan in SET/REMOVE operations +-- +SELECT * FROM cypher('unified_test', $$ + CREATE (:UpdateTest {id: 1, val: 'original1'}), + (:UpdateTest {id: 2, val: 'original2'}), + (:UpdateTest {id: 3, val: 'original3'}) +$$) AS (v agtype); + +-- Single SET operation +SELECT * FROM cypher('unified_test', $$ + MATCH (n:UpdateTest {id: 1}) + SET n.val = 'updated1' + RETURN n.id, n.val +$$) AS (id agtype, val agtype); + +-- Multiple SET operations in one query (exercises repeated index lookups) +SELECT * FROM cypher('unified_test', $$ + MATCH (n:UpdateTest) + SET n.modified = true + RETURN n.id, n.val, n.modified ORDER BY n.id +$$) AS (id agtype, val agtype, modified agtype); + +-- SET with property addition +SELECT * FROM cypher('unified_test', $$ + MATCH (n:UpdateTest {id: 2}) + SET n.extra = 'new_property', n.val = 'updated2' + RETURN n.id, n.val, n.extra +$$) AS (id agtype, val agtype, extra agtype); + +-- REMOVE property operation +SELECT * FROM cypher('unified_test', $$ + MATCH (n:UpdateTest {id: 3}) + REMOVE n.val + RETURN n.id, n.val, n.modified +$$) AS (id agtype, val agtype, modified agtype); + +-- Verify final state of all UpdateTest vertices +SELECT * FROM cypher('unified_test', $$ + MATCH (n:UpdateTest) + RETURN n ORDER BY n.id +$$) AS (n agtype); + +-- +-- Test 16: OID caching in _label_name_from_table_oid() +-- Repeated calls should use cache after first lookup +-- +-- Call multiple times to exercise cache hit path +SELECT ag_catalog._label_name_from_table_oid('unified_test."Person"'::regclass::oid); +SELECT ag_catalog._label_name_from_table_oid('unified_test."Person"'::regclass::oid); +SELECT ag_catalog._label_name_from_table_oid('unified_test."Company"'::regclass::oid); +SELECT ag_catalog._label_name_from_table_oid('unified_test."Company"'::regclass::oid); +SELECT ag_catalog._label_name_from_table_oid('unified_test."Location"'::regclass::oid); +SELECT ag_catalog._label_name_from_table_oid('unified_test."Location"'::regclass::oid); + +-- Call with unified table OID (default vertex label case) +SELECT ag_catalog._label_name_from_table_oid('unified_test._ag_label_vertex'::regclass::oid); + +-- Verify label function also benefits from caching (exercises full path) +SELECT * FROM cypher('unified_test', $$ + MATCH (p:Person) + RETURN label(p), label(p), label(p) +$$) AS (l1 agtype, l2 agtype, l3 agtype); + +-- +-- Test 17: Combined operations stress test +-- Multiple operations in sequence to verify optimizations work together +-- +SELECT * FROM cypher('unified_test', $$ + CREATE (a:StressTest {id: 1})-[:ST_EDGE]->(b:StressTest {id: 2}) +$$) AS (v agtype); + +-- startNode/endNode (get_vertex index scan) +SELECT * FROM cypher('unified_test', $$ + MATCH ()-[e:ST_EDGE]->() + RETURN startNode(e).id, endNode(e).id +$$) AS (start_id agtype, end_id agtype); + +-- SET (process_update_list index scan) +SELECT * FROM cypher('unified_test', $$ + MATCH (n:StressTest) + SET n.updated = true + RETURN n.id, n.updated ORDER BY n.id +$$) AS (id agtype, updated agtype); + +-- label() calls (OID cache) +SELECT * FROM cypher('unified_test', $$ + MATCH (n:StressTest) + RETURN n.id, label(n) ORDER BY n.id +$$) AS (id agtype, lbl agtype); + +-- DETACH DELETE (vertex_exists + process_delete_list index scans) +SELECT * FROM cypher('unified_test', $$ + MATCH (n:StressTest) + DETACH DELETE n +$$) AS (v agtype); + +-- Verify cleanup +SELECT * FROM cypher('unified_test', $$ + MATCH (n:StressTest) + RETURN count(n) +$$) AS (cnt agtype); + -- -- Cleanup -- diff --git a/src/backend/catalog/ag_label.c b/src/backend/catalog/ag_label.c index 9578276d7..5e3d004b7 100644 --- a/src/backend/catalog/ag_label.c +++ b/src/backend/catalog/ag_label.c @@ -227,10 +227,14 @@ PG_FUNCTION_INFO_V1(_label_name_from_table_oid); /* * Given a label table OID, return the label name. * Returns empty string for the default vertex/edge table. + * + * This function first checks the AGE label cache for fast lookups, + * then falls back to PostgreSQL's syscache if not found. */ Datum _label_name_from_table_oid(PG_FUNCTION_ARGS) { Oid label_table_oid; + label_cache_data *cache_data; char *label_name; if (PG_ARGISNULL(0)) @@ -241,7 +245,22 @@ Datum _label_name_from_table_oid(PG_FUNCTION_ARGS) label_table_oid = PG_GETARG_OID(0); - /* Get the relation name from the OID */ + /* Try the AGE label cache first for fast lookup */ + cache_data = search_label_relation_cache(label_table_oid); + if (cache_data != NULL) + { + label_name = NameStr(cache_data->name); + + /* Return empty string for default labels */ + if (IS_AG_DEFAULT_LABEL(label_name)) + { + PG_RETURN_CSTRING(""); + } + + PG_RETURN_CSTRING(pstrdup(label_name)); + } + + /* Fallback to PostgreSQL syscache */ label_name = get_rel_name(label_table_oid); if (label_name == NULL) diff --git a/src/backend/executor/cypher_delete.c b/src/backend/executor/cypher_delete.c index ddfed8edb..ee97343c7 100644 --- a/src/backend/executor/cypher_delete.c +++ b/src/backend/executor/cypher_delete.c @@ -19,8 +19,11 @@ #include "postgres.h" +#include "access/genam.h" #include "storage/bufmgr.h" #include "common/hashfn.h" +#include "utils/rel.h" +#include "utils/relcache.h" #include "catalog/ag_label.h" #include "executor/cypher_executor.h" @@ -376,12 +379,13 @@ static void process_delete_list(CustomScanState *node) cypher_delete_item *item; agtype_value *original_entity_value, *id, *label; ScanKeyData scan_keys[1]; - TableScanDesc scan_desc; + SysScanDesc scan_desc; ResultRelInfo *resultRelInfo; HeapTuple heap_tuple; char *label_name; Integer *pos; int entity_position; + Oid pk_index_oid; item = lfirst(lc); @@ -424,16 +428,25 @@ static void process_delete_list(CustomScanState *node) errmsg("DELETE clause can only delete vertices and edges"))); } + /* + * Get primary key index OID from relation cache for index scan. + * For vertices, this enables fast index lookups on the unified table. + */ + (void) RelationGetIndexList(resultRelInfo->ri_RelationDesc); + pk_index_oid = resultRelInfo->ri_RelationDesc->rd_pkindex; + /* * Setup the scan description, with the correct snapshot and scan keys. + * Use systable_beginscan for index-based lookup when available. */ estate->es_snapshot->curcid = GetCurrentCommandId(false); estate->es_output_cid = GetCurrentCommandId(false); - scan_desc = table_beginscan(resultRelInfo->ri_RelationDesc, - estate->es_snapshot, 1, scan_keys); + scan_desc = systable_beginscan(resultRelInfo->ri_RelationDesc, + pk_index_oid, true, + estate->es_snapshot, 1, scan_keys); /* Retrieve the tuple. */ - heap_tuple = heap_getnext(scan_desc, ForwardScanDirection); + heap_tuple = systable_getnext(scan_desc); /* * If the heap tuple still exists (It wasn't deleted after this variable @@ -442,7 +455,7 @@ static void process_delete_list(CustomScanState *node) */ if (!HeapTupleIsValid(heap_tuple)) { - table_endscan(scan_desc); + systable_endscan(scan_desc); destroy_entity_result_rel_info(resultRelInfo); continue; @@ -464,7 +477,7 @@ static void process_delete_list(CustomScanState *node) delete_entity(estate, resultRelInfo, heap_tuple); /* Close the scan and the relation. */ - table_endscan(scan_desc); + systable_endscan(scan_desc); destroy_entity_result_rel_info(resultRelInfo); } } diff --git a/src/backend/executor/cypher_set.c b/src/backend/executor/cypher_set.c index fb2625ba2..c13cee04b 100644 --- a/src/backend/executor/cypher_set.c +++ b/src/backend/executor/cypher_set.c @@ -19,7 +19,10 @@ #include "postgres.h" +#include "access/genam.h" #include "storage/bufmgr.h" +#include "utils/rel.h" +#include "utils/relcache.h" #include "catalog/ag_label.h" #include "catalog/namespace.h" @@ -398,7 +401,7 @@ static void process_update_list(CustomScanState *node) TupleTableSlot *slot; ResultRelInfo *resultRelInfo; ScanKeyData scan_keys[1]; - TableScanDesc scan_desc; + SysScanDesc scan_desc; bool remove_property; char *label_name; cypher_update_item *update_item; @@ -406,6 +409,7 @@ static void process_update_list(CustomScanState *node) HeapTuple heap_tuple; char *clause_name = css->set_list->clause_name; int cid; + Oid pk_index_oid; update_item = (cypher_update_item *)lfirst(lc); @@ -610,14 +614,23 @@ static void process_update_list(CustomScanState *node) ScanKeyInit(&scan_keys[0], 1, BTEqualStrategyNumber, F_GRAPHIDEQ, GRAPHID_GET_DATUM(id->val.int_value)); } + + /* + * Get primary key index OID from relation cache for index scan. + * This enables fast index lookups instead of sequential scans. + */ + (void) RelationGetIndexList(resultRelInfo->ri_RelationDesc); + pk_index_oid = resultRelInfo->ri_RelationDesc->rd_pkindex; + /* * Setup the scan description, with the correct snapshot and scan - * keys. + * keys. Use systable_beginscan for index-based lookup when available. */ - scan_desc = table_beginscan(resultRelInfo->ri_RelationDesc, - estate->es_snapshot, 1, scan_keys); + scan_desc = systable_beginscan(resultRelInfo->ri_RelationDesc, + pk_index_oid, true, + estate->es_snapshot, 1, scan_keys); /* Retrieve the tuple. */ - heap_tuple = heap_getnext(scan_desc, ForwardScanDirection); + heap_tuple = systable_getnext(scan_desc); /* * If the heap tuple still exists (It wasn't deleted between the @@ -629,7 +642,7 @@ static void process_update_list(CustomScanState *node) heap_tuple); } /* close the ScanDescription */ - table_endscan(scan_desc); + systable_endscan(scan_desc); } estate->es_snapshot->curcid = cid; diff --git a/src/backend/executor/cypher_utils.c b/src/backend/executor/cypher_utils.c index a2dfc6b65..20aada908 100644 --- a/src/backend/executor/cypher_utils.c +++ b/src/backend/executor/cypher_utils.c @@ -24,10 +24,13 @@ #include "postgres.h" +#include "access/genam.h" #include "catalog/namespace.h" #include "nodes/makefuncs.h" #include "parser/parse_relation.h" #include "utils/lsyscache.h" +#include "utils/rel.h" +#include "utils/relcache.h" #include "catalog/ag_graph.h" #include "catalog/ag_label.h" @@ -194,18 +197,20 @@ TupleTableSlot *populate_edge_tts( * Find out if the vertex still exists. This is for 'implicit' deletion * of a vertex - checking if a vertex was deleted by another variable. * - * NOTE: This function scans the unified _ag_label_vertex table directly. + * NOTE: This function scans the unified _ag_label_vertex table directly + * using an index scan on the primary key for efficiency. * No information is extracted from the graphid - the graphid is only used * as the search key to find the vertex row. */ bool vertex_exists(EState *estate, Oid graph_oid, graphid id) { ScanKeyData scan_keys[1]; - TableScanDesc scan_desc; + SysScanDesc scan_desc; HeapTuple tuple; Relation rel; bool result = true; Oid vertex_table_oid; + Oid pk_index_oid; /* Get the unified vertex table OID directly from graph_oid */ vertex_table_oid = get_label_relation(AG_DEFAULT_LABEL_VERTEX, graph_oid); @@ -216,8 +221,17 @@ bool vertex_exists(EState *estate, Oid graph_oid, graphid id) rel = table_open(vertex_table_oid, RowExclusiveLock); - scan_desc = table_beginscan(rel, estate->es_snapshot, 1, scan_keys); - tuple = heap_getnext(scan_desc, ForwardScanDirection); + /* Get primary key index OID from relation cache for index scan */ + (void) RelationGetIndexList(rel); + pk_index_oid = rel->rd_pkindex; + + /* + * Use systable_beginscan which will use the primary key index if available. + * This is much faster than a sequential scan for single-row lookups. + */ + scan_desc = systable_beginscan(rel, pk_index_oid, true, + estate->es_snapshot, 1, scan_keys); + tuple = systable_getnext(scan_desc); /* * If a single tuple was returned, the tuple is still valid, otherwise @@ -228,7 +242,7 @@ bool vertex_exists(EState *estate, Oid graph_oid, graphid id) result = false; } - table_endscan(scan_desc); + systable_endscan(scan_desc); table_close(rel, RowExclusiveLock); return result; diff --git a/src/backend/utils/adt/agtype.c b/src/backend/utils/adt/agtype.c index 20ccdcd35..7163cb7d5 100644 --- a/src/backend/utils/adt/agtype.c +++ b/src/backend/utils/adt/agtype.c @@ -47,6 +47,8 @@ #include "utils/builtins.h" #include "utils/float.h" #include "utils/lsyscache.h" +#include "utils/rel.h" +#include "utils/relcache.h" #include "utils/snapmgr.h" #include "utils/typcache.h" #include "utils/age_vle.h" @@ -5544,7 +5546,7 @@ static Datum get_vertex(const char *graph, int64 graphid) { ScanKeyData scan_keys[1]; Relation graph_vertex_table; - TableScanDesc scan_desc; + SysScanDesc scan_desc; HeapTuple tuple; TupleDesc tupdesc; Datum id, properties, labels_oid_datum; @@ -5552,6 +5554,7 @@ static Datum get_vertex(const char *graph, int64 graphid) Oid label_table_oid; label_cache_data *label_cache; char *label_name; + Oid pk_index_oid; /* get the specific graph namespace (schema) */ Oid graph_namespace_oid = get_namespace_oid(graph, false); @@ -5565,10 +5568,20 @@ static Datum get_vertex(const char *graph, int64 graphid) ScanKeyInit(&scan_keys[0], 1, BTEqualStrategyNumber, F_INT8EQ, Int64GetDatum(graphid)); - /* open the unified vertex table, begin the scan, and get the tuple */ + /* open the unified vertex table */ graph_vertex_table = table_open(vertex_table_oid, ShareLock); - scan_desc = table_beginscan(graph_vertex_table, snapshot, 1, scan_keys); - tuple = heap_getnext(scan_desc, ForwardScanDirection); + + /* Get primary key index OID from relation cache for index scan */ + (void) RelationGetIndexList(graph_vertex_table); + pk_index_oid = graph_vertex_table->rd_pkindex; + + /* + * Use systable_beginscan which will use the primary key index if available. + * This is much faster than a sequential scan for single-row lookups. + */ + scan_desc = systable_beginscan(graph_vertex_table, pk_index_oid, true, + snapshot, 1, scan_keys); + tuple = systable_getnext(scan_desc); /* bail if the tuple isn't valid */ if (!HeapTupleIsValid(tuple)) @@ -5614,7 +5627,7 @@ static Datum get_vertex(const char *graph, int64 graphid) result = DirectFunctionCall3(_agtype_build_vertex, id, CStringGetDatum(label_name), properties); /* end the scan and close the relation */ - table_endscan(scan_desc); + systable_endscan(scan_desc); table_close(graph_vertex_table, ShareLock); /* return the vertex datum */ return result;