crdv7 opened a new issue, #2484:
URL: https://github.com/apache/age/issues/2484

   **Is your feature request related to a problem? Please describe.**
   
   Writable Cypher clauses such as `SET`, `REMOVE`, and `DELETE` re-locate the
   backing tuple of every graph entity even though the preceding `MATCH` has
   already read that tuple in the same statement.
   
   PR [#2117](https://github.com/apache/age/pull/2117) added a unique `id` index
   to vertex labels and `start_id`/`end_id` indexes to edge labels. PR
   [#2351](https://github.com/apache/age/pull/2351) made entity lookup use an
   `id` index when available. For `M` writes against a label containing `N`
   entities, repeating that B-tree lookup adds `O(M log N)` lookup work. A
   validated CTID hint addresses the heap tuple directly, making the expected
   lookup cost `O(1)` per entity and `O(M)` overall, aside from normal MVCC and
   buffer-access costs.
   
   Edge labels have a more expensive fallback. Their default `start_id` and
   `end_id` indexes cannot resolve an edge by graphid, so without a user-created
   `id` index each lookup can scan `O(N)` rows. This adds `O(MN)` lookup work 
and
   can approach `O(N²)` for a batch that writes the whole label.
   
   For a vertex label, the following clauses repeat the default `id` B-tree
   lookup for every matched vertex:
   
   ```cypher
   MATCH (n:Person)
   SET n.updated = true
   ```
   
   ```cypher
   MATCH (n:Person)
   DELETE n
   ```
   
   For an edge label without a user-created `id` index, the writable lookup in
   the following query can instead perform a sequential scan for every matched
   edge:
   
   ```cypher
   MATCH ()-[r:KNOWS]->()
   WITH r
   REMOVE r.temporary_property
   ```
   
   In each case, `MATCH` already knows the tuple's physical location. The
   writable executor discards that information and resolves the entity again by
   graphid.
   
   **Describe the solution you'd like**
   
   Carry the tuple `ctid` as an internal, hidden lookup hint from `MATCH` to
   downstream writable clauses.
   
   This follows the same principle as PostgreSQL's own `UPDATE` and `DELETE`
   execution: carry the target tuple's physical row identity into the write
   executor instead of resolving the row again through a logical-key index.
   Because 
[`ctid`](https://www.postgresql.org/docs/current/ddl-system-columns.html)
   identifies a tuple version's physical location, it enables an expected
   `O(1)` direct heap fetch and avoids another B-tree descent.
   
   AGE's writable clauses can carry this information internally from the matched
   row to the write executor without exposing it in query results. The CTID
   would remain only a hint: the executor must validate snapshot visibility and
   graphid identity, then fall back to the existing graphid lookup when the hint
   is missing or stale. Graphid remains the logical entity identity, and
   read-only queries remain unchanged.
   
   **Describe alternatives you've considered**
   
   1. **Keep the current graphid lookup design.** This requires no planner
      changes, but repeats an `id` B-tree lookup for every entity already read 
by
      `MATCH` and falls back to repeated sequential scans when no suitable index
      exists.
   2. **Redesign writable Cypher clauses around PostgreSQL
      `ModifyTable`-style execution.** This could carry native row identity into
      PostgreSQL's update/delete machinery, but would require a much broader
      parser, planner, and executor change to preserve dynamic graph labels,
      chained Cypher clauses, `MERGE`, alias/path projection, RLS, and Cypher
      result semantics.
   
   Using a validated hidden CTID hint keeps the optimization local to writable
   custom scans while preserving graphid as the logical entity identity. It
   captures the main row-location benefit without requiring that architectural
   rewrite.
   
   **Additional context**
   
   I profiled the current implementation at commit
   
[`801417404978823bd8732452c3f7959017584785`](https://github.com/apache/age/commit/801417404978823bd8732452c3f7959017584785)
   using a 2,000,000-vertex `DELETE` workload.
   
   The baseline flame graph contains repeated B-tree work below
   `process_delete_list()`:
   
   ```text
   process_delete_list
     index_getnext_slot
       index_getnext_tid
         btgettuple
           _bt_first
             _bt_search
   ```
   
   Stacks containing `process_delete_list;index_getnext_slot` account for 14.21%
   of sampled CPU cycles. This is the graphid lookup repeated after `MATCH` has
   already located each vertex.
   
   - Baseline flame graph:
   
   <img width="1200" height="750" alt="Image" 
src="https://github.com/user-attachments/assets/39d16c1e-b7bd-4382-a868-be4109ab2ea2";
 />
   
   I also measured current edge-label scaling without an `id` index using the
   workload reproduced by
   
[repro_edge_set_no_id_index.sql](https://github.com/user-attachments/files/30529509/repro_edge_set_no_id_index.sql).
   The script creates an edge label with only the default `start_id` and 
`end_id`
   indexes, loads the requested number of edges, and runs `SET` on every edge.
   
   The following results use that workload without a user-created `id` index.
   Each value is the median of five measured runs after one unreported warm-up:
   
   | Edges | Median time | Growth from 10,000 edges |
   |---:|---:|---:|
   | 10,000 | 860.594 ms | 1.00x |
   | 20,000 | 3.260 s | 3.79x |
   | 100,000 | 81.856 s | 95.12x |
   
   Increasing the label from 10,000 to 100,000 edges increases elapsed time by
   95.12x for a 10x increase in data, consistent with repeated per-edge
   sequential lookup approaching quadratic scaling. `REMOVE` uses the same
   shared update lookup path, but was not separately measured in this edge
   workload.
   
   I plan to submit a pull request implementing this validated hidden-CTID
   lookup while preserving the existing graphid lookup as a fallback.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to