Published: 2026-07-11 • Updated: 2026-07-15
SQL/PGQ in Practice: GRAPH_TABLE on Oracle and PostgreSQL 19
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. Two engines now speak it: Oracle Database 23ai and later (native, GA, every edition including Free) and PostgreSQL 19, whose first implementation shipped in beta 1 in June 2026. This post is a practical tour: a worked example, a side-by-side of what each engine's dialect actually accepts — the Oracle column live-tested in July 2026, error codes included — and the one gotcha that bites every Node.js-based client. PostgreSQL 19 is beta software throughout; treat its half of everything below as subject to change until GA.
The short version
| Engine | SQL/PGQ status (mid-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, shipped in beta 1 (June 2026); GA expected later in the year. v1 supports fixed-length patterns only — no quantifiers. |
| 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:
-- Works on Oracle 23ai+ and PostgreSQL 19 (beta)
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:
-- Works on Oracle 23ai+ and PostgreSQL 19 (beta)
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)
);On PostgreSQL 19 beta 1 this quantifier is rejected — the v1 implementation is fixed-length only. To cover "up to three levels" you union three fixed-length patterns, or keep the recursive CTE for variable-depth questions until a later release grows quantifiers. That single limitation is the biggest practical difference between the two dialects today.
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. Oracle's grammar may loosen in future release updates; these are current-server observations, not promises.
| Capability | Oracle 23ai+ | PostgreSQL 19 beta 1 |
|---|---|---|
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 — check the PostgreSQL 19 docs |
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
Both engines treat property graphs as first-class schema objects, which means you can enumerate and script them like tables:
- 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 stores a property graph as a new relation kind in
pg_class;psqllists them with\dG, andpg_get_propgraphdef(oid)returns theCREATE PROPERTY GRAPHstatement. Under the hood there is no separate graph engine — the rewriter expandsGRAPH_TABLEinto ordinary joins, soEXPLAINshows a regular relational plan.
Where Jam SQL Studio meets it
Jam SQL Studio (from version 1.4.15) treats property graphs as first-class objects on both engines. On an Oracle 23ai+ or PostgreSQL 19+ connection, the Object Explorer grows a Property Graphs folder per schema. 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). GRAPH_TABLE IntelliSense — graph names, labels, and property completions — is on the roadmap as part of the autocomplete overhaul. The scripting details live in the Scripting guide, and the broader PostgreSQL 19 feature tour is in What's New in PostgreSQL 19.
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, and PostgreSQL 19's fixed-length v1 is in beta with GA in sight. The dialect gap is real — bounded quantifiers are the Oracle-only superpower today, unbounded traversal is nobody's — 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 mid-2026: Oracle Database 23ai and later ship SQL/PGQ natively in every edition, including the free one, and PostgreSQL 19 (currently in beta) includes a first implementation limited to fixed-length patterns. 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 18 or earlier support property graphs?
No. SQL/PGQ arrives in PostgreSQL 19, currently in beta. No extension adds standard SQL/PGQ to earlier versions — Apache AGE adds openCypher (a different language), and pgRouting is graph algorithms, not pattern matching. To run GRAPH_TABLE on PostgreSQL you need a 19 beta server today, or general availability expected later in 2026.
Jam SQL Studio