Hi Parastoo,
Short answer: yes, a single Cypher query can use multiple workers, but all of
that parallelism is PostgreSQL's. AGE has no parallel execution mechanism of
its own.
Read-only Cypher lowers to ordinary PostgreSQL plan nodes over the label
tables, so the standard parallel machinery applies unchanged. On AGE 1.8.0 with
200k vertices:
EXPLAIN (COSTS OFF)
SELECT * FROM cypher('g', $$ MATCH (n:v) WHERE n.id > 100 RETURN count(n) $$)
AS (c agtype);
Finalize Aggregate
-> Gather
Workers Planned: 4
-> Partial Aggregate
-> Parallel Seq Scan on v n
Filter: (agtype_access_operator(...) > '100'::agtype)
Gather, Partial Aggregate and Parallel Seq Scan are all stock PostgreSQL nodes.
There is no AGE-specific parallel operator anywhere in the plan, which is why
your max_parallel_workers / max_parallel_workers_per_gather experiments moved
the needle: those GUCs are the only knobs involved.
Writable clauses are the boundary. CREATE, SET, DELETE and MERGE each execute
inside an AGE custom scan node, and all four custom paths are constructed with
parallelism explicitly disabled:
src/backend/optimizer/cypher_pathnode.c
create_cypher_create_path() :59-61
create_cypher_set_path() :95-97
create_cypher_delete_path() :135-137
create_cypher_merge_path() :178-180
each setting
cp->path.parallel_aware = false;
cp->path.parallel_safe = false;
cp->path.parallel_workers = 0;
So the same query with a write clause gets no Gather:
Custom Scan (Cypher Set)
-> Subquery Scan on cypher
-> Seq Scan on v n
Note the scan feeding the custom scan is also serial — a non-parallel-safe node
in the tree bars a Gather from being placed beneath it.
Practical summary:
* Read-only MATCH / WHERE / RETURN, and aggregates over them: parallelize
normally, subject to the usual PostgreSQL costing and GUCs.
* Anything containing CREATE / SET / DELETE / MERGE: serial, by construction.
* Function-level markings are mostly not the constraint: 334 of AGE's SQL
functions are PARALLEL SAFE and only 6 are PARALLEL UNSAFE. For the write
clauses it is the custom scan paths above, not the function markings, that
disable parallelism.
* The 6 PARALLEL UNSAFE functions do matter for reads, and three of them are
graph traversal entry points: age_vle (both overloads), age_shortest_path,
and age_all_shortest_paths (plus _agehash_self_test and age_reduce_transfn).
So a variable-length pattern like (a)-[:e*1..3]->(b), or either shortest
path SRF, forces the whole query serial even though it is read-only. The
age_vle declaration carries a literal "-- might be safe" comment next to the
marking, so this looks like conservatism rather than a known hazard.
If you are benchmarking, the cleanest way to attribute parallelism is exactly
what you were doing: EXPLAIN and look for a Gather. If it is there, it is
PostgreSQL's; AGE contributes no workers of its own.
Best,
Greg Felice