Chapter 05 · Oracle SQL Fundamentals: Joins, Subqueries, Set Operations, and Hierarchical Queries

CONNECT BY, Recursive WITH, Hierarchical Data, Cycle Handling, and Tree Traversal

Traverse Oracle hierarchies safely with START WITH/CONNECT BY and recursive subquery factoring, make paths and depth observable, and detect or repair cycles deliberately.

Intermediate → Advanced115–135 minutesHierarchy + cycle-detection labCONNECT BY + recursive subquery factoringOracle AI Database Free · SEARCH/CYCLE syntaxLast reviewed: August 2026

Learning outcomes

ServiceHub stores components in an adjacency list: each component optionally points to a parent component. A malformed import creates a cycle, while another report sorts all rows with a normal ORDER BY and destroys the tree’s sibling structure. Oracle offers two strong ways to traverse this data: its long-standing START WITH ... CONNECT BY syntax and recursive subquery factoring with SEARCH/CYCLE.

01

Build a tree with START WITH, CONNECT BY, PRIOR, LEVEL, and SYS_CONNECT_BY_PATH.

02

Preserve sibling ordering with ORDER SIBLINGS BY instead of flattening the hierarchy.

03

Detect CONNECT BY loops with NOCYCLE and CONNECT_BY_ISCYCLE.

04

Write Oracle recursive subquery factoring with a required column alias list plus SEARCH/CYCLE clauses.

05

Choose traversal constraints and indexing from the actual hierarchy workload and failure modes.

Prerequisite connection

Lesson 3 introduced ordinary WITH subquery factoring. Recursive subquery factoring adds an anchor member, recursive member, required column alias list, and optional SEARCH/CYCLE clauses. Unlike PostgreSQL, Oracle does not add a RECURSIVE keyword after WITH.

Lab and version baseline

Mandatory examples target a disposable ServiceHub schema in Oracle AI Database Free 26ai. The chapter was reviewed against the July 2026 RU 23.26.3 documentation, SQL Developer 26.2, and SQLcl 26.2.1. No paid option, management pack, RAC, Data Guard, Exadata, or cloud service is required. Oracle AI Database Free remains limited to 2 foreground CPUs, 2 GB combined SGA/PGA memory, and 12 GB user data and receives no Oracle patches or Support service requests; use it as a learning engine, not a production default.

1. Model the hierarchy explicitly

The simplest relational tree stores a row identifier plus a nullable parent identifier. The root has no parent; every non-root row points to its parent. A self-referencing foreign key can enforce that the referenced parent exists, but it does not automatically prove the graph is acyclic. Cycles are a data-quality rule that traversal logic must detect or prevent.

sql · adjacency-list shape
CREATE TABLE servicehub_sql_component (    component_id        NUMBER PRIMARY KEY,    parent_component_id NUMBER,    component_name      VARCHAR2(80) NOT NULL,    CONSTRAINT sh05_component_parent_fk      FOREIGN KEY (parent_component_id)      REFERENCES servicehub_sql_component(component_id));

2. CONNECT BY: root, parent-child rule, depth, and path

START WITH selects root rows. CONNECT BY defines how a parent row relates to its child. The unary PRIOR operator identifies which expression comes from the parent row. LEVEL is a pseudocolumn reporting depth, beginning at 1 for each root.

sql · depth-first ServiceHub component tree
SELECT    component_id,    parent_component_id,    component_name,    LEVEL AS depth,    SYS_CONNECT_BY_PATH(component_name, '/') AS pathFROM servicehub_sql_componentSTART WITH parent_component_id IS NULLCONNECT BY PRIOR component_id = parent_component_idORDER SIBLINGS BY component_name;

SYS_CONNECT_BY_PATH makes the ancestor chain visible. ORDER SIBLINGS BY sorts children under each parent while preserving hierarchical structure. A normal top-level ORDER BY component_name can flatten the display order and obscure the traversal.

3. Deliberately wrong: introduce a cycle and observe ORA-01436

A cycle exists when following parent/child relationships eventually returns to an ancestor. In the disposable lab, make the root a child of one of its descendants. The ordinary CONNECT BY query now detects a loop.

sql · inject a disposable cycle
UPDATE servicehub_sql_componentSET parent_component_id = 3WHERE component_id = 1;COMMIT;SELECT component_id, component_name, LEVELFROM servicehub_sql_componentSTART WITH component_id = 1CONNECT BY PRIOR component_id = parent_component_id;-- ORA-01436: CONNECT BY loop in user data

This is not a performance problem. It is a graph-integrity problem exposed by traversal.

sql · diagnose without infinite traversal
SELECT    component_id,    component_name,    LEVEL AS depth,    CONNECT_BY_ISCYCLE AS is_cycle,    SYS_CONNECT_BY_PATH(component_name, '/') AS pathFROM servicehub_sql_componentSTART WITH component_id = 1CONNECT BY NOCYCLE PRIOR component_id = parent_component_id;

NOCYCLE allows rows to be returned despite the loop, while CONNECT_BY_ISCYCLE marks a row whose child is also its ancestor. Use this to investigate; do not normalize cyclic data as a valid tree merely because NOCYCLE can render it.

sql · repair the data rule
UPDATE servicehub_sql_componentSET parent_component_id = NULLWHERE component_id = 1;COMMIT;

4. Recursive subquery factoring: anchor + recursive member

Current Oracle SQL also supports recursive subquery factoring. The factored query’s column alias list is required for recursion. One branch is the anchor member that selects roots; the recursive branch references the query name and joins children to rows already produced.

sql · recursive WITH in Oracle syntax
WITH component_tree    (component_id, parent_component_id, component_name, depth, path) AS (    SELECT        component_id,        parent_component_id,        component_name,        1,        '/' || component_name    FROM servicehub_sql_component    WHERE parent_component_id IS NULL    UNION ALL    SELECT        c.component_id,        c.parent_component_id,        c.component_name,        t.depth + 1,        t.path || '/' || c.component_name    FROM servicehub_sql_component c    JOIN component_tree t      ON c.parent_component_id = t.component_id)SEARCH DEPTH FIRST BY component_name SET traversal_orderCYCLE component_id SET cycle_mark TO 'Y' DEFAULT 'N'SELECT    component_id,    parent_component_id,    component_name,    depth,    path,    cycle_markFROM component_treeORDER BY traversal_order;

The SEARCH clause generates an ordering column for depth-first or breadth-first traversal. The CYCLE clause defines which columns identify a cycle and adds a cycle marker. If you omit CYCLE and Oracle discovers a recursive cycle, the statement errors rather than recursing forever.

5. CONNECT BY versus recursive WITH

Need CONNECT BY Recursive subquery factoring
Established Oracle hierarchy code Native, concise, common Useful during modernization/composition
Depth LEVEL Explicit depth expression
Path SYS_CONNECT_BY_PATH Build explicitly in recursive member
Sibling/traversal order ORDER SIBLINGS BY SEARCH DEPTH/BREADTH FIRST
Cycle handling NOCYCLE + CONNECT_BY_ISCYCLE CYCLE clause
Portability Oracle-specific Closer to standard recursive-query structure, but Oracle syntax/details still matter

Choose based on readability, existing code, required features, and team familiarity. Do not rewrite a correct CONNECT BY query merely because recursive WITH looks newer.

6. Performance and correctness boundaries

Index the parent reference when the workload repeatedly finds children by parent_component_id; this is a normal access-path consideration, not a universal tuning mandate. Constrain roots and traversal depth when the business question permits it. For large graphs, measure actual row counts and path fan-out because a valid tree can still expand dramatically.

Do not confuse a hierarchy with a general graph problem. A component can have one parent in this adjacency-list model. If the domain needs arbitrary many-to-many edges, graph modeling or an explicit edge table may be more accurate than forcing the data into a tree.

7. Hands-on lab: build, break, diagnose, repair, and traverse

sql · setup
DROP TABLE servicehub_sql_component IF EXISTS PURGE;CREATE TABLE servicehub_sql_component (    component_id        NUMBER PRIMARY KEY,    parent_component_id NUMBER,    component_name      VARCHAR2(80) NOT NULL,    CONSTRAINT sh05_component_parent_fk      FOREIGN KEY (parent_component_id)      REFERENCES servicehub_sql_component(component_id));CREATE INDEX sh05_component_parent_ixON servicehub_sql_component(parent_component_id);INSERT INTO servicehub_sql_component VALUES (1,NULL,'Cooling System');INSERT INTO servicehub_sql_component VALUES (2,1,'Pump Group');INSERT INTO servicehub_sql_component VALUES (3,2,'Pump A');INSERT INTO servicehub_sql_component VALUES (4,2,'Pump B');INSERT INTO servicehub_sql_component VALUES (5,1,'Valve Group');INSERT INTO servicehub_sql_component VALUES (6,5,'Valve A');COMMIT;
sql · verify the good tree
SELECT    LPAD(' ', 2 * (LEVEL - 1)) || component_name AS tree_line,    LEVEL AS depth,    SYS_CONNECT_BY_PATH(component_name, '/') AS pathFROM servicehub_sql_componentSTART WITH parent_component_id IS NULLCONNECT BY PRIOR component_id = parent_component_idORDER SIBLINGS BY component_name;
sql · cleanup
DROP TABLE servicehub_sql_component PURGE;

Verification checklist:

  • The initial root is Cooling System at LEVEL=1.
  • Pump A and Pump B are siblings below Pump Group.
  • The deliberate cycle produces ORA-01436 without NOCYCLE.
  • CONNECT_BY_ISCYCLE or recursive CYCLE can make cycle evidence visible.
  • The repaired tree is acyclic and the cleanup touches only the probe table.

8. Production judgment and chapter bridge

Production hierarchy code needs a documented root rule, parent-key rule, cycle policy, traversal order, and expected fan-out. A foreign key proves parent existence but not acyclicity. Decide whether cycles should be blocked at write time, detected in validation, or tolerated only in non-tree graph data. Monitor pathological depth/fan-out rather than prescribing a universal maximum.

No paid option, management pack, restart, or COMPATIBLE change is required for these SQL hierarchy examples on the declared 26ai baseline. This closes the fundamentals chapter: you now have Oracle-specific SELECT, join, subquery, set, and hierarchy semantics. Chapter 06 builds on that foundation with analytic functions, advanced aggregation, MATCH_RECOGNIZE, MODEL, and SQL macros.

Check your understanding

  1. What does PRIOR identify in a CONNECT BY condition?
  2. Why is ORDER SIBLINGS BY different from a plain ORDER BY?
  3. What does NOCYCLE do when Oracle detects a CONNECT BY loop?
  4. Does Oracle recursive subquery factoring use WITH RECURSIVE?
  5. What does a self-referencing foreign key fail to prove?
Review the answers

PRIOR marks the expression evaluated from the parent row in the parent-child relationship.

ORDER SIBLINGS BY sorts children under the same parent while preserving hierarchical traversal; a plain ORDER BY can override the tree display order.

It allows traversal to return rows despite the loop, and CONNECT_BY_ISCYCLE can mark cycle evidence.

No. Oracle uses WITH plus a recursive factored query; the column alias list is required for recursion.

It proves referenced parents exist, but it does not by itself prove the relationship graph is acyclic.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.