Published: 2026-07-11 • Updated: 2026-09-26
SQL/PGQ in Practice: GRAPH_TABLE on Oracle and PostgreSQL
SQL/PGQ — Part 16 of the SQL standard (ISO/IEC 9075-16:2023) — lets you define a property graph over the relational tables you already have and query it with visual patterns like (a)-[r]->(b) instead of hand-written recursive CTEs. Among mainstream relational databases, Oracle Database 23ai and later (native, GA, every edition including Free) is the one that ships it. PostgreSQL 19 carried a first implementation in betas 1 to 3, but Beta 4 reverted it on September 24, 2026, so PostgreSQL 19 will be released without SQL/PGQ. This post is a practical tour: a worked example, a side-by-side of what each dialect accepts — the Oracle column live-tested in July 2026, error codes included, and the PostgreSQL column kept as a record of the beta implementation in case it returns in a later release — and the one gotcha that bites every Node.js-based client.
The short version
| Engine | SQL/PGQ status (September 2026) |
|---|---|
| Oracle 23ai+ | Native and GA in every edition, including Free — a converged-database feature, no extra license. Bounded quantifiers, no path modes (details below). |
| PostgreSQL 19 | Committed March 2026 and shipped in betas 1–3 (fixed-length patterns only, no quantifiers); reverted in Beta 4 on September 24, 2026, so the 19 release will not include it. |
| PostgreSQL ≤18 | Nothing — and no extension backfills standard PGQ. Apache AGE is openCypher, a different language. |
| SQL Server / MySQL / SQLite | No SQL/PGQ. SQL Server's graph tables + MATCH are proprietary, pre-standard syntax. |
| DuckDB | duckpgq community extension — the reference non-Oracle implementation, research-grade. |
What GRAPH_TABLE replaces
The canonical use case is a reachability question over a self-referencing relationship — an org chart, a bill of materials, a dependency tree. Take two tables: employees and a reports_to relationship table. "Everyone in Priya's reporting chain, up to three levels down" is a recursive CTE:
-- Works on both engines
-- (Oracle: drop the RECURSIVE keyword and give the CTE a column list)
WITH RECURSIVE chain AS (
SELECT r.employee_id, 1 AS depth
FROM reports_to r
JOIN employees m ON m.employee_id = r.manager_id
WHERE m.full_name = 'Priya Sharma'
UNION ALL
SELECT r.employee_id, c.depth + 1
FROM reports_to r
JOIN chain c ON r.manager_id = c.employee_id
WHERE c.depth < 3
)
SELECT DISTINCT e.full_name
FROM chain c
JOIN employees e ON e.employee_id = c.employee_id;It works, but the traversal logic — which column chases which, in which direction, to what depth — is smeared across a seed query, a join condition, and a depth counter. SQL/PGQ splits the problem in two: declare the graph once, then write the traversal as a picture.
Step 1: define the property graph
CREATE PROPERTY GRAPH maps existing tables onto vertices and edges. Nothing is copied — the graph is a metadata object over live rows, like a view:
-- Oracle 23ai+ (also ran on the PostgreSQL 19 betas 1-3)
CREATE PROPERTY GRAPH org_chart
VERTEX TABLES (
employees
KEY (employee_id)
LABEL employee
PROPERTIES (employee_id, full_name, title)
)
EDGE TABLES (
reports_to
KEY (employee_id)
SOURCE KEY (employee_id) REFERENCES employees (employee_id)
DESTINATION KEY (manager_id) REFERENCES employees (employee_id)
LABEL reports_to
NO PROPERTIES
);Each row of employees becomes a vertex labeled employee carrying three properties; each row of reports_to becomes a directed edge from the report to the manager. If your schema keeps manager_id directly on employees instead, the standard lets the same table serve as both a vertex table and an edge table — map SOURCE to employee_id and DESTINATION to manager_id.
employees row is a vertex, each reports_to row a directed edge — the same rows, queryable as a graph. MATCH (e IS employee)-[r IS reports_to]->(m IS employee) walks the arrows.Oracle additionally accepts CREATE OR REPLACE PROPERTY GRAPH, IF NOT EXISTS, an OPTIONS (…) clause, and ALTER PROPERTY GRAPH … COMPILE (compile-only — structural changes go through OR REPLACE). Both engines support DROP PROPERTY GRAPH [IF EXISTS].
Step 2: query it with GRAPH_TABLE
GRAPH_TABLE sits in the FROM clause and produces an ordinary result set — the columns are whatever you project in COLUMNS (…). Direct reports of one manager, on either engine:
-- Oracle 23ai+ (also ran on the PostgreSQL 19 betas 1-3)
SELECT report
FROM GRAPH_TABLE (
org_chart
MATCH (e IS employee)-[r IS reports_to]->(m IS employee)
WHERE m.full_name = 'Priya Sharma'
COLUMNS (e.full_name AS report)
);On Oracle, the depth-bounded version of the recursive CTE is one bounded quantifier:
-- Oracle 23ai+ only: bounded quantifier {1,3}
SELECT DISTINCT report
FROM GRAPH_TABLE (
org_chart
MATCH (e IS employee)-[r IS reports_to]->{1,3}(m IS employee)
WHERE m.full_name = 'Priya Sharma'
COLUMNS (e.full_name AS report)
);The PostgreSQL 19 beta implementation rejected this quantifier — it was fixed-length only. To cover "up to three levels" there you had to union three fixed-length patterns, or keep the recursive CTE. That was the biggest practical difference between the two dialects before PostgreSQL pulled the feature from 19; on PostgreSQL today, the recursive CTE above is the way to write this query.
What each engine actually supports
The Oracle column below was live-tested in July 2026 against Oracle AI Database 26ai Free (reporting 23.26.1.0.0) — the 26ai branding is a rename of the 23ai lineage, and converged-database features behave the same across editions. The PostgreSQL column reflects the PostgreSQL 19 beta 1 documentation and commit history; the feature was reverted in Beta 4, so no released PostgreSQL version runs any of it. Oracle's grammar may loosen in future release updates; these are current-server observations, not promises.
| Capability | Oracle 23ai+ | PostgreSQL 19 beta 1 (reverted in Beta 4) |
|---|---|---|
GRAPH_TABLE in FROM | Yes | Yes |
CREATE / DROP PROPERTY GRAPH | Yes, plus OR REPLACE, IF [NOT] EXISTS, ALTER … COMPILE | Yes, plus psql \dG and pg_get_propgraphdef() |
Fixed-length MATCH patterns | Yes | Yes |
Bounded quantifiers {n} / {n,m} / {,m} | Yes | No — fixed-length only in v1 |
Unbounded quantifiers * / + / {n,} | No (ORA-40985 / ORA-40984) | No |
Optional edge ? | No (ORA-49003) | No |
Path modes WALK / TRAIL / ACYCLIC / SIMPLE | No (ORA-00927) | No |
SHORTEST / path-search prefixes | No — and without variable-length patterns there is nothing to apply them to | No |
Label disjunction IS a|b | Yes; !a parses but fails ORA-40951, wildcard % is rejected | Untested here |
What Oracle rejects, with the exact errors
If you learned graph pattern matching from Cypher, GQL, or the SQL/PGQ paper, your muscle memory includes constructs Oracle does not run today. Here is what each attempt returned on a live 23.26 server, so you can recognize the errors when you hit them:
| You write | Oracle answers |
|---|---|
-[r IS reports_to]->{1,3}(m) | Works — bounded quantifiers are fine. |
-[r]->* or -[r]->+ | ORA-40985 — unbounded quantifiers not supported. |
-[r]->{1,} | ORA-40984 — an unbounded upper bound is the same story. |
-[r]->? | ORA-49003 — the optional quantifier is called out by name as unsupported. |
MATCH WALK (a)-[r]->(b) (also TRAIL / ACYCLIC / SIMPLE) | ORA-00927 — path modes don't parse. |
MATCH SHORTEST 2 … | ORA-00927. |
(a IS person|company) | Works — label disjunction is supported. |
(a IS !person) | Parses, then ORA-40951 "Complex label expressions" at semantic analysis. |
(a IS %) | ORA-00911 — no label wildcard. |
What Oracle does run beyond the core, all verified against a real graph: COLUMNS (a.*) expansion, graph-pattern WHERE, path variables (p = (a)-[r]->(b)), a IS LABELED employee, MATCHNUM(), ELEMENT_NUMBER(), VERTEX_ID(), and ONE ROW PER STEP. Several of these arrived in release updates rather than the initial 23ai release: elem.* and IS SOURCE/DESTINATION OF in 23.4, BINDING_COUNT() in 23.6, ONE ROW PER MATCH/VERTEX/STEP in 23.7, MATCHNUM()/ELEMENT_NUMBER() in 23.8, and path variables, IS LABELED, and PROPERTY_EXISTS() in 23.9 — so an older 23ai patch level may reject syntax a newer one runs.
The :label trap in every Node.js client
The standard defines two spellings for a label check: (a IS person) and the shorthand (a:person). If your client is built on node-oracledb — as most JavaScript/TypeScript tooling for Oracle is — the shorthand never reaches the server. The driver scans the SQL text for :name bind placeholders, decides :person is one, finds no bind value attached, and fails client-side with an NJS-098 bind-count error. No ORA- code, no server round-trip — which makes it look like a driver bug rather than what it is: a lexical collision between GPML's label shorthand and Oracle's decades-old bind syntax. The fix is simply to standardize on the IS form inside GRAPH_TABLE. Jam SQL Studio's generated starter queries emit the IS form for exactly this reason, and if you're writing PGQ through any other node-oracledb-based tool or script, you should too.
Where the graphs live in the catalog
Oracle treats property graphs as first-class schema objects, which means you can enumerate and script them like tables, and the PostgreSQL 19 betas did the same:
- Oracle exposes the usual
USER_/ALL_/DBA_dictionary triples:*_PROPERTY_GRAPHS(one row per graph, with its mode),*_PG_ELEMENTS(vertex and edge elements),*_PG_ELEMENT_LABELS,*_PG_LABELS,*_PG_LABEL_PROPERTIES(properties per label, including data types),*_PG_PROP_DEFINITIONS(property-to-column mappings), and*_PG_KEYS. The full DDL round-trips throughDBMS_METADATA.GET_DDL('PROPERTY_GRAPH', name, owner). - PostgreSQL 19 betas 1–3 stored a property graph as a new relation kind in
pg_class;psqllisted them with\dG, andpg_get_propgraphdef(oid)returned theCREATE PROPERTY GRAPHstatement. There was no separate graph engine — the rewriter expandedGRAPH_TABLEinto ordinary joins, soEXPLAINshowed a regular relational plan. All of this left with the Beta 4 revert.
Where Jam SQL Studio meets it
Jam SQL Studio (from version 1.4.15) treats property graphs as first-class objects. On an Oracle 23ai+ connection the Object Explorer grows a Property Graphs folder per schema; the same folder reads PostgreSQL's catalog, so it listed graphs on the 19 beta 1–3 servers and is empty on Beta 4 and later. Each graph gets:
- Script as CREATE — the server's own definition (
DBMS_METADATAon Oracle,pg_get_propgraphdefon PostgreSQL), so what you script is exactly what the server holds. - Script as DROP — the teardown statement, wired into the destructive-statement confirmation.
- Query with GRAPH_TABLE — a starter query pre-filled with real labels and sample properties read from the graph's catalog definition, in the
IS-label form, so you don't reconstruct the pattern syntax from memory.
Queries themselves run as pass-through SQL in the Query Editor — GRAPH_TABLE returns a plain result set, and the standard results grid renders it (Oracle's JSON-shaped VERTEX_ID() values included). Since version 1.5.0 the editor also parses GRAPH_TABLE and offers schema-aware completions for graph names, labels, and properties (marked beta in the changelog). The scripting details live in the Scripting guide, and the PostgreSQL 19 release status and remaining features are in PostgreSQL 19 Release Date and New Features.
The takeaway
SQL/PGQ is the first time standard SQL has had a native answer to "query this as a graph," and 2026 is the year it stopped being a paper feature: Oracle ships a solid bounded-pattern subset in every edition including Free. PostgreSQL's fixed-length v1 made it into the 19 betas and was pulled before release, so on PostgreSQL the wait continues. Bounded quantifiers are Oracle-only today and unbounded traversal is supported nowhere, so keep recursive CTEs in your toolbox for variable-depth questions a while longer. And if you write PGQ from Node.js tooling, spell your labels with IS.
FAQ
What is SQL/PGQ?
SQL/PGQ is Part 16 of the SQL standard (ISO/IEC 9075-16, published 2023). It adds property graphs to SQL: CREATE PROPERTY GRAPH defines a graph view over existing tables (rows become vertices and edges), and the GRAPH_TABLE operator matches visual patterns like (a)-[r]->(b) against it inside an ordinary FROM clause. The pattern language (GPML) is shared with the standalone GQL standard (ISO/IEC 39075).
Which databases support SQL/PGQ today?
As of September 2026: Oracle Database 23ai and later ship SQL/PGQ natively in every edition, including the free one. PostgreSQL 19 carried a first implementation, limited to fixed-length patterns, in betas 1 to 3, but Beta 4 (September 24, 2026) reverted it, so PostgreSQL 19 will be released without SQL/PGQ. DuckDB has a community extension (duckpgq). MySQL, SQL Server, and SQLite have no SQL/PGQ support — SQL Server's graph tables with MATCH are a proprietary feature that predates the standard and uses different syntax.
Is SQL/PGQ the same as Cypher or GQL?
No. Cypher is Neo4j's query language, and Apache AGE brings openCypher to PostgreSQL as a separate language. GQL is a standalone ISO standard for graph databases. SQL/PGQ embeds graph pattern matching inside SQL itself — you keep your relational tables and query them as a graph from a normal SELECT. PGQ and GQL share the same pattern language, so MATCH patterns look alike, but PGQ lives inside SQL.
Why does my GRAPH_TABLE query fail with NJS-098 in Node.js?
The colon-label shorthand (a:person) collides with node-oracledb's bind-variable syntax. The driver treats :person as a bind placeholder before the SQL ever reaches Oracle and raises NJS-098 because no bind value was supplied. Use the equivalent IS form — (a IS person) — which the standard also defines. This applies to any tool built on node-oracledb.
Does PostgreSQL support SQL/PGQ property graphs?
Not in any released version. SQL/PGQ was committed for PostgreSQL 19 and shipped in betas 1 to 3, then Beta 4 (September 24, 2026) reverted it; the project says removed features can potentially return in a later major release. No extension adds standard SQL/PGQ to PostgreSQL 18 or earlier — Apache AGE adds openCypher (a different language), and pgRouting is graph algorithms, not pattern matching.
Jam SQL Studio