MATCH (n) without a label pays PostgreSQL table-inheritance planning cost proportional to the total number of vertex labels in the graph

Open
#2,575 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
72/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
c, postgresql, sql
Domain
databases

Research direction

Start by reading src/backend/parser/cypher_clause.c:7125-7141 and run the supplied N-label SQL benchmark with EXPLAIN (ANALYZE, BUFFERS). Confirm how planning time changes for labelled and unlabelled MATCH queries, then inspect the manual's MATCH section. Done should clearly document the label-count planning cost, or provide the agreed warning if that scope is chosen.

Written by the indexing model from the issue text.

Description

Environment: Apache AGE 1.7.0, PostgreSQL 17. Graph with ~45,700 vertices (label
:Function among others) and ~139,000 :CALLS edges, on the order of a few dozen distinct
vertex labels total. Standard btree expression indexes present on the per-label child tables.

Symptom

An unlabelled MATCH (n) is dramatically more expensive to plan than the same pattern with
a label, even when both return comparable row counts and neither touches many rows at
execution time:

MATCH (n) RETURN n LIMIT 1

Planning Time: 133.367 ms

MATCH (n:Function) RETURN n LIMIT 1

Planning Time: 0.312 ms

That's 427x, and it's entirely in planning, not execution — the execution time for both
is negligible by comparison. This is easy to miss in practice because Cypher hides the
relational model underneath, so nothing about the query looks like it should be
label-count-sensitive.

Mechanism

Read in source (src/backend/parser/cypher_clause.c:7125-7141, AGE 1.7.0): when a MATCH
pattern has no label, the generated RTE points at the parent table (_ag_label_vertex for
nodes, the equivalent for edges) with inheritance active (inh = true). Every vertex label is
implemented as a PostgreSQL child table of that parent (<graph>."<Label>"), so with
inheritance active the planner has to expand and cost every child table — and every index on
every child table — during planning. With a label given, the RTE points directly at that one
child table, no expansion needed.

This is not an AGE executor bug — it's standard PostgreSQL inheritance-planning behavior,
working as designed. The gap is that nothing surfaces this cost to a Cypher user before they
hit it: an unlabelled MATCH (n) reads as a small, generic query, but its planning cost scales
with the total number of labels defined in the graph, independent of how selective the
pattern is or how many rows match.

Minimal reproduction (anyone can run this)
SELECT create_graph('label_scale_bench');

-- create N distinct vertex labels and insert a handful of vertices into each
DO $$
BEGIN
  FOR i IN 1..50 LOOP
    PERFORM create_vlabel('label_scale_bench', format('L%s', i));
    EXECUTE format(
      $q$SELECT * FROM cypher('label_scale_bench', $c$ CREATE (:L%s {v: 1}) $c$) AS (v agtype)$q$,
      i
    );
  END LOOP;
END $$;

-- compare planning time, unlabelled vs labelled
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM cypher('label_scale_bench', $$ MATCH (n) RETURN n LIMIT 1 $$) AS (n agtype);

EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM cypher('label_scale_bench', $$ MATCH (n:L1) RETURN n LIMIT 1 $$) AS (n agtype);

Repeat with N = 10, 50, 100, 200 labels and chart Planning Time for the unlabelled form against
N. We'd expect it to grow with the label count (at least linearly, likely worse once per-child
index stats get pulled in); the labelled form should stay flat.

Suggested fix (cheap end first)

This doesn't need an executor rewrite to be worth fixing:

  1. Document it — a line in the manual's MATCH section noting that an unlabelled MATCH
    scans/plans over every vertex (or edge) label in the graph, and that this cost grows with
    the number of distinct labels, would let people opt into labelling on purpose instead of
    discovering it via a planning-time cliff.
  2. Consider a NOTICE/HINT when planning an unlabelled MATCH against a graph with more
    than some threshold of labels, pointing at the label-count cost.
  3. Longer term, this is presumably related to how much PostgreSQL's own inheritance-planning
    cost can be reduced (partition pruning heuristics, constraint_exclusion, etc.) — but that's
    a much bigger scope than this issue is asking for.

Happy to run the N-label benchmark script above and post the growth curve if that'd help
prioritize this.

Dominant language
C
Stars
4.8k
Forks
525
Avg merge
11h 57m
Merged PRs (30d)
4

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from apache/age

All issues in apache/age

Similar issues

More C issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.