PostgreSQL 19 ships with GRAPH_TABLE, an implementation of the ISO/IEC 9075-16:2023 SQL/PGQ standard. The shorthand version: you can declare a property graph over a set of relational tables, and then query that graph with pattern-matching syntax that looks a lot like Cypher, without leaving Postgres.

The longer version requires some context, because graph databases are one of those product categories where the marketing has consistently outrun the engineering, and where the technical question “what does a graph query actually do” is harder to find a straight answer to than it should be.

What a graph database is, briefly

A graph database stores vertices (nodes) and edges (relationships). A vertex represents a thing — a person, an account, a product. An edge represents a connection — a friendship, an ownership, a purchase. Both can carry properties: a Person vertex might have name and birthdate; a Knows edge might have since.

The reason to want a graph database — the only compelling technical reason, leaving aside the modeling-aesthetic arguments — is that you have a workload where the query of interest is “find all paths of length up to N between these vertices, subject to constraints on the edges and intermediate vertices.” That kind of query, expressed in standard SQL over a normalized schema, is a nightmare of self-joins and recursive CTEs. Expressed in a graph query language, it’s a single readable line.

The three major graph query languages in the wild are Cypher (Neo4j’s, now also implemented by Memgraph and others), Gremlin (Apache TinkerPop’s traversal language, more procedural), and SPARQL (the W3C language for RDF triple stores, technically a different data model). The ISO standards body looked at this landscape, decided everyone should have one, and produced GQL as a standalone language and SQL/PGQ as an embedded form of the same pattern-matching syntax inside SQL. Both were finalized in 2023. SQL/PGQ is what PostgreSQL 19 implements.

How PostgreSQL implements it

The implementation is deliberately not a graph storage engine. It is a rewriter: a property graph is metadata that maps existing relational tables into the vertex/edge abstraction, and a graph pattern in GRAPH_TABLE is rewritten into a tree of relational joins and filters that the planner then handles using its existing machinery.

You declare the graph:

1CREATE PROPERTY GRAPH social_graph
2 VERTEX TABLES (
3 people LABEL Person PROPERTIES (id, name, birth_year)
4 )
5 EDGE TABLES (
6 friendships SOURCE KEY (person_a) REFERENCES people (id)
7 DESTINATION KEY (person_b) REFERENCES people (id)
8 LABEL Knows PROPERTIES (since)
9 );

You query it:

1SELECT b_name
2FROM GRAPH_TABLE (social_graph
3 MATCH (a:Person WHERE a.name = 'Jan')-[k:Knows]->(b:Person)
4 COLUMNS (b.name AS b_name)
5);

The MATCH clause describes a pattern; (a:Person) is a vertex labeled Person, -[k:Knows]-> is an outgoing edge labeled Knows, and the rest is filters and projection. The rewriter looks up Person and Knows in pg_propgraph_element, finds the underlying people and friendships tables, generates the equivalent joins, and the planner takes it from there.

Two things follow from this design that you should sit with.

First: your existing indexes work. Because the rewrite produces ordinary relational joins, an index on friendships(person_a) is used exactly the way it is used by any other query. There is no separate graph storage to populate, no separate index structure to maintain, no separate query optimizer to tune. The cost model is the cost model you already have.

Second: your existing performance characteristics also apply. A graph traversal of depth N becomes N joins. For small N this is fine. For variable-depth traversals — and the initial PG19 implementation doesn’t support those yet — the rewrite would be doing what your recursive CTEs already do, with similar performance.

Where this lands relative to Neo4j and friends

The honest comparison breaks into two cases.

For shallow, fixed-depth graph queries — the vast majority of “graph” queries in actual production systems — Postgres 19 is now competitive. A two-hop or three-hop traversal over indexed foreign keys is fast in Postgres because Postgres is good at indexed joins, and that is exactly what the rewriter produces. If your “graph” workload is “show me my friends’ recent purchases,” you should be running this on Postgres. You almost certainly already were, in the form of awkward joins; SQL/PGQ just makes the awkward part go away.

For deep, variable-length traversals — the workloads Neo4j was actually built for — Postgres is not competitive yet, and SQL/PGQ in 19 does not change that. The PG19 implementation explicitly does not support quantified patterns (-[k:Knows*1..5]->, find-all-paths-up-to-five-hops). Variable-length paths are planned for a future release. Even when they land, the underlying execution model — relational joins driven by the existing planner — is structurally different from Neo4j’s index-free adjacency, where every vertex stores direct pointers to its incident edges and traversal cost is proportional to result size rather than to total graph size.

Index-free adjacency is the one real technical reason to run Neo4j. It buys you O(degree) traversal at each step, where the relational equivalent is O(log n) per step at best, and dramatically worse if your edge table is large and your indexes are not well-suited to the access pattern. For a six-degrees-of-separation query over a hundred-million-vertex graph, that difference can be three orders of magnitude.

For a “find the accountants my CFO has emailed in the last quarter” query, it isn’t.

Where this leaves you

If you have ever spun up Neo4j because you had a vague intuition that your data was graph-shaped, this is the release that lets you reconsider. The question to ask is whether your actual queries are shallow and fixed-depth or deep and variable-length. The first category is now well-served by Postgres, and you can collapse your stack. The second category remains the case for a dedicated graph database, at least until the variable-length-path implementation lands and the planner learns to cost it sensibly — and even then, the index-free-adjacency gap will keep specialized graph databases competitive for genuine deep-traversal workloads.

The more interesting case is the third one: you didn’t have a graph workload, but now that the syntax is right there, you might. There is a real category of operational queries that are awkward enough in plain SQL that nobody writes them, and that become writable with SQL/PGQ. Auditing access paths through a permission model. Tracing the lineage of a derived dataset through a chain of transformations. Mapping the dependency graph of your application’s foreign-key relationships. These were always expressible in SQL; they were rarely written in SQL. Lowering the syntactic cost lowers the threshold for someone on your team to actually do the analysis.

That’s the under-discussed benefit of putting SQL/PGQ in the box. It isn’t that Postgres becomes a graph database; it’s that the universe of graph-shaped questions you can ask of your existing data without changing your storage layer just got materially larger.