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, ADD 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) 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 / ADD GRAPH / CONNECT | ✓ | ✗ | ✗ |
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
Before querying across graphs, register them in the catalog using ADD GRAPH or CREATE GRAPH:
-- Register existing graphs on different backends
ADD GRAPH social ON CONNECTOR neo4j_prod GRAPH neo4j;
ADD GRAPH events ON CONNECTOR clickhouse_logs READ ONLY;
ADD GRAPH warehouse ON CONNECTOR postgres_dw MAPPING company_mapping READ ONLY;
ADD GRAPH dev ON CONNECTOR memgraph_dev;Each graph entry stores:
- Name: Local catalog identifier
- Connector: Which connector hosts this graph
- Remote graph: Backend-specific graph/database name (optional)
- Mapping: Schema mapping for relational connectors (optional)
- Access mode:
READ WRITE(default) orREAD ONLY
Single Graph Queries
Once registered, use graphs by name without specifying connectors:
-- Query the social graph (resolves to neo4j_prod)
USE social MATCH (p:Person) RETURN p.name;
-- Query the events graph (read-only ClickHouse)
USE events MATCH (e:Event) WHERE e.ts > '2025-01-01' RETURN e;
-- Query the warehouse with mapping translation
USE warehouse MATCH (c:Company) WHERE c.revenue > 1000000 RETURN c;The engine resolves the graph name to its bound connector and executes the query on the appropriate backend.
USE-free 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) — 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. - Session defaults never tie-break. A sticky
default_connection(fromSET DEFAULT CONNECTION) does not resolve an otherwise-ambiguous query. - No-signal queries like
MATCH (n) RETURN n(no label, rel-type, or property to route by) fall back to today’s default-connection behavior unchanged.
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.
USE-free 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
existing cross-backend hash 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 USE-free 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;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 both registers the graph and creates it on the remote backend:
-- Create an empty graph on a specific backend
CREATE GRAPH analytics ON CONNECTOR memgraph_dev;
-- Create with a GQL schema
CREATE GRAPH typed_graph {
NODE Person ({name STRING, age INT}),
EDGE KNOWS ()-[]->()
} ON CONNECTOR neo4j_prod;Modifying Graphs
-- Change access mode
ALTER GRAPH social SET READ ONLY;
ALTER GRAPH social SET READ WRITE;
-- Rebind to a different connector (e.g., failover)
ALTER GRAPH social SET CONNECTOR neo4j_staging;
-- Change the remote graph name
ALTER GRAPH social SET GRAPH production_db;
-- Attach or change a mapping
ALTER GRAPH knowledge SET MAPPING updated_mapping;
ALTER GRAPH knowledge REMOVE MAPPING;Removing Graphs
-- Remove from catalog only (remote graph is untouched)
UNADD GRAPH social;
UNADD GRAPH IF EXISTS social;
-- Remove from catalog AND drop on backend
DROP GRAPH analytics;
DROP GRAPH IF EXISTS analytics;Cross-Graph Query Rules
-
No pushed joins across backends: Cross-graph joins always materialize both sides locally and hash-join. The engine never pushes join operations across different backends.
-
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 USE-free 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. Register graphs
ADD GRAPH social ON CONNECTOR neo4j_prod GRAPH neo4j;
ADD GRAPH warehouse ON CONNECTOR pg_warehouse MAPPING company_mapping READ ONLY;
ADD GRAPH events ON CONNECTOR ch_logs 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
UNADD GRAPH social;
UNADD GRAPH warehouse;
UNADD GRAPH events;