Skip to content

ORA-01436: CONNECT BY Loop in User Data - Fix Circular References

Error Text: ORA-01436: CONNECT BY loop in user data

This error occurs when a hierarchical query using CONNECT BY encounters a circular reference in the data. Oracle detects that following the parent-child relationships creates an infinite loop (e.g., row A is the parent of B, B is the parent of C, and C is the parent of A), and raises this error to prevent infinite recursion.

Normal Hierarchy: Circular Reference:
CEO (1) A (parent=C)
├── VP (2) │
│ ├── Dir (3) ▼
│ └── Dir (4) B (parent=A)
└── VP (5) │
└── Dir (6) ▼
C (parent=B)
└──► A (parent=C) ← LOOP!
  • Self-referencing rows - A row where the parent ID equals its own ID
  • Circular parent chains - A chain like A references B, B references C, C references A
  • Data corruption - Bad updates creating unintended parent-child loops
  • Bad data imports - Bulk loading hierarchical data with inconsistent parent references
  • Organizational restructuring - Moving nodes in an org chart creating accidental cycles
  • Bill of materials - Component assemblies where part A contains part B which contains part A
-- Find rows where a record is its own parent
SELECT
id,
parent_id,
name
FROM your_table
WHERE id = parent_id;
-- For employee/org chart tables
SELECT
employee_id,
manager_id,
employee_name,
department_id
FROM employees
WHERE employee_id = manager_id;
-- Use NOCYCLE to detect loops without raising ORA-01436
-- CONNECT_BY_ISCYCLE = 1 flags the row that closes the loop
SELECT
LEVEL AS depth,
id,
parent_id,
name,
CONNECT_BY_ISCYCLE AS is_cycle,
SYS_CONNECT_BY_PATH(id, ' -> ') AS path
FROM your_table
START WITH parent_id IS NULL
CONNECT BY NOCYCLE PRIOR id = parent_id;
-- Filter to show ONLY the rows involved in cycles
SELECT
id,
parent_id,
name,
SYS_CONNECT_BY_PATH(name, ' -> ') AS cycle_path
FROM your_table
WHERE CONNECT_BY_ISCYCLE = 1
START WITH parent_id IS NULL
CONNECT BY NOCYCLE PRIOR id = parent_id;
-- Find all two-node cycles (A->B->A)
SELECT
a.id AS id_a,
a.parent_id AS parent_a,
b.id AS id_b,
b.parent_id AS parent_b
FROM your_table a
JOIN your_table b
ON a.parent_id = b.id
WHERE b.parent_id = a.id
AND a.id != b.id;
-- Find all three-node cycles (A->B->C->A)
SELECT
a.id AS id_a,
b.id AS id_b,
c.id AS id_c
FROM your_table a
JOIN your_table b ON a.parent_id = b.id
JOIN your_table c ON b.parent_id = c.id
WHERE c.parent_id = a.id
AND a.id != b.id
AND b.id != c.id;
-- Generic cycle detection with recursive CTE (12c+)
WITH cycle_check (id, parent_id, path, is_cycle) AS (
-- Anchor: start from all rows
SELECT
id,
parent_id,
CAST(id AS VARCHAR2(4000)) AS path,
0 AS is_cycle
FROM your_table
UNION ALL
-- Recursive: follow parent chain
SELECT
t.id,
t.parent_id,
c.path || ' -> ' || t.id,
CASE WHEN INSTR(c.path, CAST(t.id AS VARCHAR2(20))) > 0 THEN 1 ELSE 0 END
FROM your_table t
JOIN cycle_check c ON c.parent_id = t.id
WHERE c.is_cycle = 0
)
CYCLE id SET is_cycle_flag TO 'Y' DEFAULT 'N'
SELECT id, parent_id, path
FROM cycle_check
WHERE is_cycle_flag = 'Y';
-- Count orphaned records (parent references non-existent row)
SELECT
t.id,
t.parent_id,
t.name
FROM your_table t
WHERE t.parent_id IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM your_table p
WHERE p.id = t.parent_id
);
-- Check hierarchy depth and width
SELECT
MAX(LEVEL) AS max_depth,
COUNT(DISTINCT CONNECT_BY_ROOT id) AS root_count,
COUNT(*) AS total_nodes
FROM your_table
START WITH parent_id IS NULL
CONNECT BY NOCYCLE PRIOR id = parent_id;
-- Find nodes involved in multiple parent chains
SELECT
id,
COUNT(*) AS parent_count
FROM your_table
WHERE parent_id IS NOT NULL
GROUP BY id
HAVING COUNT(*) > 1;

1. Use NOCYCLE to Skip Loops (Query-Level Fix)

Section titled “1. Use NOCYCLE to Skip Loops (Query-Level Fix)”
-- Add NOCYCLE keyword to allow the query to run
-- This skips the looping rows instead of raising an error
-- Before (fails with ORA-01436):
SELECT LEVEL, employee_id, manager_id, employee_name
FROM employees
START WITH manager_id IS NULL
CONNECT BY PRIOR employee_id = manager_id;
-- After (succeeds, skipping circular references):
SELECT
LEVEL,
employee_id,
manager_id,
employee_name,
CONNECT_BY_ISCYCLE AS is_loop,
SYS_CONNECT_BY_PATH(employee_name, ' / ') AS hierarchy_path
FROM employees
START WITH manager_id IS NULL
CONNECT BY NOCYCLE PRIOR employee_id = manager_id;
-- Set parent to NULL for self-referencing root nodes
UPDATE your_table
SET parent_id = NULL
WHERE id = parent_id;
COMMIT;
-- Verify the fix
SELECT COUNT(*) AS self_references
FROM your_table
WHERE id = parent_id;
-- Step 1: Identify the cycles
SELECT
id,
parent_id,
name,
SYS_CONNECT_BY_PATH(id, ' -> ') AS cycle_path
FROM your_table
WHERE CONNECT_BY_ISCYCLE = 1
START WITH parent_id IS NULL
CONNECT BY NOCYCLE PRIOR id = parent_id;
-- Step 2: Break the cycle by nullifying one parent reference
-- Choose the link that makes the least business sense
UPDATE your_table
SET parent_id = NULL
WHERE id = :id_to_make_root; -- The node that should become a new root
COMMIT;
-- Step 3: Verify hierarchy is now acyclic
SELECT COUNT(*) AS remaining_cycles
FROM your_table
WHERE CONNECT_BY_ISCYCLE = 1
START WITH parent_id IS NULL
CONNECT BY NOCYCLE PRIOR id = parent_id;
-- For severely corrupted hierarchies, rebuild from a known-good state
-- Create backup
CREATE TABLE your_table_backup AS SELECT * FROM your_table;
-- Clear all parent references
UPDATE your_table SET parent_id = NULL;
-- Rebuild parent-child relationships from trusted source
MERGE INTO your_table t
USING (
SELECT child_id, parent_id
FROM trusted_hierarchy_source
) s
ON (t.id = s.child_id)
WHEN MATCHED THEN
UPDATE SET t.parent_id = s.parent_id;
COMMIT;
-- Validate: no cycles should exist
SELECT COUNT(*)
FROM your_table
WHERE CONNECT_BY_ISCYCLE = 1
START WITH parent_id IS NULL
CONNECT BY NOCYCLE PRIOR id = parent_id;
-- Recursive WITH clause (12c+) provides more control than CONNECT BY
-- and can handle cycles with the CYCLE clause
WITH org_hierarchy (employee_id, manager_id, employee_name, lvl, path) AS (
-- Anchor: root nodes
SELECT
employee_id,
manager_id,
employee_name,
1 AS lvl,
CAST(employee_name AS VARCHAR2(4000)) AS path
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- Recursive member
SELECT
e.employee_id,
e.manager_id,
e.employee_name,
h.lvl + 1,
h.path || ' > ' || e.employee_name
FROM employees e
JOIN org_hierarchy h ON e.manager_id = h.employee_id
)
CYCLE employee_id SET is_cycle TO 'Y' DEFAULT 'N'
SELECT
lvl,
employee_id,
manager_id,
employee_name,
path,
is_cycle
FROM org_hierarchy
ORDER BY path;

1. Add Constraints to Prevent Self-References

Section titled “1. Add Constraints to Prevent Self-References”
-- Prevent a row from being its own parent
ALTER TABLE your_table
ADD CONSTRAINT chk_no_self_ref
CHECK (id != parent_id);
-- For employee tables
ALTER TABLE employees
ADD CONSTRAINT chk_emp_not_own_mgr
CHECK (employee_id != manager_id);
-- Trigger to prevent circular references on INSERT or UPDATE
CREATE OR REPLACE TRIGGER trg_prevent_cycle
BEFORE INSERT OR UPDATE OF parent_id ON your_table
FOR EACH ROW
DECLARE
v_current_parent NUMBER;
v_depth NUMBER := 0;
v_max_depth NUMBER := 100; -- Safety limit
BEGIN
-- Skip if no parent assigned
IF :NEW.parent_id IS NULL THEN
RETURN;
END IF;
-- Prevent self-reference
IF :NEW.id = :NEW.parent_id THEN
RAISE_APPLICATION_ERROR(-20001,
'Cannot set parent_id equal to id (self-reference)');
END IF;
-- Walk up the parent chain to detect a cycle
v_current_parent := :NEW.parent_id;
WHILE v_current_parent IS NOT NULL AND v_depth < v_max_depth LOOP
-- If we find the new row's ID in its ancestor chain, it's a cycle
IF v_current_parent = :NEW.id THEN
RAISE_APPLICATION_ERROR(-20002,
'Circular reference detected: setting parent_id to ' ||
:NEW.parent_id || ' would create a cycle');
END IF;
-- Move up to the next parent
SELECT parent_id INTO v_current_parent
FROM your_table
WHERE id = v_current_parent;
v_depth := v_depth + 1;
END LOOP;
END;
/
-- Function to validate hierarchy before making changes
CREATE OR REPLACE FUNCTION would_create_cycle(
p_child_id NUMBER,
p_new_parent_id NUMBER
) RETURN BOOLEAN AS
v_current NUMBER;
v_max_depth CONSTANT NUMBER := 100;
v_depth NUMBER := 0;
BEGIN
IF p_new_parent_id IS NULL THEN
RETURN FALSE;
END IF;
IF p_child_id = p_new_parent_id THEN
RETURN TRUE;
END IF;
v_current := p_new_parent_id;
WHILE v_current IS NOT NULL AND v_depth < v_max_depth LOOP
BEGIN
SELECT parent_id INTO v_current
FROM your_table
WHERE id = v_current;
EXCEPTION
WHEN NO_DATA_FOUND THEN
v_current := NULL;
END;
IF v_current = p_child_id THEN
RETURN TRUE; -- Cycle detected
END IF;
v_depth := v_depth + 1;
END LOOP;
RETURN FALSE;
END;
/
-- Use the function before updates
DECLARE
v_has_cycle BOOLEAN;
BEGIN
v_has_cycle := would_create_cycle(
p_child_id => 100,
p_new_parent_id => 50
);
IF v_has_cycle THEN
DBMS_OUTPUT.PUT_LINE('ERROR: This change would create a circular reference');
ELSE
UPDATE your_table SET parent_id = 50 WHERE id = 100;
COMMIT;
END IF;
END;
/
-- Validate hierarchical data in a staging table before loading
-- Step 1: Check for self-references
SELECT COUNT(*) AS self_refs
FROM staging_table
WHERE id = parent_id;
-- Step 2: Check for orphaned parents
SELECT COUNT(*) AS orphans
FROM staging_table s
WHERE s.parent_id IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM staging_table p WHERE p.id = s.parent_id
);
-- Step 3: Check for cycles using CONNECT BY NOCYCLE
SELECT COUNT(*) AS cycles
FROM staging_table
WHERE CONNECT_BY_ISCYCLE = 1
START WITH parent_id IS NULL
CONNECT BY NOCYCLE PRIOR id = parent_id;
-- Step 4: Only load if all checks pass
DECLARE
v_self_refs NUMBER;
v_orphans NUMBER;
v_cycles NUMBER;
BEGIN
SELECT COUNT(*) INTO v_self_refs FROM staging_table WHERE id = parent_id;
SELECT COUNT(*) INTO v_orphans FROM staging_table s
WHERE s.parent_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM staging_table p WHERE p.id = s.parent_id);
SELECT COUNT(*) INTO v_cycles
FROM staging_table
WHERE CONNECT_BY_ISCYCLE = 1
START WITH parent_id IS NULL
CONNECT BY NOCYCLE PRIOR id = parent_id;
IF v_self_refs + v_orphans + v_cycles = 0 THEN
INSERT INTO your_table SELECT * FROM staging_table;
COMMIT;
DBMS_OUTPUT.PUT_LINE('Data loaded successfully');
ELSE
DBMS_OUTPUT.PUT_LINE('Data quality issues found:');
DBMS_OUTPUT.PUT_LINE(' Self-references: ' || v_self_refs);
DBMS_OUTPUT.PUT_LINE(' Orphaned parents: ' || v_orphans);
DBMS_OUTPUT.PUT_LINE(' Circular references: ' || v_cycles);
END IF;
END;
/
-- Always use NOCYCLE in production queries to handle unexpected loops
SELECT
LEVEL,
LPAD(' ', 2 * (LEVEL - 1)) || name AS indented_name,
id,
parent_id,
CONNECT_BY_ISCYCLE AS has_cycle,
CONNECT_BY_ISLEAF AS is_leaf,
CONNECT_BY_ROOT name AS root_name,
SYS_CONNECT_BY_PATH(name, ' > ') AS full_path
FROM your_table
START WITH parent_id IS NULL
CONNECT BY NOCYCLE PRIOR id = parent_id
ORDER SIBLINGS BY name;
-- Limit depth to prevent excessive recursion
SELECT
LEVEL,
id,
parent_id,
name
FROM your_table
WHERE LEVEL <= 20 -- Reasonable depth limit
START WITH parent_id IS NULL
CONNECT BY NOCYCLE PRIOR id = parent_id;
  • ORA-01489 - Result of string concatenation is too long (from SYS_CONNECT_BY_PATH)
  • ORA-01788 - CONNECT BY clause required in this query block
  • ORA-01437 - Cannot have join with CONNECT BY
  • ORA-00600 - Internal error (can occur with deeply nested hierarchies)
  • ORA-30009 - Not enough memory for CONNECT BY operation
  1. Add NOCYCLE to the CONNECT BY clause for immediate query execution
  2. Use CONNECT_BY_ISCYCLE to identify which rows form the loop
  3. Use SYS_CONNECT_BY_PATH to trace the exact circular path
  4. Fix the data by breaking the cycle (NULL out a parent reference or correct the parent ID)
  5. Add constraints or triggers to prevent future circular references
-- Make query work despite cycles
CONNECT BY NOCYCLE PRIOR id = parent_id
-- Find which rows cause the loop
SELECT id, parent_id, CONNECT_BY_ISCYCLE
FROM your_table
WHERE CONNECT_BY_ISCYCLE = 1
START WITH parent_id IS NULL
CONNECT BY NOCYCLE PRIOR id = parent_id;
-- Find self-referencing rows
SELECT * FROM your_table WHERE id = parent_id;
-- Fix self-references
UPDATE your_table SET parent_id = NULL WHERE id = parent_id;
-- Prevent self-references with constraint
ALTER TABLE your_table ADD CONSTRAINT chk_no_self_ref CHECK (id != parent_id);
-- Trace the loop path
SELECT SYS_CONNECT_BY_PATH(id, ' -> ') AS loop_path
FROM your_table
WHERE CONNECT_BY_ISCYCLE = 1
START WITH parent_id IS NULL
CONNECT BY NOCYCLE PRIOR id = parent_id;
  • Add CHECK constraints - Prevent self-referencing rows at the database level
  • Use NOCYCLE in production - Always include NOCYCLE in hierarchical queries as a safety measure
  • Validate before bulk loads - Check staging data for cycles before inserting into production tables
  • Limit recursion depth - Use WHERE LEVEL <= n to prevent runaway queries
  • Implement validation triggers - Walk the parent chain to detect cycles before they are committed
  • Use recursive CTEs - The CYCLE clause in recursive WITH provides built-in cycle detection