If you're still writing CONNECT BY queries in Oracle because you think recursive CTEs aren't supported, it's time to update that assumption. Recursive CTEs have been available in Oracle since 11g Release 2, and are fully supported in every version since, including 19c and 23ai.
I still run into teams that don't know this. They'll write CONNECT BY out of habit, then struggle with cycle detection or portability issues that a recursive CTE would handle more cleanly. Both approaches work. But for new queries, it's worth knowing what you have available.
What Is a Recursive CTE?
A recursive CTE (Common Table Expression) is a WITH clause that references itself, allowing you to traverse hierarchical or graph-structured data iteratively. If you've used them in SQL Server or PostgreSQL, the syntax in Oracle is identical.
CONNECT BY: The Oracle Classic
Oracle's CONNECT BY syntax has been around since the early days and remains valid. For a simple employee-manager hierarchy:
SELECT
employee_id,
manager_id,
first_name,
LEVEL AS depth
FROM employees
START WITH manager_id IS NULL
CONNECT BY PRIOR employee_id = manager_id;
It works, it's readable, and Oracle DBAs know it well. But it comes with quirks, especially around cycle detection, filtering, and portability.
The Recursive CTE Equivalent
The same query written as a recursive CTE:
WITH emp_hierarchy (employee_id, manager_id, first_name, depth) AS (
-- Anchor: the root node(s)
SELECT employee_id, manager_id, first_name, 1 AS depth
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- Recursive: join each level to the next
SELECT e.employee_id, e.manager_id, e.first_name, h.depth + 1
FROM employees e
JOIN emp_hierarchy h ON e.manager_id = h.employee_id
)
SELECT * FROM emp_hierarchy;
The structure is explicit: an anchor member defines the starting rows, and the recursive member joins each iteration back to the CTE itself. Oracle handles the looping internally.
What I like about this approach is that the logic is visible. You can read it top to bottom and understand what it's doing. With CONNECT BY, the behavior of PRIOR and pseudo-columns like LEVEL can be opaque if you haven't worked with them recently.
Translating CONNECT BY Features
Several CONNECT BY pseudo-columns have recursive CTE equivalents:
LEVELbecomes an incrementeddepthcounter in the recursive member.CONNECT_BY_ROOTbecomes carrying the root value down through each recursive step.SYS_CONNECT_BY_PATHbecomes building a path string using concatenation at each level.CONNECT_BY_ISLEAFbecomes a post-query check: rows with no children in the result set.NOCYCLEbecomes theCYCLEclause on the CTE to handle loops.
The translations aren't always one-liners, but they're straightforward once you understand the pattern.
When to Use Each
Stick with CONNECT BY if:
- You rely heavily on
SYS_CONNECT_BY_PATHand the team knows the syntax well. - The query is simple and already in production with no issues.
- You're on a version older than 11gR2. Rare, but they exist.
Prefer recursive CTEs if:
- You want SQL that's portable across Oracle, PostgreSQL, and SQL Server.
- You need cleaner cycle detection with the
CYCLEclause. - You're building complex multi-step logic where CTE readability helps.
- Your team comes from a non-Oracle background.
There's no urgent reason to rewrite working CONNECT BY queries. They're not deprecated and they're not going anywhere. But for new hierarchical queries in Oracle 19c and beyond, recursive CTEs are a clean, portable, and fully supported choice. Use whichever fits the team and the task.