PostgreSQL 19 Graph Database — SQL/PGQ Explained
PostgreSQL 19 Is Now a Graph Database Too
For years, every team running Postgres hit the same wall. Your data starts simple — users, orders, products, neat rows in neat tables. Then the product grows and suddenly the questions become graph-shaped: which users are connected to this user? What does this customer's purchase network look like? Which accounts share a device with a flagged account? And relational SQL, which handled everything gracefully until now, starts fighting you.

The traditional answer was painful: either torture PostgreSQL with recursive CTEs and five-level self-joins, or stand up a dedicated graph database like Neo4j next to your Postgres — and then live with two databases, two query languages, and a sync pipeline between them that breaks at 3 a.m.
PostgreSQL 19 ends that trade-off. The upcoming release — Beta 1 shipped June 4, 2026, with general availability expected around September–October 2026 — implements SQL/PGQ (SQL Property Graph Queries), the graph query part of the ISO SQL:2023 standard. In plain terms: you can now define a graph on top of the tables you already have and query it with graph pattern-matching syntax, natively, inside Postgres. No extension. No new storage engine. No data migration.
If you run a SaaS on Postgres — I do, Peekwise.ai among them — this is one of the most consequential Postgres features in years. Here is a direct comparison before we go deeper:
| PostgreSQL 19 (SQL/PGQ) | Neo4j | Apache AGE | |
|---|---|---|---|
| Query language | SQL + GRAPH_TABLE (ISO standard) | Cypher (proprietary, openCypher) | openCypher via extension |
| Storage | Your existing tables | Native graph store | Postgres tables (extension-managed) |
| Data migration needed | None | Full ETL from Postgres | Import into AGE's graph format |
| Transactions with relational data | Same transaction, same database | Cross-system consistency problem | Same database |
| Variable-length paths | Not yet (first release) | Yes, mature | Yes |
| Graph algorithms (PageRank etc.) | No built-in library | GDS library, very mature | Limited |
| Operational cost | Zero new infrastructure | Second cluster to run, back up, secure | One extension to maintain |
The verdict in one sentence: if your data already lives in Postgres and your graph questions are bounded (2–5 hops, known shapes), SQL/PGQ removes every reason to leave; if you need deep variable-length traversals or graph algorithms at scale, Neo4j still earns its place.
The Mental Model: A Graph Is a Lens, Not a Copy
The single most important thing to understand about SQL/PGQ is what it does not do. It does not copy your data into a new graph structure. It does not add a graph storage engine. A property graph in PostgreSQL 19 is a lens over tables you already have — internally it is processed much like a view, and every graph query is rewritten into an ordinary relational query before execution.
Think about what your schema already is. A customers table and an orders table connected by a foreign key — that is a graph. Customers are nodes. Orders are nodes. The foreign key relationship is an edge. Your schema has been a graph all along; SQL just never gave you a way to say so.
CREATE PROPERTY GRAPH is the declaration that makes this mapping official: these tables are my vertices, these tables (or foreign keys) are my edges. GRAPH_TABLE is then the function that lets you traverse that mapping with pattern syntax instead of JOIN chains. Under the hood, PostgreSQL rewrites the pattern into the same joins you would have written by hand — the planner, the indexes, MVCC, backups, replication, everything you already trust keeps working unchanged.
This is the exact opposite of the Neo4j model, where the graph is the storage: nodes physically store pointers to their neighbors (index-free adjacency). That design makes deep traversals fast, but it also means your data has to physically live there — which is why adopting Neo4j always meant a migration and a sync pipeline.
The Implementation: CREATE PROPERTY GRAPH and GRAPH_TABLE
Two new pieces of syntax carry the whole feature. First, you declare the graph once, as a schema object:
CREATE PROPERTY GRAPH myshop
VERTEX TABLES (
products,
customers,
orders
)
EDGE TABLES (
order_items SOURCE orders DESTINATION products,
customer_orders SOURCE customers DESTINATION orders
);PostgreSQL infers the wiring from your primary keys and foreign keys. If your schema doesn't have clean foreign keys (plenty of production schemas don't), you can spell the joins out explicitly:
CREATE PROPERTY GRAPH myshop
VERTEX TABLES (
customers KEY (customer_id),
orders KEY (order_id)
)
EDGE TABLES (
customer_orders KEY (customer_orders_id)
SOURCE KEY (customer_id) REFERENCES customers (customer_id)
DESTINATION KEY (order_id) REFERENCES orders (order_id)
);Then you query it with GRAPH_TABLE and a MATCH pattern. The pattern syntax will look familiar to anyone who has seen Cypher — nodes in parentheses, edges in square brackets, arrows for direction:
SELECT customer_name
FROM GRAPH_TABLE (myshop
MATCH (c IS customers)-[IS customer_orders]->(o IS orders
WHERE o.ordered_when = current_date)
COLUMNS (c.name AS customer_name)
);Read it out loud: match a customer, follow a customer_orders edge, arrive at an order placed today, give me the customer's name. That query is exactly equivalent to this relational version — and PostgreSQL literally rewrites it into this before executing:
SELECT customers.name
FROM customers
JOIN customer_orders USING (customer_id)
JOIN orders USING (order_id)
WHERE orders.ordered_when = current_date;Two hops, the graph version is a convenience. But push to a real graph question — customers who bought products that were also bought by customers who bought product X — and the join version becomes a wall of aliases (customers c1, orders o1, order_items oi1, products p, order_items oi2, orders o2, customers c2) that nobody can review confidently. The graph version stays readable:
SELECT other_customer
FROM GRAPH_TABLE (myshop
MATCH (c1 IS customers WHERE c1.customer_id = 42)
-[IS customer_orders]->(IS orders)
-[IS order_items]->(p IS products)
<-[IS order_items]-(IS orders)
<-[IS customer_orders]-(c2 IS customers)
COLUMNS (c2.name AS other_customer)
);The pattern is the documentation. That readability gain compounds in code review, onboarding, and 2 a.m. debugging in ways that are hard to overstate.
One more detail worth knowing: labels are a mapping layer, not table names. You can give one table multiple labels (customers LABEL customer LABEL person), rename properties (employee_name AS name), and expose the same physical tables through several different logical graphs. Same rows, multiple lenses.
The Failure Modes: What Breaks and What's Missing
This is a first-release feature, and pretending otherwise would be dishonest. Know these before you build on it.
No variable-length paths yet. This is the big one. SQL/PGQ as a standard supports quantified patterns — "follow this edge 1 to 5 times" or "find any path between these nodes." PostgreSQL 19's initial implementation does not. Every hop must be written explicitly. That means shortest-path, transitive closure, and "friends-of-friends to arbitrary depth" still require recursive CTEs. Variable-length path support is expected in a future release, but if unbounded traversal is your core workload today, PostgreSQL 19 doesn't cover it yet.
Vertex tables need primary keys, and default edge inference needs foreign keys. If you point CREATE PROPERTY GRAPH at a schema with missing constraints, graph creation fails. The explicit KEY ... REFERENCES syntax is your escape hatch, but it means the "just works" experience assumes a well-constrained schema. Legacy schemas with implicit relationships will need the verbose form.
Shared labels demand matching properties. When two tables share a label (say, customers and employees both labeled person), the exposed properties must match in number, name, and type. Mismatch throws at definition time. This bites hardest when you evolve one table later — an ALTER TABLE on employees can break a property graph definition you wrote months earlier.
Reserved words trip people constantly. The most natural table name in graph modeling — order — is an SQL keyword. MATCH (o IS "order") with quotes, every time. Trivial, but it will be the first error message half of us hit.
It's still beta. Feature details can change before GA. Test on Beta releases, but don't ship production dependencies on syntax that could shift before September.
The Tradeoffs: When Neo4j Still Wins
Being honest about where the line sits is what makes the "you don't need to migrate" claim credible.
Choose PostgreSQL 19 SQL/PGQ when your data already lives in Postgres, your graph queries have known shapes and bounded depth (fraud-ring checks, recommendation hops, org-chart lookups, dependency checks 2–5 levels deep), and the thing you value most is one system — one backup story, one security surface, transactional consistency between your relational writes and your graph reads. A graph query in Postgres sees the row you committed a millisecond ago. A synced Neo4j sees it whenever the pipeline catches up.
Choose Neo4j when the graph is the product. Unbounded traversals, shortest-path at scale, community detection, PageRank, node embeddings — Neo4j's Graph Data Science library has a decade of work PostgreSQL simply does not have. Index-free adjacency also means traversal cost scales with the size of the neighborhood, not the size of the table; at hop six across a hundred-million-edge graph, joins lose to pointer-chasing, and no planner cleverness fully closes that gap.
Apache AGE occupied the middle ground — openCypher inside Postgres via an extension. It still works, but SQL/PGQ being in core and in the ISO standard changes its trajectory: standard syntax, no extension to maintain across major version upgrades, and planner integration that an extension can't match. For new work on graph-over-Postgres, SQL/PGQ is now the default answer.
The trade in one line: SQL/PGQ trades traversal depth and algorithm breadth for zero migration, zero sync, and transactional consistency. If your system does deep analytics on graph structure, choose the dedicated engine. Most SaaS products don't — they have relational data with a few graph-shaped questions, and that is exactly the case this feature was built for.
Operational Reality: What This Means for a Postgres SaaS
Here is how this lands for me concretely. Peekwise.ai runs on Postgres, like most of my projects. Any SaaS accumulates graph-shaped questions over time — which users influence which users, how entities reference each other, what connects to what — and until now every one of those questions forced a decision: ugly recursive SQL, or new infrastructure. I've written the five-alias self-join. I've evaluated the "just add Neo4j" architecture and rejected it because a second stateful system for a handful of queries is an operational tax that never stops billing you: another thing to monitor, patch, back up, secure, and explain to the next engineer.
With PostgreSQL 19 the adoption path is almost embarrassingly small. Upgrade (dump/restore, pg_upgrade, or logical replication from your current version). Write one CREATE PROPERTY GRAPH statement over the schema you already have — the graph definition is a schema object, so it belongs in your migrations like any table. Start replacing your most unreadable join chains with MATCH patterns one query at a time. There is no big-bang moment, no data movement, and full rollback safety: the underlying tables never changed, so if a graph query misbehaves, the old join version still sits right next to it.
Monitoring stays boring in the best way — EXPLAIN ANALYZE works on GRAPH_TABLE queries because they are relational queries after rewrite. Your existing indexes on foreign key columns are what make the traversals fast; if a graph query is slow, the fix is the same index work you already know. Six months in, I expect the failure mode won't be performance — it will be teams modeling graphs with labels that drift out of sync with evolving tables, which is why the property-matching rule above deserves a comment block in your migration files.
The bigger picture is the pattern Postgres keeps repeating. JSONB blunted the document-database argument in 9.4. Full-text search absorbed a chunk of Elasticsearch's low end years ago. pgvector is currently doing it to dedicated vector stores — the same consolidation logic I keep running into when choosing tools for AI stacks. SQL/PGQ is the graph chapter of the same story, and it arrives with something the others didn't have: an ISO standard behind the syntax. The interesting question is no longer "which database should we add?" It's becoming "what's actually left that Postgres can't absorb?" — and each release, that list gets shorter.
References
- PostgreSQL 19 Beta 1 Released! — PostgreSQL Global Development Group
- PostgreSQL 19 Documentation: 5.15. Property Graphs
- PostgreSQL 19 Beta Introduces SQL Graph Queries and Concurrent Table Repacking — InfoQ