Multiple Graphs & Composite Queries
MemGQL introduces a graph catalog that makes graphs first-class entities. Rather than specifying connectors and connections in every query, you register graphs once and reference them by name. This enables seamless multi-graph queries across heterogeneous backends using standard ISO GQL composite clauses.
If you want a running stack to try these queries against, the Docker Compose - Complete Setup guide spins up MemGQL with Memgraph and PostgreSQL backends already wired up, so you can follow along here against real data.
Where the catalog DSL works
The catalog statements (ADD CONNECTOR, CREATE GRAPH, SHOW GRAPHS, SHOW CONNECTORS, DROP GRAPH, USE <graph>, …) are available in CONNECTOR_TYPE=multi mode. Single-backend modes (memgraph-gql, neo4j-gql, postgres, mysql, oracle, duckdb, clickhouse, iceberg, pinot, snowflake) connect to one backend configured via env vars and don’t expose the catalog.
| Statement | multi | Cypher single-backend | SQL single-backend |
|---|---|---|---|
SHOW GRAPHS / SHOW CONNECTORS / SHOW MAPPINGS | ✓ | ✗ | ✗ |
SHOW SCHEMA / REFRESH SCHEMA | ✓ | ✗ | ✗ |
ADD CONNECTOR / CREATE GRAPH … FROM / PING | ✓ | ✗ | ✗ |
CREATE GRAPH <x> | ✓ (catalog entry) | ✓ (forwarded as CREATE DATABASE <x>) | ✗ |
DROP GRAPH <x> | ✓ | ✗ | ✗ |
TRUNCATE <x> | ✓ | ✓ (forwarded as MATCH (n) DETACH DELETE n) | ✗ |
USE <graph> … | ✓ (routes via catalog) | ✓ (treated as backend database) | ✗ |
If you need graph management today, run MemGQL in multi mode.
Graph Registration
Register connectors first, then define graphs over them. A connector is a
connection; a graph is a mapping (vertices + edges) laid over one or more
connectors.
-- 1. Register connectors (connections only)
ADD CONNECTOR neo4j_prod TYPE neo4j URI 'bolt://neo4j:7687' USER 'neo4j' PASSWORD 'secret' GRAPH neo4j;
ADD CONNECTOR clickhouse_logs TYPE clickhouse URI 'http://clickhouse:8123';
ADD CONNECTOR postgres_dw TYPE postgres URI 'postgresql://pg:5432/dw';
-- 2. Define graphs over them (mapping body from a file, or inline JSON)
CREATE GRAPH social FROM FILE '/data/social.graph.json';
CREATE GRAPH events FROM FILE '/data/events.graph.json';
CREATE GRAPH warehouse FROM FILE '/data/warehouse.graph.json';
-- read-only access where you want it
ALTER GRAPH events SET READ ONLY;
ALTER GRAPH warehouse SET READ ONLY;Each graph carries a name, its per-connector mapping (vertices / edges; see
the Schema File page), and an access mode
(READ WRITE default, or READ ONLY). A single graph may span several
connectors.
Single Graph Queries
Reference a label and the query routes to the graph that defines it, with no
connector or USE clause needed:
MATCH (p:Person) RETURN p.name;
MATCH (e:Event) WHERE e.ts > '2025-01-01' RETURN e;
MATCH (c:Company) WHERE c.revenue > 1000000 RETURN c;The engine resolves each label to its owning graph and executes the query on the appropriate backend.
Routing
You don’t always have to name the graph. MemGQL maintains a unified schema
index (the labels, relationship types, and properties every registered source
defines) and uses it to infer which backend a query belongs to from the query
itself. A query with no USE clause routes automatically when its schema
signals point to exactly one source.
-- `name` is defined only on the warehouse's Person table → routes to warehouse
MATCH (p:Person) RETURN p.name;
-- FRIEND_OF exists only in the social graph → routes to social
MATCH (me:Person {id: 1})-[:FRIEND_OF]->(f:Person) RETURN f.id;The schema index is built from two sources:
- SQL-family connectors (PostgreSQL, MySQL, Oracle, DuckDB, ClickHouse, Iceberg, Pinot, Snowflake): taken from the registered mapping (labels, rel-types, and properties are known exactly).
- Cypher-family connectors (Memgraph, Neo4j): introspected at
CONNECTtime and cached. Memgraph usesSHOW SCHEMA INFO(the server must run with--schema-info-enabled); Neo4j usesdb.schema.*, falling back to a labels-and-rel-types-only view. When property names can’t be introspected, a source can still be selected by its labels/rel-types but is never excluded by a property it might have.
Routing policy
Routing is strict and deterministic:
- Exactly one candidate → the query routes there.
- Zero candidates → a hard error naming the sources that were checked and
pointing you at
SHOW SCHEMA. - Two or more candidates → an ambiguous hard error listing the candidates.
Disambiguate by referencing a property or relationship type that exists in
only one of them, or add an explicit
USE. - Explicit
USEalways wins and bypasses inference entirely. - No-signal queries (
MATCH (n) RETURN nwith no label, relationship type, or property to route by) have nothing to infer from; pin them with an explicitUSE <graph>.
Identifier matching is exact: :person does not match a mapping (or
introspected schema) that declares Person. Routing, translation, and the
backends all agree on the same casing.
Federated queries
Routing happens per query part, so multi-backend joins and composites work
without any USE clauses; each part (or branch) routes independently and the
cross-backend join takes over at the boundary:
-- Federated JOIN with zero USE clauses: the FRIEND_OF part routes to social,
-- the ORDERED part routes to the transactional backend, joined on f.id = fp.id
MATCH (me:Person {id: 1})-[:FRIEND_OF]->(f:Person)
MATCH (fp:Person)-[:ORDERED]->(prod:Product) WHERE fp.id = f.id
RETURN fp.name AS friend, prod.name AS item;
-- Federated UNION: each branch routes on its own signals
MATCH (p:Person)-[:ORDERED]->(prod:Product) WHERE p.id = 1 RETURN prod.id
UNION
MATCH (:Product {id: 1})-[:SIMILAR_TO]->(s:Product) RETURN s.id;Writes (INSERT) route the same way and require a unique candidate. A
successful INSERT also teaches the schema cache the labels it wrote, so a
follow-up read routes without needing a REFRESH SCHEMA.
What still needs an explicit USE
- A graph bound to a non-default remote database (Memgraph multi-tenancy) is
fenced to explicit
USE; per-tenant introspection isn’t wired up yet. - A single
MATCHpattern that spans two backends errors with guidance to split it into oneMATCHclause per graph; auto-splitting one pattern is out of scope. - A label-less
MATCH (n)against a SQL backend still surfaces a raw backend error (there’s nothing to route or translate by).
Schema discovery
SHOW SCHEMA is the discoverability surface for routing; it’s the
unified index the router consults, and what routing errors point you at. It’s
especially useful for agents that need to learn what’s queryable without
hardcoded knowledge.
SHOW SCHEMA;+----------------+---------+-----------+--------------------------+--------+---------+
| source | element | name | properties | from | to |
+----------------+---------+-----------+--------------------------+--------+---------+
| transactional | node | Person | id, name | — | — |
| transactional | node | Product | id, name, price | — | — |
| transactional | edge | ORDERED | quantity | Person | Product |
| social | node | Person | (not introspected) | — | — |
| social | edge | FRIEND_OF | (not introspected) | Person | Person |
+----------------+---------+-----------+--------------------------+--------+---------+Each row is one label (element = node) or relationship type
(element = edge) defined by a source. properties lists the known property
names, or (not introspected) when a Cypher backend exposed only labels and
rel-types. from / to are the endpoint labels for relationships. Sources with
no schema information at all are listed with a (no schema info: …) reason so
you know to query them with an explicit USE.
Filter to a single source:
SHOW SCHEMA FOR social;REFRESH SCHEMA re-introspects every live Cypher connection (Memgraph / Neo4j)
and updates the cache. Introspection already runs eagerly at CONNECT; use
this after the underlying schema changes:
REFRESH SCHEMA;+-------------+-------------+--------------------------------------------+
| connection | connector | status |
+-------------+-------------+--------------------------------------------+
| social_conn | mg_social | refreshed (3 labels, 2 relationship types) |
| tx_conn | pg_tx | mapping-backed (nothing to introspect) |
+-------------+-------------+--------------------------------------------+SQL-family connections report mapping-backed (nothing to introspect); their
schema comes from the mapping, not from a live query. If introspection fails
(for example, a Memgraph started without --schema-info-enabled), the status
explains why; the connection stays reachable via explicit USE.
Focused Multi-Graph Queries
A single linear query can chain multiple USE clauses. Variables bind across parts, enabling joins across backends:
-- Find people in the social graph who work at high-revenue companies in the warehouse
USE social
MATCH (p:Person)-[:WORKS_AT]->(c:Company)
USE warehouse
MATCH (co:Company)
WHERE co.name = c.name AND co.revenue > 1000000
RETURN p.name AS person, c.name AS company, co.revenue AS revenue;Execution flow:
- The
USE socialpart executes onneo4j_prod, returning(p, c)bindings - The
USE warehousepart executes onpostgres_dw, usingc.namevalues as filters - Results are joined locally and returned
Another example correlating social connections with event logs:
-- Find friends of Alice who made purchases
USE social
MATCH (alice:Person {name: 'Alice'})-[:KNOWS]->(friend:Person)
USE events
MATCH (e:Event)
WHERE e.user_email = friend.email AND e.type = 'purchase'
RETURN friend.email AS email, e.item AS item, e.amount AS amount;Piping: fetching only what joins
Since v0.7.0, when one side of a two-backend query is selective (it carries a
literal property filter, a LIMIT, or a bounded CALL), MemGQL runs that side
first and uses its join keys to fetch only the matching rows from the other
backend, instead of pulling the whole table:
-- {community: 42} makes the Memgraph side selective; only the matching
-- profiles are fetched from Postgres.
MATCH (u:User {community: 42})
MATCH (p:Profile {uid: u.uid})
RETURN p.name, u.rank ORDER BY u.rank DESC;Piping is an optimization, not a semantic change; results are identical with
or without it. It engages for backends registered as catalog graphs
(via CREATE GRAPH); when it doesn’t apply, MemGQL pulls both sides and joins
locally. If the selective side matches nothing, the other backend is never
queried.
Composite Queries Across Graphs
The GQL standard defines composite expressions combining query branches with UNION, INTERSECT, and EXCEPT. Each branch can target a different graph.
UNION
Combine results from multiple graphs:
-- All people from both production and dev environments
USE social MATCH (p:Person) RETURN p.name AS name, p.email AS email
UNION
USE dev MATCH (p:Person) RETURN p.name AS name, p.email AS email;
-- Events from ClickHouse combined with activity logs from Postgres
USE events
MATCH (e:Event) WHERE e.ts > '2025-06-01'
RETURN e.type AS activity, e.user_email AS user, e.ts AS timestamp
UNION ALL
USE warehouse
MATCH (a:ActivityLog) WHERE a.date > '2025-06-01'
RETURN a.action AS activity, a.employee_email AS user, a.date AS timestamp;INTERSECT
Find entities present in both graphs:
-- People who exist in both production and dev
USE social MATCH (p:Person) RETURN p.email AS email
INTERSECT
USE dev MATCH (p:Person) RETURN p.email AS email;
-- Companies appearing in both social graph and data warehouse
USE social
MATCH (c:Company) RETURN c.name AS company_name
INTERSECT
USE warehouse
MATCH (c:Company) RETURN c.name AS company_name;EXCEPT
Find entities in one graph but not another:
-- People in production not yet migrated to dev
USE social MATCH (p:Person) RETURN p.email AS email
EXCEPT
USE dev MATCH (p:Person) RETURN p.email AS email;
-- Events in ClickHouse with no matching warehouse activity log
USE events
MATCH (e:Event) WHERE e.type = 'purchase'
RETURN e.user_email AS user, e.ts AS timestamp
EXCEPT
USE warehouse
MATCH (a:ActivityLog) WHERE a.action = 'purchase'
RETURN a.employee_email AS user, a.date AS timestamp;Mixed Composites
Combine multiple composite operations:
-- People in production OR dev, but NOT in the warehouse's inactive list
USE social MATCH (p:Person) RETURN p.email AS email
UNION
USE dev MATCH (p:Person) RETURN p.email AS email
EXCEPT
USE warehouse
MATCH (p:Person) WHERE p.status = 'inactive'
RETURN p.email AS email;Graph Introspection
View all registered graphs:
SHOW GRAPHS;+------------+---------------+----------------+--------------+------------------+---------------+---------------+------------+
| name | connector | connector_type | remote | mapping | mapping_nodes | mapping_edges | access |
+------------+---------------+----------------+--------------+------------------+---------------+---------------+------------+
| social | neo4j_prod | neo4j | neo4j | — | — | — | READ WRITE |
| events | ch_logs | clickhouse | — | — | — | — | READ ONLY |
| warehouse | pg_warehouse | postgres | — | company_mapping | 2 | 2 | READ ONLY |
| dev | mg_dev | memgraph | — | — | — | — | READ WRITE |
+------------+---------------+----------------+--------------+------------------+---------------+---------------+------------+Filter by connector:
SHOW GRAPHS ON CONNECTOR neo4j_prod;View details for a specific graph:
SHOW GRAPH social;SHOW GRAPHS lists how graphs are registered (connector, mapping, access
mode). To see what each one actually defines (the labels, relationship types,
and properties used for routing), use SHOW SCHEMA.
Graph Lifecycle Management
Creating graphs
CREATE GRAPH … FROM registers a graph from a mapping body (inline JSON or a
file) and auto-connects the connectors it references:
-- from a file
CREATE GRAPH analytics FROM FILE '/data/analytics.graph.json';
-- or inline
CREATE GRAPH social FROM '{
"vertices": [ { "label": "User", "mappedGraphSource": { "connector": "neo4j_prod" } } ],
"edges": [ { "label": "FOLLOWS", "from": "User", "to": "User", "mappedGraphSource": { "connector": "neo4j_prod" } } ]
}';See the Schema File page for the mapping body format.
Modifying graphs
-- Change access mode
ALTER GRAPH social SET READ ONLY;
ALTER GRAPH social SET READ WRITE;A graph’s mapping is fixed at creation. To change it, drop and re-create the
graph with the new body (DROP GRAPH, then CREATE GRAPH … FROM).
Removing graphs
DROP GRAPH social;
DROP GRAPH IF EXISTS social;DROP GRAPH removes the graph from the catalog; the backend data is untouched.
Connectors are removed separately with DROP CONNECTOR (refused while a graph
still references the connector).
Caching a graph in Memgraph
Since v0.7.0, a graph can be cached in a Memgraph instance. This is aimed at data-warehouse connectors (Iceberg first) where a table is too large to materialize wholesale and every federated query otherwise re-scans it from object storage. The cache is query-driven: it is populated by execution itself (populate-on-read), so only the subgraph your queries actually touch lands in Memgraph.
Register a Memgraph connector to hold the cached data, then mark a graph
cache-enabled with ALTER GRAPH … SET CACHE:
-- A Memgraph instance (storage mode IN_MEMORY_ANALYTICAL) holds the cache
ADD CONNECTOR mg TYPE memgraph URI 'memgraph:7687';
-- A warehouse graph that reads from Iceberg (connectors registered earlier)
CREATE GRAPH events FROM FILE '/data/events.graph.json';
-- Enable caching in `mg`, with optional bounds
ALTER GRAPH events SET CACHE CONNECTOR mg TTL 3600 MAX_BYTES 8G;
-- Disable caching (and drop the cached fragments)
ALTER GRAPH events REMOVE CACHE;The cache connector must be a Memgraph-type connector. Re-issuing SET CACHE
drops any fragments already recorded, so re-pointing the cache never serves a
stale result.
TTL is in seconds; MAX_BYTES accepts a plain byte count or a K/M/G/T
binary suffix (8G = 8 × 1024³ bytes).
The cache engages per routed part, whether the part is pinned with USE or
routed by its labels:
-
First read (cache MISS): the query runs on the source and the engine tees the scanned labels and edge types into the Memgraph cache, recording which scopes it cached.
-
Later covered read (cache HIT): a query whose scans are covered by existing cached scopes is served entirely from Memgraph, with no source I/O.
-
Capability upgrade: because cached data is a real graph in Memgraph, queries over cached scopes can use Memgraph’s full capability profile (variable-length paths, quantified patterns, graph algorithms) even when the source connector cannot express them:
-- iceberg-direct cannot execute a variable-length expand; against the -- Memgraph-resident cache of the same data it just works USE events MATCH (a:Event) (-[:SPAWNED]->())+ (b:Event) RETURN a.event_id, b.event_id;
In a cross-backend query (split by explicit USE, or routed by label), each
cache-enabled part is served from its cache independently while the other parts
run on their own sources.
Inspect what is cached with SHOW GRAPH CACHES (returns each cache-enabled
graph, its cache connector, TTL, MAX_BYTES, and the number of cached
fragments). Cache activity is logged at DEBUG as [CACHE] MISS … teeing …
and [CACHE] HIT … serving from cache connector ….
Note: The query-driven cache is currently validated with Iceberg sources; the mechanism generalizes to the other warehouse connectors.
Cross-Graph Query Rules
-
No pushed joins across backends: The join itself always executes locally; the engine never pushes join operations across different backends. (Since v0.7.0, piping may push the selective side’s join keys as a filter so the other side materializes only matching rows.)
-
Property-based join keys: Join keys must use node/edge properties. Internal IDs are backend-specific and not comparable across connectors.
-
Single-graph writes: Mutations in multi-graph queries must target a single graph. The engine rejects mutations spanning multiple backends in one statement.
-
Compatible column types: All branches of a composite query must produce result sets with the same number of columns, in the same order, with compatible types.
-
Routing is per part: With routing each query part (and each composite branch) routes on its own schema signals. A part that matches zero or multiple sources is a hard error; add an explicit
USEor reference a label/property unique to one source.
Complete Example
Set up multiple backends and run cross-graph queries:
-- 1. Configure connectors
ADD CONNECTOR neo4j_prod TYPE neo4j URI 'bolt://neo4j:7687' USER 'neo4j' PASSWORD 'secret';
ADD CONNECTOR pg_warehouse TYPE postgres URI 'postgresql://pg:5432/dw';
ADD CONNECTOR ch_logs TYPE clickhouse URI 'http://clickhouse:8123';
-- 2. Define graphs over the connectors (mapping bodies live in files)
CREATE GRAPH social FROM FILE '/data/social.graph.json';
CREATE GRAPH warehouse FROM FILE '/data/warehouse.graph.json';
CREATE GRAPH events FROM FILE '/data/events.graph.json';
ALTER GRAPH warehouse SET READ ONLY;
ALTER GRAPH events SET READ ONLY;
-- 3. Single-graph queries
USE social MATCH (p:Person) RETURN p.name LIMIT 5;
USE events MATCH (e:Event) RETURN e.type, COUNT(*) GROUP BY e.type;
-- 4. Multi-graph join
USE social
MATCH (p:Person)-[:WORKS_AT]->(c:Company)
USE warehouse
MATCH (wc:Company)
WHERE wc.name = c.name AND wc.revenue > 1000000
RETURN p.name, c.name, wc.revenue;
-- 5. Composite across graphs
USE social MATCH (p:Person) RETURN p.email AS email
UNION
USE warehouse MATCH (e:Employee) RETURN e.email AS email;
-- 6. Cleanup
DROP GRAPH social;
DROP GRAPH warehouse;
DROP GRAPH events;