Back openDesk Edu for a sovereign, open-source education β every vote counts.
Vote nowTeaser: Neo4j Virtual Graph lets you query PostgreSQL, MongoDB, BigQuery, Snowflake, and REST APIs through Cypher as if they were part of your knowledge graph β no ETL, no data movement, no duplication. This article explores the architecture (query compiler, connector agent, schema mapper), three practical query patterns (pure federated, hybrid physical/virtual, incremental materialisation), and production performance benchmarks.
In July 2026, Neo4j announced the public preview of Neo4j Virtual Graph β a capability that lets you query external data sources through the Cypher query language as if they were part of your Neo4j graph database. No data movement, no ETL pipelines, no duplication.
Virtual Graph extends the concept of graph federation β the ability to project a graph-shaped query interface over non-graph data stores. It connects to PostgreSQL, MySQL, MongoDB, Google BigQuery, Snowflake, and REST APIs, mapping their schemas to virtual node labels and relationship types that Cypher queries can traverse.
For teams managing enterprise knowledge graphs, this addresses a recurring friction point: the data that belongs in your graph already lives in operational databases, data warehouses, and SaaS APIs. Virtual Graph lets you query it in place, then materialise only what earns its place in the persistent graph.
A virtual knowledge graph presents a graph-shaped view of non-graph data without physically transforming or copying it. The concept builds on decades of research in ontology-based data access (OBDA) and virtual graph databases, but Neo4j's implementation is distinctive in two ways:
Full Cypher support β Queries are not translated into a limited subset of SQL. Virtual Graph compiles Cypher into native queries against the target system, supporting MATCH, OPTIONAL MATCH, aggregation, UNION, and subqueries.
Bi-directional federation β Queries can join virtual nodes (from external sources) with physical nodes (stored in Neo4j) in a single Cypher statement, enabling hybrid graph traversals.
Virtual Graph runs as a query compiler within the AuraDB Enterprise control plane, with a lightweight connector agent deployed in the customer's network:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Cypher Query β
β MATCH (c:Customer)-[:ORDERED]->(o:Virtual:Order) β
β WHERE c.region = 'EMEA' β
β RETURN c.name, o.total, o.date β
βββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββ
β Neo4j Virtual Graph Engine β
β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
β β Schema β β Query β β Result β β
β β Mapper βββΆβ Compiler βββΆβ Merger β β
β ββββββββ¬βββββββ ββββββββ¬βββββββ ββββββββ¬βββββββ β
β β β β β
β ββββββββΌβββββββ ββββββββΌβββββββ β β
β β Connector β β Optimiser β β β
β β Registry β β β’ Predicate β β β
β β β β pushdown β β β
β β β’ JDBC β β β’ Limit β β β
β β β’ MongoDB β β pushdown β β β
β β β’ BigQuery β β β’ Join β β β
β β β’ REST β β planning β β β
β βββββββββββββββ βββββββββββββββ β β
βββββββββββββββββββββββββββββββββββββββββββββΌβββββββββββ
β
ββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββ
β Customer Network β β
β βββββββββββββββββββββββββββββββββΌβββββββββββββββ β
β β Virtual Graph Connector Agent β β
β β β β
β β ββββββββββββ ββββββββββββ ββββββββββββ β β
β β βPostgreSQLβ β MongoDB β β REST API β ... β β
β β ββββββββββββ ββββββββββββ ββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Virtual Graph requires a semantic mapping that describes how source schemas translate into graph terms. Mappings are defined in YAML and stored in the Aura Console:
name: "ecommerce-federation"
sources:
- name: postgres-orders
type: jdbc
connection: "jdbc:postgresql://host:5432/orders"
mappings:
- source: postgres-orders
table: customers
node_label: Customer
columns:
id: { property: customerId, type: string }
name: { property: name }
email: { property: email }
region: { property: region }
- source: postgres-orders
table: orders
node_label: Order
properties:
id: { property: orderId }
total: { property: total, type: float }
date: { property: orderDate, type: datetime }
- source: postgres-orders
relationship:
from: customers
to: orders
type: ORDERED
foreign_key: orders.customer_id -> customers.id
Once mapped, the virtual schema appears in Neo4j's schema introspection:
CALL db.schema.virtual()
// Returns: Node labels: [Customer, Order, Product]
// Relationship types: [ORDERED, CONTAINS]
Query data entirely from external sources without involving the local graph:
MATCH (c:Customer)-[:ORDERED]->(o:Virtual:Order)
WHERE c.region = 'EMEA'
AND o.total > 1000
AND o.date >= date('2026-01-01')
RETURN c.name, count(o) AS orderCount, sum(o.total) AS totalSpend
ORDER BY totalSpend DESC
Virtual Graph compiles this into a SQL query against PostgreSQL, pushes down the predicate filters (region = 'EMEA', total > 1000, date >= '2026-01-01'), and returns only the aggregated result set.
Join virtual nodes from an external database with physical nodes stored in Neo4j:
MATCH (c:Virtual:Customer)-[:ORDERED]->(o:Virtual:Order)
MATCH (c)-[:HAS_PROFILE]->(p:Profile)
WHERE o.total > 5000
AND p.segment IN ['enterprise', 'strategic']
RETURN c.name, p.segment, count(o) AS largeOrders
Here Customer and Order are virtual (PostgreSQL), while Profile is a physical Neo4j node. The query engine partitions the query: it pushes the CustomerβOrder traversal to PostgreSQL, fetches the matching customer IDs, joins them against the in-graph Profile nodes, and merges results.
sequenceDiagram
participant Client as Cypher Client
participant VGE as Virtual Graph Engine
participant PG as PostgreSQL (Virtual)
participant Neo4j as Neo4j (Physical)
Client->>VGE: MATCH (c:Virtual:Customer)-[:ORDERED]->(o:Virtual:Order)
VGE->>PG: SELECT c.id, c.name FROM customers c WHERE c.region = 'EMEA'
PG-->>VGE: {id: 101, name: "Acme"}
VGE->>Neo4j: MATCH (c:Customer {id: 101})-[:HAS_PROFILE]->(p:Profile)
Neo4j-->>VGE: {segment: "enterprise"}
VGE-->>Client: Merged result: Customer + Profile
Virtual Graph supports CALL procedures for materialising subsets of virtual data into the physical graph:
// Identify high-value customers from the virtual layer
MATCH (c:Virtual:Customer)-[:ORDERED]->(o:Virtual:Order)
WITH c, sum(o.total) AS lifetimeValue
WHERE lifetimeValue > 100000
CALL graph.virtual.materialize(c, { label: "VIPCustomer" })
YIELD nodeId
MATCH (vip:VIPCustomer) WHERE id(vip) = nodeId
SET vip.lifetimeValue = lifetimeValue
This pattern lets teams start with a full virtual graph, then selectively materialise the most valuable subsets into Neo4j for graph-native features like GDS algorithms or vector indexes.
Virtual Graph's query compiler applies several optimisations:
| Optimisation | Description | Impact |
|---|---|---|
| Predicate pushdown | Filters applied at source before data transfer | ~10β100Γ reduction in data volume |
| Limit pushdown | LIMIT clauses pushed to source query | Avoids full table scans |
| Join planning | Determines optimal join order across sources | 2β5Γ faster multi-source joins |
| Result streaming | Results streamed, not buffered | Supports gigabyte-scale result sets |
| Connector caching | Schema metadata cached for 5 min | Eliminates repeated DESCRIBE calls |
In benchmarks against a 10 GB PostgreSQL instance with 50 million rows, Virtual Graph's predicate-pushed queries achieved 85β95% of native PostgreSQL query latency, while cross-source joins added 10β30ms of federation overhead.
Replace point-to-point ETL jobs with on-demand graph federation. Query your data lake (Snowflake, BigQuery) through Cypher without moving data into Neo4j:
MATCH (p:Virtual:Product)
WHERE p.category = 'semiconductor'
AND p.inventoryStatus = 'critical'
MATCH (s:Virtual:Supplier)-[:SUPPLIES]->(p)
RETURN s.name, p.name, p.leadTimeDays
ORDER BY p.leadTimeDays DESC
Join real-time operational data (PostgreSQL order system) with your knowledge graph (product ontology, customer segments) without duplicating operational data:
MATCH (o:Virtual:Order)-[:CONTAINS]->(p:Virtual:Product)
MATCH (p)-[:IS_A]->(cat:Category {name: 'HighValue'})
WHERE o.status = 'pending_fulfillment'
RETURN o.orderId, p.name, o.shippingAddress
Wrap legacy mainframe or COBOL data sources behind REST APIs, then map them as virtual graph nodes β no modifications to the legacy system required.
| Limitation | Details |
|---|---|
| Read-only | Virtual Graph supports queries only. Writes (CREATE, SET, DELETE) target physical nodes only. |
| Connector availability | JDBC (PostgreSQL, MySQL, MariaDB), MongoDB, BigQuery, Snowflake, REST. More connectors upcoming. |
| Predicate pushdown depth | Pushed down to a maximum of 3 JOINs per source; deeper joins computed in memory. |
| Transactionality | Each source participates in its own transaction scope. No distributed transactions across sources. |
| Schema changes | Source schema changes require remapping. CALL db.schema.virtual.refresh() updates cached metadata. |
| Feature | Neo4j Virtual Graph | Apache Calcite | Dremio | GraphQL Federation |
|---|---|---|---|---|
| Graph query model | β Cypher (native) | β SQL | β SQL | β οΈ GraphQL |
| Hybrid physical/virtual | β First-class | β | β | β |
| Predicate pushdown | β Deep (3 JOINs) | β Deep | β Deep | β οΈ Limited |
| Schema mapping | YAML declarative | SQL DDL | GUI | SDL |
| MCP support | β (via Aura MCP) | β | β | β |
| Deployment | AuraDB Enterprise | Self-hosted | Cloud/Self-hosted | Cloud/Self-hosted |
Neo4j Virtual Graph is available in public preview on AuraDB Enterprise. Enable it from the Aura Console under Database Settings β Virtual Graph.
Virtual: or configure an aliasCALL db.schema.virtual() to inspect available virtual entitiesDocumentation is live at neo4j.com/docs/aura/virtual-graph (July 2026).
Neo4j Virtual Graph redefines the boundary between the knowledge graph and the data sources it draws from. Instead of forcing a binary choice β ETL everything into the graph or query sources separately β Virtual Graph lets you project a graph over your existing data estate and materialise only what matters.
For organisations building enterprise knowledge graphs, this is a paradigm shift. The question is no longer "what data can I get into Neo4j?" but "what questions do I want to ask across all my data?" β and the graph model provides the answer.