MATCH (n) without a label pays PostgreSQL table-inheritance planning cost proportional to the total number of vertex labels in the graph
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 72/100
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:
- Document it — a line in the manual's
MATCHsection noting that an unlabelledMATCH
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. - Consider a
NOTICE/HINTwhen planning an unlabelledMATCHagainst a graph with more
than some threshold of labels, pointing at the label-count cost. - 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from apache/age
-
Difficulty 1/5 Under an hour Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
-
bug
Difficulty 3/5 1-2 days Newbie friendliness 82/100
Similar issues
-
level/task module/gcp type/bug
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
-
Difficulty 1/5 Under an hour Newbie friendliness 86/100
hapostgres/pg_auto_failover#1190 ·
-
docs
Difficulty 1/5 Under an hour Newbie friendliness 85/100
-
P3 sonic-vpp
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
sonic-net/sonic-buildimage#29662 ·
-
Difficulty 1/5 Under an hour Newbie friendliness 94/100
spack/spack-packages#6586 ·