| |

SQL 112 🛢️ Graph Queries in Relational Databases (Recursive & Native Graph Extensions)

⏱️ Estimated read: 18 min | |

Relational databases have stored graph-shaped data since before graph databases existed. An org chart is a self-referencing table; a bill of materials is a parent-child relationship; a social network is a many-to-many table with a join table. What has changed is the query interface. For decades, the only portable way to traverse these structures was the recursive CTE, standardized in SQL:1999 and implemented by every major engine. Then SQL:2023 introduced SQL/PGQ (Property Graph Queries), a syntax that lets relational tables be declared as a property graph and queried with a pattern-matching syntax that resembles Cypher. Oracle 23ai shipped SQL/PGQ first; PostgreSQL 19 is adding it in stages, with the current beta supporting fixed-length patterns but not the variable-length quantifiers that make graph traversal natural .

The two approaches are not competitors in the short term. The recursive CTE is universal, portable, and handles variable-depth traversal today. SQL/PGQ is declarative, composable with regular SQL, and handles fixed-length pattern matching elegantly, but its variable-length support is incomplete in the engines that have shipped it. A team building a graph query today uses the recursive CTE for traversal and watches SQL/PGQ for the queries that are naturally expressed as patterns—finding paths of a specific shape, matching multiple edge types, or projecting a subgraph as a result set.

This chapter covers the recursive CTE structure and its cycle-handling mechanisms, Oracle’s proprietary CONNECT BY syntax that predates the standard, the SQL/PGQ model as implemented in Oracle 23ai and PostgreSQL 19, and the practical decision between the two for different query shapes. It assumes familiarity with basic SQL and self-referencing tables.

Key point: The recursive CTE is the portable graph traversal tool. SQL/PGQ is the declarative pattern-matching layer on top of relational tables. PostgreSQL 19 supports fixed-length patterns but not the {n,m} quantifiers that Oracle 23ai provides. For variable-depth traversal that works everywhere today, the recursive CTE with cycle detection is still the answer.


Why graph queries in relational databases matter

The data is already relational. A property graph in SQL/PGQ does not copy data. It is a metadata object—a view-like declaration that maps existing tables to vertices and edges . Oracle’s implementation creates a property graph definition over live rows, and PostgreSQL’s rewriter translates GRAPH_TABLE queries into standard joins and filters that the optimizer can index and parallelize . The graph is a query interface, not a storage engine.

Recursive CTEs are the portable baseline. PostgreSQL, SQL Server, MySQL, and Oracle all support WITH RECURSIVE or its equivalent. The structure is consistent: an anchor member produces the starting rows, a recursive member joins the CTE back to the source table, and recursion stops when the recursive member returns no rows . The differences are in cycle handling and depth limits. PostgreSQL supports a CYCLE clause that tracks visited rows and adds is_cycle and path columns automatically . SQL Server requires a manual MAXRECURSION hint or a depth column in the query . MySQL has a session variable cte_max_recursion_depth that defaults to 1000 .

Oracle’s CONNECT BY is a separate syntax with different semantics. Before the standard recursive CTE, Oracle introduced hierarchical queries with START WITH and CONNECT BY PRIOR. The PRIOR operator evaluates the parent row’s value, and the condition defines the parent-child relationship . The syntax is more compact than the recursive CTE for pure hierarchies, but it is Oracle-specific and it errors on loops rather than tracking them. It remains widely used in Oracle shops because the recursive CTE syntax is more verbose for simple tree traversal.

SQL/PGQ changes the query model, not the storage. The CREATE PROPERTY GRAPH statement declares vertex tables and edge tables. Vertex tables map rows to nodes; edge tables map rows to directed edges with source and destination keys . The GRAPH_TABLE operator in the FROM clause takes a MATCH pattern and a COLUMNS projection, returning a normal result set that can be joined, aggregated, and filtered with regular SQL . The pattern syntax uses ASCII-art arrows: (a IS person)-[f IS follows]->(b IS person) matches a directed edge from a person to a person .

The trade-off. Recursive CTEs are verbose and imperative: the query describes how to traverse, step by step. SQL/PGQ is declarative: the query describes what pattern to match, and the engine decides how to execute it. For fixed-length patterns—find users who follow users who follow Alice—SQL/PGQ is shorter and clearer. For variable-depth traversal—find all ancestors of a node, or the shortest path between two nodes—the recursive CTE is the only portable option until quantifier support ships.


a. Recursive CTEs: structure, termination, and cycle detection

The recursive CTE has three parts. The anchor member is a non-recursive SELECT that produces the starting rows. The recursive member is a SELECT that references the CTE name and joins it to the source table. The two are connected by UNION ALL (SQL Server requires UNION ALL; PostgreSQL accepts UNION but it removes duplicates and can mask cycles) . Recursion stops when the recursive member returns zero rows, or when a depth limit is reached.

-- PostgreSQL: traverse an employee hierarchy.
WITH RECURSIVE org_chart AS (
    -- Anchor: the root (CEO, manager_id IS NULL).
    SELECT id, name, manager_id, 1 AS depth
    FROM employees
    WHERE manager_id IS NULL

    UNION ALL

    -- Recursive: employees whose manager is in the previous level.
    SELECT e.id, e.name, e.manager_id, oc.depth + 1
    FROM employees e
    JOIN org_chart oc ON e.manager_id = oc.id
)
SELECT * FROM org_chart ORDER BY depth, name;

The depth column is not required by the syntax, but it is the simplest termination check. Without a depth limit or a cycle guard, a table with a circular reference loops until the engine’s recursion limit is hit. PostgreSQL’s default is no limit; a runaway query consumes memory until it is cancelled. SQL Server’s default MAXRECURSION is 100; the query errors after 100 levels unless OPTION (MAXRECURSION 0) is specified .

PostgreSQL 14 added a CYCLE clause that makes cycle detection declarative. The clause specifies the columns to track, the name of the boolean cycle flag, and the name of the path array. The engine rewrites the query internally to add the array comparison .

-- PostgreSQL CYCLE clause: automatic cycle detection.
WITH RECURSIVE graph_walk AS (
    SELECT id, link, ARRAY[id] AS path, false AS is_cycle
    FROM graph
    WHERE id = 1

    UNION ALL

    SELECT g.id, g.link, gw.path || g.id, g.id = ANY(gw.path)
    FROM graph g
    JOIN graph_walk gw ON g.id = gw.link
    WHERE NOT gw.is_cycle
)
CYCLE id SET is_cycle USING path
SELECT * FROM graph_walk WHERE NOT is_cycle;

The CYCLE clause eliminates the manual is_cycle and path bookkeeping. The engine adds the columns to the output, and the query filters on is_cycle to stop the traversal . This is PostgreSQL-specific; SQL Server and MySQL require the manual array or path column.


b. Oracle’s CONNECT BY: the pre-standard hierarchical syntax

Oracle’s CONNECT BY predates the SQL standard’s recursive CTE and remains the idiomatic way to query hierarchies in Oracle. The query has a START WITH clause that identifies the root rows and a CONNECT BY clause that defines the parent-child relationship using the PRIOR operator .

-- Oracle: employee hierarchy with CONNECT BY.
SELECT employee_id, last_name, manager_id, LEVEL
FROM employees
START WITH manager_id IS NULL
CONNECT BY PRIOR employee_id = manager_id
ORDER SIBLINGS BY last_name;

PRIOR employee_id = manager_id means “the parent’s employee_id equals the current row’s manager_id.” The LEVEL pseudocolumn returns the depth in the hierarchy, starting at 1 for the root. CONNECT_BY_ROOT returns a column from the root row, and SYS_CONNECT_BY_PATH returns the full path as a string .

The semantic difference from the recursive CTE is significant. Oracle evaluates the CONNECT BY condition for each row and recursively selects children. If the hierarchy contains a loop, Oracle detects it at runtime and returns an error rather than looping . The recursive CTE with UNION ALL loops until the engine’s limit or a manual cycle guard stops it. Oracle’s approach is safer for unknown data but less flexible for graphs that legitimately contain cycles (which the query author may want to traverse with cycle tracking rather than error out).

Oracle also supports the standard recursive CTE syntax, so a query written for PostgreSQL or SQL Server can often be adapted with minimal changes. The choice between CONNECT BY and WITH RECURSIVE in Oracle is stylistic; both execute, and both produce a hierarchy.


c. SQL/PGQ: property graphs over relational tables

SQL/PGQ defines a property graph as a set of vertex tables and edge tables. The CREATE PROPERTY GRAPH statement maps columns to properties and declares the source and destination keys for edges. No data is copied; the graph is a metadata object .

-- Oracle 23ai / PostgreSQL 19: define a property graph.
CREATE PROPERTY GRAPH org_chart
  VERTEX TABLES (
    employees
      KEY (employee_id)
      PROPERTIES (employee_id, full_name, title)
  )
  EDGE TABLES (
    reports_to
      SOURCE KEY (employee_id) REFERENCES employees (employee_id)
      DESTINATION KEY (manager_id) REFERENCES employees (employee_id)
      PROPERTIES (since)
  );

The GRAPH_TABLE operator queries the graph with a MATCH pattern. The pattern is a chain of vertex and edge patterns, with variables bound to each element. The COLUMNS clause projects the bound variables into a result set that is a normal table expression .

-- Fixed-length pattern: direct reports of a manager.
SELECT report_name
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_name)
);

The pattern (e)-[r]->(m) matches a directed edge from e to m. The IS employee and IS reports_to labels restrict the vertex and edge types. The WHERE clause filters the matched vertices or edges, and COLUMNS projects properties from the bound variables.

The critical limitation in PostgreSQL 19 beta 1 is the absence of variable-length quantifiers. Oracle 23ai supports {1,3} after an edge pattern to match one to three hops, and {2,} for two or more. PostgreSQL’s v1 implementation is fixed-length only; a query that needs “friends of friends within three hops” must either union three fixed-length patterns or fall back to a recursive CTE . Oracle’s bounded quantifiers are the feature that makes SQL/PGQ practical for variable-depth traversal, and PostgreSQL’s lack of them is the biggest gap between the two implementations today.

Unbounded quantifiers (*, +, {n,}) are not supported in either engine as of the current releases. Oracle returns ORA-40985 or ORA-40984 for these patterns, and PostgreSQL rejects them outright . Path modes like SHORTEST and TRAIL are also unsupported. The SQL/PGQ standard defines these features, but the shipping implementations are a subset.


Complete Example Session

-- ============================================
-- PART 1: RECURSIVE CTE — PORTABLE HIERARCHY
-- ============================================
-- Works in PostgreSQL, SQL Server, MySQL, Oracle.
WITH RECURSIVE org_chart AS (
    SELECT id, name, manager_id, 1 AS depth
    FROM employees
    WHERE manager_id IS NULL

    UNION ALL

    SELECT e.id, e.name, e.manager_id, oc.depth + 1
    FROM employees e
    JOIN org_chart oc ON e.manager_id = oc.id
    WHERE oc.depth < 10  -- termination guard
)
SELECT * FROM org_chart ORDER BY depth;

-- ============================================
-- PART 2: POSTGRESQL CYCLE CLAUSE
-- ============================================
WITH RECURSIVE graph_walk AS (
    SELECT id, link, ARRAY[id] AS path, false AS is_cycle
    FROM graph WHERE id = 1

    UNION ALL

    SELECT g.id, g.link, gw.path || g.id, g.id = ANY(gw.path)
    FROM graph g
    JOIN graph_walk gw ON g.id = gw.link
    WHERE NOT gw.is_cycle
)
CYCLE id SET is_cycle USING path
SELECT * FROM graph_walk WHERE NOT is_cycle;

-- ============================================
-- PART 3: ORACLE CONNECT BY
-- ============================================
SELECT employee_id, last_name, LEVEL,
       CONNECT_BY_ROOT last_name AS root_name
FROM employees
START WITH manager_id IS NULL
CONNECT BY PRIOR employee_id = manager_id
ORDER SIBLINGS BY last_name;

-- ============================================
-- PART 4: SQL/PGQ GRAPH DEFINITION
-- ============================================
CREATE PROPERTY GRAPH org_chart
  VERTEX TABLES (
    employees
      KEY (employee_id)
      PROPERTIES (employee_id, full_name, title)
  )
  EDGE TABLES (
    reports_to
      SOURCE KEY (employee_id) REFERENCES employees (employee_id)
      DESTINATION KEY (manager_id) REFERENCES employees (employee_id)
  );

-- ============================================
-- PART 5: SQL/PGQ FIXED-LENGTH QUERY
-- ============================================
SELECT report_name, manager_name
FROM GRAPH_TABLE (
    org_chart
    MATCH (e IS employee)-[r IS reports_to]->(m IS employee)
    WHERE m.title = 'VP'
    COLUMNS (e.full_name AS report_name, m.full_name AS manager_name)
);

-- ============================================
-- PART 6: SQL/PGQ VARIABLE-LENGTH (ORACLE ONLY)
-- ============================================
-- Oracle 23ai: bounded quantifier {1,3}.
SELECT DISTINCT report_name
FROM GRAPH_TABLE (
    org_chart
    MATCH (e IS employee)-[r IS reports_to]->{1,3}(m IS employee)
    WHERE m.full_name = 'CEO'
    COLUMNS (e.full_name AS report_name)
);

-- PostgreSQL 19 beta 1: quantifier rejected.
-- Union three fixed-length patterns instead:
-- (e)-[r]->(m)
-- UNION ALL
-- (e)-[r1]->(x)-[r2]->(m)
-- UNION ALL
-- (e)-[r1]->(x)-[r2]->(y)-[r3]->(m)

-- ============================================
-- PART 7: COMPOSING GRAPH_TABLE WITH SQL
-- ============================================
-- GRAPH_TABLE returns a result set; join and aggregate normally.
SELECT m.title, COUNT(*) AS report_count
FROM GRAPH_TABLE (
    org_chart
    MATCH (e IS employee)-[r IS reports_to]->(m IS employee)
    COLUMNS (e.employee_id, m.title)
)
GROUP BY m.title
HAVING COUNT(*) > 5;

-- ============================================
-- PART 8: CYCLE HANDLING COMPARISON
-- ============================================
-- PostgreSQL: CYCLE clause tracks path.
-- SQL Server: MAXRECURSION hint or depth column.
-- MySQL: cte_max_recursion_depth session variable.
-- Oracle CONNECT BY: runtime loop detection, errors.
-- SQL/PGQ: no cycle handling in v1 (fixed-length only).

-- ============================================
-- PART 9: WHEN TO USE WHICH
-- ============================================
-- Variable-depth, portable, cycles possible:
--   Recursive CTE with CYCLE or depth guard.
-- Fixed-length pattern, Oracle 23ai:
--   SQL/PGQ with GRAPH_TABLE.
-- Fixed-length pattern, PostgreSQL 19:
--   SQL/PGQ (no quantifiers) or recursive CTE.
-- Legacy Oracle hierarchy:
--   CONNECT BY (compact, Oracle-specific).

-- ============================================
-- PART 10: THE COMPLETE FLOW
-- ============================================
-- 1. Data lives in relational tables.
-- 2. Recursive CTE traverses it portably.
-- 3. SQL/PGQ declares a property graph over it.
-- 4. GRAPH_TABLE queries the graph with patterns.
-- 5. Quantifiers are Oracle-only today.
-- 6. PostgreSQL 19 supports fixed-length only.

Quick Reference

Recursive CTE vs CONNECT BY

AspectRecursive CTEOracle CONNECT BY
PortabilityPostgreSQL, SQL Server, MySQL, OracleOracle only
StructureAnchor + recursive memberSTART WITH + CONNECT BY PRIOR
Cycle handlingManual guard or CYCLE clauseRuntime error
Depth limitEngine default or manualLEVEL pseudocolumn

SQL/PGQ Support

FeatureOracle 23aiPostgreSQL 19 beta 1
CREATE PROPERTY GRAPHYesYes
GRAPH_TABLE in FROMYesYes
Fixed-length MATCHYesYes
Bounded quantifiers {n,m}YesNo
Unbounded quantifiers *, +NoNo
Path modes (SHORTEST, TRAIL)NoNo

Cycle Handling

EngineMechanism
PostgreSQLCYCLE clause, is_cycle + path columns
SQL ServerMAXRECURSION hint, depth column
MySQLcte_max_recursion_depth session variable
Oracle CONNECT BYRuntime loop detection, error

When to Use

Query ShapeTool
Variable-depth traversalRecursive CTE
Unknown cyclesRecursive CTE with CYCLE
Fixed-length pattern, OracleSQL/PGQ
Fixed-length pattern, PostgreSQLSQL/PGQ or recursive CTE
Legacy Oracle hierarchyCONNECT BY

Best Practices

✅ Do This:

-- Add a depth guard to every recursive CTE.                 // ✅
WHERE oc.depth < 10

-- Use PostgreSQL CYCLE clause for graphs with cycles.       // ✅
CYCLE id SET is_cycle USING path

-- Declare property graph over existing tables.               // ✅
CREATE PROPERTY GRAPH org_chart VERTEX TABLES (...);

-- Compose GRAPH_TABLE with regular SQL.                     // ✅
SELECT ... FROM GRAPH_TABLE (...) GROUP BY ...

-- Use recursive CTE for variable-depth traversal.           // ✅

❌ Don’t Do This:

-- Omit termination guard in recursive CTE.                  // ❌
-- Infinite loop until engine limit.

-- Assume SQL/PGQ quantifiers work in PostgreSQL 19.         // ❌
-- Fixed-length only; use recursive CTE or union.

-- Use CONNECT BY in non-Oracle databases.                   // ❌
-- Not standard; use WITH RECURSIVE.

-- Expect SQL/PGQ to copy data.                              // ❌
-- It is a metadata view over existing tables.

Common Pitfalls

PitfallWhy It HappensFix
Recursive CTE loops foreverNo depth guard or cycle detectionAdd depth < N or CYCLE clause
SQL Server errors at 100 levelsDefault MAXRECURSIONOPTION (MAXRECURSION 0)
PostgreSQL quantifier rejectedv1 fixed-length onlyUnion fixed patterns or use recursive CTE
CONNECT BY errors on loopOracle detects loops at runtimeRestructure data or use recursive CTE
Graph query returns unexpected rowsPattern direction or label mismatchVerify -> vs <- and IS label

Real-World Examples

1. Recursive CTE Org Chart

WITH RECURSIVE org AS (
  SELECT id, name, manager_id, 1 AS depth FROM employees WHERE manager_id IS NULL
  UNION ALL
  SELECT e.id, e.name, e.manager_id, o.depth + 1
  FROM employees e JOIN org o ON e.manager_id = o.id
) SELECT * FROM org;

2. PostgreSQL CYCLE Clause

CYCLE id SET is_cycle USING path

3. Oracle CONNECT BY

SELECT employee_id, LEVEL FROM employees
START WITH manager_id IS NULL
CONNECT BY PRIOR employee_id = manager_id;

4. Create Property Graph

CREATE PROPERTY GRAPH g
  VERTEX TABLES (employees KEY (id))
  EDGE TABLES (reports_to SOURCE KEY (id) REFERENCES employees(id)
                          DESTINATION KEY (mgr_id) REFERENCES employees(id));

5. Fixed-Length GRAPH_TABLE

SELECT report FROM GRAPH_TABLE (g
  MATCH (e IS employee)-[r IS reports_to]->(m IS employee)
  COLUMNS (e.name AS report));

6. Oracle Quantifier

MATCH (e IS employee)-[r IS reports_to]->{1,3}(m IS employee)

7. Compose with Aggregation

SELECT title, COUNT(*) FROM GRAPH_TABLE (g
  MATCH (e)-[r]->(m) COLUMNS (m.title))
GROUP BY title;

8. MySQL Recursion Limit

SET SESSION cte_max_recursion_depth = 5000;

9. SQL Server MAXRECURSION

OPTION (MAXRECURSION 0)

10. Union for PostgreSQL 19

-- (a)-[r]->(b) UNION ALL (a)-[r1]->(x)-[r2]->(b)

Visual: Recursive CTE Execution

┌──────────────────────────────────────────────┐
│  RECURSIVE CTE                               │
│                                              │
│  Anchor member:                              │
│    SELECT root rows (manager_id IS NULL)     │
│         │                                    │
│         ▼                                    │
│  Result set T0                               │
│         │                                    │
│         ▼                                    │
│  Recursive member:                           │
│    JOIN T0 to source table                   │
│         │                                    │
│         ▼                                    │
│  Result set T1 (children of T0)              │
│         │                                    │
│         ▼                                    │
│  Repeat until recursive member returns 0 rows│
│         │                                    │
│         ▼                                    │
│  UNION ALL T0 + T1 + ... + Tn                │
│                                              │
│  No cycle detection unless CYCLE clause.     │
│                                              │
└──────────────────────────────────────────────┘

SQL/PGQ Query Flow

┌──────────────────────────────────────────────┐
│  SQL/PGQ                                     │
│                                              │
│  CREATE PROPERTY GRAPH                       │
│    VERTEX TABLES (employees)                 │
│    EDGE TABLES (reports_to)                  │
│         │                                    │
│         ▼                                    │
│  GRAPH_TABLE (org_chart                      │
│    MATCH (e)-[r]->(m)                        │
│    COLUMNS (e.name, m.name))                 │
│         │                                    │
│         ▼                                    │
│  Rewriter translates to joins                │
│         │                                    │
│         ▼                                    │
│  Normal execution plan                       │
│  (indexes, parallel, aggregation)            │
│                                              │
│  No data copied. Graph is metadata.          │
│                                              │
└──────────────────────────────────────────────┘

Quantifier Support

┌──────────────────────────────────────────────┐
│  VARIABLE-LENGTH PATTERNS                    │
│                                              │
│  Oracle 23ai:                                │
│    MATCH (a)-[r]->{1,3}(b)                   │
│    → 1 to 3 hops, supported                  │
│                                              │
│  PostgreSQL 19 beta 1:                       │
│    MATCH (a)-[r]->{1,3}(b)                   │
│    → rejected; fixed-length only             │
│    → union 3 patterns or use recursive CTE   │
│                                              │
│  Unbounded (*, +):                           │
│    → not supported in either                 │
│                                              │
└──────────────────────────────────────────────┘

Decision Flow

┌──────────────────────────────────────────────┐
│  WHICH GRAPH QUERY TOOL?                     │
│                                              │
│  Variable-depth traversal?                   │
│       └── Recursive CTE (portable)           │
│                                              │
│  Fixed-length pattern, Oracle 23ai?          │
│       └── SQL/PGQ with quantifiers           │
│                                              │
│  Fixed-length pattern, PostgreSQL 19?        │
│       └── SQL/PGQ (fixed) or recursive CTE   │
│                                              │
│  Legacy Oracle hierarchy?                    │
│       └── CONNECT BY                         │
│                                              │
│  Cycles possible?                            │
│       └── Recursive CTE + CYCLE clause       │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
Portable traversalWITH RECURSIVE (anchor + recursive member)
PostgreSQL cycle detectionCYCLE id SET is_cycle USING path
SQL Server depth limitMAXRECURSION option
Oracle hierarchySTART WITH + CONNECT BY PRIOR
SQL/PGQ definitionCREATE PROPERTY GRAPH over existing tables
SQL/PGQ queryGRAPH_TABLE with MATCH and COLUMNS
Oracle quantifiers{n,m} supported in 23ai
PostgreSQL quantifiersNot in 19 beta 1; fixed-length only
Unbounded quantifiersNot supported in either engine
Graph storageMetadata view; no data copied

Key takeaways:

  • The recursive CTE is the portable graph traversal tool. PostgreSQL, SQL Server, MySQL, and Oracle all support it. The structure is anchor member, recursive member, and a termination condition. Cycle detection requires a manual guard or PostgreSQL’s CYCLE clause .
  • Oracle’s CONNECT BY is a separate syntax with different semantics. It is compact for pure hierarchies and errors on loops rather than tracking them. It remains idiomatic in Oracle shops but is not portable .
  • SQL/PGQ declares a property graph over existing tables. CREATE PROPERTY GRAPH maps tables to vertices and edges; GRAPH_TABLE queries the graph with a pattern-matching syntax. No data is copied, and the resulting query is translated into standard joins .
  • Variable-length quantifiers are the dividing line. Oracle 23ai supports {1,3} after an edge pattern. PostgreSQL 19 beta 1 supports fixed-length patterns only. For variable-depth traversal that works today, the recursive CTE is still the answer .
  • Unbounded quantifiers are not supported in either engine. Patterns like * and + are part of the SQL/PGQ standard but are rejected by both Oracle and PostgreSQL in their current releases .
  • Compose GRAPH_TABLE with regular SQL. The result set is a normal table expression. Aggregation, GROUP BY, window functions, and joins all work around it. This is the advantage over a separate graph database .

Remember: Relational databases have always stored graph-shaped data. The recursive CTE is the portable way to traverse it, with cycle detection and depth guards as the safety mechanisms. SQL/PGQ is the declarative pattern-matching layer that makes fixed-length graph queries shorter and more readable, but its variable-length support is incomplete in PostgreSQL 19 and absent for unbounded patterns in both shipping engines. Use the recursive CTE for traversal that must work everywhere today; use SQL/PGQ for fixed-length patterns on Oracle 23ai, and watch PostgreSQL’s quantifier support in future releases. The graph is a query interface over relational storage, not a separate system.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!