PostgreSQL 19 SQL/PGQ adds a graph-query layer over relational tables. Learn how it works, where it fits, and why Neo4j still matters.
PostgreSQL 19 SQL/PGQ adds a graph-query layer over relational tables. Learn how it works, where it fits, and why Neo4j still matters.
PostgreSQL 19 SQL/PGQ is one of the more interesting additions to PostgreSQL in recent years. It brings the Property Graph Queries part of the SQL standard into PostgreSQL core.
At first glance, the feature can sound much bigger than it actually is:
PostgreSQL now supports graphs. Do we still need Neo4j?
The answer is yes, Neo4j and other native graph databases still have an important role.
PostgreSQL 19 is not turning PostgreSQL into a native graph database. Instead, SQL/PGQ gives us something that can be extremely useful in ordinary application databases:
a graph-oriented way to query relational data that already exists.
Your tables remain tables. Your foreign keys remain foreign keys. PostgreSQL does not create a second copy of the data or introduce a new graph storage engine.
Instead, you can describe some of those tables and relationships as a property graph, then query relationships using graph patterns instead of repeatedly reconstructing them through joins.
As of August 2026, PostgreSQL 19 is still in beta, with PostgreSQL 19 Beta 3 released on August 13, 2026. SQL/PGQ is one of the headline features planned for PostgreSQL 19. PostgreSQL does not recommend beta releases for production use, and the syntax can still change before general availability.
Let’s understand why this matters.
Relational databases are perfectly capable of representing relationships.
Consider a very normal organization model.
We have two tables:
CREATE TABLE departments (
department_id INT PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE employees (
id INT PRIMARY KEY,
name TEXT NOT NULL,
manager_id INT REFERENCES employees(id),
department_id INT NOT NULL
REFERENCES departments(department_id)
);SQLAn employee belongs to a department:
An employee can also report to another employee:
Nothing unusual here.
Let’s add some data.
INSERT INTO departments (department_id, name)
VALUES
(10, 'Engineering'),
(20, 'Platform'),
(30, 'Executive');
INSERT INTO employees (id, name, manager_id, department_id)
VALUES
(1, 'Maya', NULL, 30),
(2, 'Arjun', 1, 20),
(3, 'Neha', 2, 10);SQLNow imagine we receive this requirement:
Find Neha, her manager, and the department in which her manager works.
We need to join employees with itself because a manager is also an employee.
Then we need another join to find the manager’s department.
SELECT
e.name AS employee_name,
m.name AS manager_name,
d.name AS manager_department
FROM employees e
JOIN employees m
ON e.manager_id = m.id
JOIN departments d
ON m.department_id = d.department_id
WHERE e.id = 3;SQLResult:
employee_name | manager_name | manager_department
---------------+--------------+-------------------
Neha | Arjun | PlatformTEXTThere is nothing inherently wrong with this query.
It is actually quite reasonable.
And this is important because graph discussions sometimes exaggerate the difficulty of relational SQL.
Two joins are not a database architecture crisis.
The interesting problem starts appearing when relationships become the thing we are primarily querying.
Suppose the requirement changes.
We now need:
Find Neha’s manager, her manager’s manager, and the department of each one.
Our query starts growing:
SELECT
e.name AS employee_name,
m1.name AS manager_name,
d1.name AS manager_department,
m2.name AS senior_manager_name,
d2.name AS senior_manager_department
FROM employees e
JOIN employees m1
ON e.manager_id = m1.id
JOIN departments d1
ON m1.department_id = d1.department_id
JOIN employees m2
ON m1.manager_id = m2.id
JOIN departments d2
ON m2.department_id = d2.department_id
WHERE e.id = 3;SQLStill possible.
But notice what we are doing.
Conceptually, the question follows two REPORTS_TO relationships and then looks up where each manager WORKS_IN.
Yet the SQL query is dominated by implementation details such as employees.manager_id = employees.id and employees.department_id = departments.department_id.
The relationship exists conceptually in our domain as REPORTS_TO and WORKS_IN.
But SQL normally makes us reconstruct that relationship from columns every time we query it.
That is one of the problems SQL/PGQ tries to address.
A foreign key tells PostgreSQL that employees.manager_id references employees.id. It provides referential integrity.
It does not, however, give our queries a reusable business relationship called REPORTS_TO.
Likewise, employees.department_id references departments.department_id, but SQL does not automatically give that relationship a reusable name such as WORKS_IN.
Applications repeatedly express those relationships through join predicates.
SQL/PGQ allows us to move those relationships into a graph definition.
The PostgreSQL documentation describes a property graph as a graph-oriented representation over existing relational data. The property graph behaves much like a read-only view over the underlying relational tables rather than introducing a separate graph storage system.
That distinction is fundamental.
SQL/PGQ is the Property Graph Queries part of the SQL standard, defined in ISO/IEC 9075-16.
PostgreSQL 19 introduces constructs including CREATE PROPERTY GRAPH and GRAPH_TABLE.
CREATE PROPERTY GRAPH describes how relational tables should appear as a graph.
GRAPH_TABLE lets us match patterns against that graph.
The easiest mental model is:
SQL/PGQ adds a graph lens over relational data.
It does not move your data somewhere else.
For our organization example, we can think of Employee and Department as vertices, and REPORTS_TO and WORKS_IN as edges.
Conceptually:
For our sample data:
Now the organization begins to look much closer to the domain we actually think about.
PostgreSQL 19 lets us map the existing tables into a property graph.
CREATE PROPERTY GRAPH organization_graph
VERTEX TABLES (
employees
LABEL employee,
departments
LABEL department
)
EDGE TABLES (
employees AS reports
SOURCE KEY (id)
REFERENCES employees (id)
DESTINATION KEY (manager_id)
REFERENCES employees (id)
LABEL reports_to,
employees AS department_membership
SOURCE KEY (id)
REFERENCES employees (id)
DESTINATION KEY (department_id)
REFERENCES departments (department_id)
LABEL works_in
);SQLSomething interesting is happening here.
We did not create tables such as employee_nodes, reports_to_edges, or department_nodes.
The original tables remain the source of truth.
The same employees table participates as a vertex and in multiple relationships.
One alias exposes an employee row as a REPORTS_TO edge. Another exposes the same row as a WORKS_IN edge.
PostgreSQL explicitly allows tables to appear more than once within a property graph by assigning aliases to graph elements. Maya has a NULL manager_id, so her row does not produce a reports_to edge with a destination.
This lets us take relationships that were previously implicit in foreign-key columns and give them domain-specific names.
Remember our original requirement:
Find Neha, her manager, and the department where her manager works.
The relational version required two joins.
With SQL/PGQ, we can express the relationship itself:
SELECT *
FROM GRAPH_TABLE (
organization_graph
MATCH
(e IS employee WHERE e.id = 3)
-[IS reports_to]->
(m IS employee)
-[IS works_in]->
(d IS department)
COLUMNS (
e.name AS employee_name,
m.name AS manager_name,
d.name AS manager_department
)
);SQLThe important part is:
You can almost read the query directly:
Find employee
e, followreports_toto managerm, then followworks_into departmentd.
PostgreSQL’s graph query syntax uses parentheses for vertices, square brackets for edges, and arrows to represent edge direction.
The query now expresses the domain relationship, rather than repeatedly describing the foreign-key implementation.
The benefit becomes clearer when a question crosses several named relationships. To find Neha’s manager and that manager’s manager, the pattern describes the path directly:
SELECT *
FROM GRAPH_TABLE (
organization_graph
MATCH
(e IS employee WHERE e.name = 'Neha')
-[IS reports_to]->
(m1 IS employee)
-[IS reports_to]->
(m2 IS employee)
COLUMNS (
e.name AS employee,
m1.name AS manager,
m2.name AS managers_manager
)
);SQLThis is easier to read than another chain of employee aliases. The important boundary is that PostgreSQL 19’s initial implementation is strongest when the number of hops is known in advance.
The SQL/PGQ standard includes broader features, but quantified path patterns, shortest paths, graph types, and path constraints are not implemented in the initial PostgreSQL 19 feature set. For arbitrary-depth hierarchies, a recursive CTE may still be necessary. The limitation is documented in the PostgreSQL development discussion.
Not automatically. SQL/PGQ changes how the relationship is expressed, not how PostgreSQL stores the underlying rows.
PostgreSQL describes a property graph as a read-only view over relational tables. Graph patterns are processed through the same planning and execution infrastructure as ordinary SQL, and the PostgreSQL 19 release notes say they are internally written as relational queries.
That means familiar engineering still matters: indexes, statistics, join selectivity, row counts, and the shape of the generated plan. Use EXPLAIN and EXPLAIN ANALYZE. Graph syntax does not make an inefficient physical design disappear.
PostgreSQL and Neo4j overlap at the query-model level, but their storage models and target workloads remain different.
PostgreSQL keeps rows in relational tables and exposes selected tables as vertices and edges. Neo4j is designed around the management, storage, and traversal of nodes and relationships. Its operations documentation explicitly describes native graph processing and storage.
For a bounded application question such as Employee -> Manager -> Department, PostgreSQL SQL/PGQ can be a very natural fit. Deep and unpredictable traversal is a different workload:
Those questions benefit from a graph-native storage model, mature path operations, and graph analytics. PostgreSQL 19 does not turn the relational engine into that architecture.
| Area | PostgreSQL 19 SQL/PGQ | Neo4j |
|---|---|---|
| Primary storage | Relational tables | Native graph |
| Graph model | Logical property graph over tables | Native property graph |
| Existing PostgreSQL data | Used directly | Usually needs loading/synchronization |
| SQL support | Native | Not the primary query model |
| Graph query language | SQL/PGQ | Cypher |
| Fixed relationship traversal | Good fit | Good fit |
| Deep graph traversal | Not its primary architecture | Core use case |
| Shortest-path workloads | Initial PG19 support is limited | Mature graph capability |
| Graph analytics | Limited compared with graph platforms | Strong ecosystem |
| Existing OLTP application | Excellent fit | Usually additional architecture |
| Additional system beside existing PostgreSQL | No | Yes |
| Relationship-oriented developer experience | Much improved | Native from the beginning |
SQL/PGQ is especially attractive when:
REPORTS_TO, OWNS, MEMBER_OF, or DEPENDS_ON.In that middle ground, adding another database, a change-data-capture pipeline, operational monitoring, and consistency handling may provide little value.
A dedicated graph database remains compelling when the graph itself is the primary data model: social networks, fraud investigation, knowledge graphs, network topology, recommendation systems, and other workloads centered on deep traversal or graph algorithms.
GRAPH_TABLE produces a tabular result. PostgreSQL can join that result to ordinary tables, filter it, aggregate it, and order it like another FROM source.
This may be the feature’s most practical strength. A query can use a graph pattern where relationships are central, then return to relational SQL for the rest of the workload. It is not a choice between two separate query worlds.
Treat a property graph definition as a schema contract. Labels, properties, edge direction, source keys, and destination keys become part of the application’s domain-facing query model.
PROPERTIES (...) lists or NO PROPERTIES for edges that carry no data.CREATE INDEX idx_employees_manager
ON employees(manager_id);
CREATE INDEX idx_employees_department
ON employees(department_id);SQLThe property and privilege behavior is documented in PostgreSQL’s CREATE PROPERTY GRAPH reference.
PostgreSQL 19 SQL/PGQ does not replace relational SQL, and it does not turn PostgreSQL into Neo4j. It adds a standardized graph-query layer over relational data that already exists.
Use it when named relationships and bounded traversal are central to a question, but PostgreSQL remains the natural system of record. Continue using ordinary joins for ordinary relational work. Consider a native graph database when deep paths, shortest-path queries, highly connected datasets, and graph analytics define the workload.
The real improvement is not a new storage engine. It is the ability to query the meaning of relationships without repeatedly expressing every relationship as column equality.
For more practical backend and architecture deep dives, browse the complete article archive.
Senior Technical Architect at HCLTech · working with Leonteq Security AG
I have 10+ years of experience building distributed systems and fintech platforms. I write about practical, non-obvious engineering details that official documentation often skips.
Scala, JVM, AI, and backend systems. I send practical articles when there is something worth reading.
Join the newsletterEngineering deep dives on Scala, Java, Rust, and AI Systems. Written by a senior engineer who builds real fintech systems.
TOPICS
© 2026 prabhat.dev