A redundant type(r) = 'P' check makes a statically typed relationship query about 6x slower
I found that adding WHERE type(r) = 'P' to a relationship already matched as [r:P] makes the query about six times slower. The condition is always true, and both queries return the same count.
- Apache AGE Version: 1.7.0
- PostgreSQL Version: 18.1
- Operating System: macOS 27.0 arm64
- Installation Method: official
apache/age Docker image
- API/Driver: PostgreSQL wire protocol / Psycopg 3.3.4
Steps to reproduce
-
Load AGE and create a fresh graph:
LOAD 'age';
SET search_path = ag_catalog, "$user", public;
SELECT ag_catalog.create_graph('relationship_type_guard_perf');
-
Create 20,000 relationships of type P:
SELECT * FROM ag_catalog.cypher('relationship_type_guard_perf', $$
UNWIND range(1, 20000) AS i
CREATE (:X {id: i})-[:P {id: i}]->(:Y {id: i})
RETURN count(*) AS created
$$) AS (created agtype);
-
Refresh the label statistics:
ANALYZE relationship_type_guard_perf."X";
ANALYZE relationship_type_guard_perf."Y";
ANALYZE relationship_type_guard_perf."P";
-
Run query A:
SELECT * FROM ag_catalog.cypher('relationship_type_guard_perf', $$
MATCH (:X)-[r:P]->(:Y)
RETURN count(*) AS c
$$) AS (c agtype);
-
Run query B, which only adds a condition already guaranteed by [r:P]:
SELECT * FROM ag_catalog.cypher('relationship_type_guard_perf', $$
MATCH (:X)-[r:P]->(:Y)
WHERE type(r) = 'P'
RETURN count(*) AS c
$$) AS (c agtype);
-
Prefix both queries with EXPLAIN (ANALYZE, BUFFERS) to inspect their plans.
Expected behavior
Both queries should return c = 20000. Since [r:P] already limits r to type P, query B should not need to rebuild each relationship value and check its type again.
Actual behavior
Both queries return c = 20000, but query B is consistently slower. Each independent instance ran 12 alternating comparisons, and query B was slower in all of them:
| Independent instance |
Query A |
Query B |
Slowdown |
| AGE 1.7.0, instance 1 |
9.43 ms |
53.86 ms |
5.71x |
| AGE 1.7.0, instance 2 |
8.13 ms |
52.45 ms |
6.45x |
The slow plan applies this filter while scanning the P relationship table:
Filter: (age_type(_agtype_build_edge(...)) = '"P"'::agtype)
The fast plan scans the same P table without that filter.
A redundant
type(r) = 'P'check makes a statically typed relationship query about 6x slowerI found that adding
WHERE type(r) = 'P'to a relationship already matched as[r:P]makes the query about six times slower. The condition is always true, and both queries return the same count.apache/ageDocker imageSteps to reproduce
Load AGE and create a fresh graph:
Create 20,000 relationships of type
P:Refresh the label statistics:
Run query A:
Run query B, which only adds a condition already guaranteed by
[r:P]:Prefix both queries with
EXPLAIN (ANALYZE, BUFFERS)to inspect their plans.Expected behavior
Both queries should return
c = 20000. Since[r:P]already limitsrto typeP, query B should not need to rebuild each relationship value and check its type again.Actual behavior
Both queries return
c = 20000, but query B is consistently slower. Each independent instance ran 12 alternating comparisons, and query B was slower in all of them:The slow plan applies this filter while scanning the
Prelationship table:The fast plan scans the same
Ptable without that filter.