Recursive SQL solves problems in which the output from one step becomes the input to the next. Typical examples are an organization chart, a category tree, a bill of materials, a chain of managers, and a dependency graph. In DB2 for z/OS, you write this logic with a recursive common table expression, or recursive CTE. The statement remains declarative: you describe the starting rows and the relationship to follow, and Db2 performs the repeated traversal.
This tutorial follows IBM's documented recursive CTE model. You will build an anchor member and a recursive member, connect them with UNION ALL, add levels and paths, prevent cycles, apply a practical recursion ceiling, respect unsupported constructs, and finish with ordering, performance, and debugging techniques.
Before writing the query, identify a node key, a parent key, and the direction you want to travel. The following EMPLOYEE_HIERARCHY table uses an adjacency-list design: EMPLOYEE_ID identifies a person and MANAGER_ID points to another row in the same table. A top-level employee has a null manager. The examples assume that EMPLOYEE_ID is unique and that an index beginning with MANAGER_ID supports downward traversal.
123456789101112CREATE TABLE EMPLOYEE_HIERARCHY ( EMPLOYEE_ID INTEGER NOT NULL PRIMARY KEY, EMPLOYEE_NAME VARCHAR(80) NOT NULL, MANAGER_ID INTEGER, JOB_TITLE VARCHAR(80), ACTIVE_FLAG CHAR(1) NOT NULL, FOREIGN KEY (MANAGER_ID) REFERENCES EMPLOYEE_HIERARCHY (EMPLOYEE_ID) ); CREATE INDEX IX_EMPLOYEE_MANAGER ON EMPLOYEE_HIERARCHY (MANAGER_ID, EMPLOYEE_ID);
A self-referencing foreign key prevents a manager value from pointing to a nonexistent employee, but it does not by itself prove that every route is acyclic. Data can still be wrong in ways that form a longer loop. The SQL therefore needs defensive logic even when referential integrity is enabled.
IBM describes a common table expression as a temporary named result that exists for one SQL statement. A CTE becomes recursive when a fullselect in its definition references the CTE name in its FROM clause. Db2 first evaluates the initialization fullselect, often called the anchor member. It then repeatedly evaluates the iterative fullselect, often called the recursive member, using newly produced rows to find the next set.
123456789101112131415161718192021222324252627282930WITH ORG_TREE ( EMPLOYEE_ID, EMPLOYEE_NAME, MANAGER_ID, TREE_LEVEL ) AS ( SELECT E.EMPLOYEE_ID, E.EMPLOYEE_NAME, E.MANAGER_ID, 0 FROM EMPLOYEE_HIERARCHY E WHERE E.EMPLOYEE_ID = :START_EMPLOYEE UNION ALL SELECT C.EMPLOYEE_ID, C.EMPLOYEE_NAME, C.MANAGER_ID, P.TREE_LEVEL + 1 FROM ORG_TREE P INNER JOIN EMPLOYEE_HIERARCHY C ON C.MANAGER_ID = P.EMPLOYEE_ID WHERE P.TREE_LEVEL < :MAX_LEVEL ) SELECT EMPLOYEE_ID, EMPLOYEE_NAME, MANAGER_ID, TREE_LEVEL FROM ORG_TREE ORDER BY TREE_LEVEL, EMPLOYEE_ID;
If the host variable START_EMPLOYEE is 100, the anchor emits employee 100 at level zero. The first recursive iteration finds direct reports and gives them level one. The next iteration finds their reports at level two. A branch ends naturally when its current row has no children. The whole recursion ends when the latest iteration produces no rows or when MAX_LEVEL prevents additional rows from expanding.
The direction of the recursive join controls the result. The previous query walks from a manager down to descendants. To walk upward, join the current row's MANAGER_ID to another row's EMPLOYEE_ID. The CTE pattern does not change; only the edge followed by the iterative fullselect changes.
123456789101112131415161718192021WITH MANAGER_CHAIN ( EMPLOYEE_ID, EMPLOYEE_NAME, MANAGER_ID, DISTANCE ) AS ( SELECT EMPLOYEE_ID, EMPLOYEE_NAME, MANAGER_ID, 0 FROM EMPLOYEE_HIERARCHY WHERE EMPLOYEE_ID = :EMPLOYEE_ID UNION ALL SELECT M.EMPLOYEE_ID, M.EMPLOYEE_NAME, M.MANAGER_ID, C.DISTANCE + 1 FROM MANAGER_CHAIN C INNER JOIN EMPLOYEE_HIERARCHY M ON M.EMPLOYEE_ID = C.MANAGER_ID WHERE C.DISTANCE < 20 ) SELECT EMPLOYEE_ID, EMPLOYEE_NAME, MANAGER_ID, DISTANCE FROM MANAGER_CHAIN ORDER BY DISTANCE;
A level column answers “how far is this row from the anchor?” A path answers “which route reached this row?” Both are useful for displaying and debugging a hierarchy. Cast the anchor path to the intended final type because its inferred length establishes the result type. If the anchor is merely EMPLOYEE_NAME, a later concatenation can exceed that short type. An explicit VARCHAR gives recursive rows room to grow.
123456789101112131415161718192021222324252627282930WITH ORG_PATH ( EMPLOYEE_ID, EMPLOYEE_NAME, MANAGER_ID, TREE_LEVEL, DISPLAY_PATH ) AS ( SELECT E.EMPLOYEE_ID, E.EMPLOYEE_NAME, E.MANAGER_ID, 0, CAST(E.EMPLOYEE_NAME AS VARCHAR(2000)) FROM EMPLOYEE_HIERARCHY E WHERE E.EMPLOYEE_ID = :START_EMPLOYEE UNION ALL SELECT C.EMPLOYEE_ID, C.EMPLOYEE_NAME, C.MANAGER_ID, P.TREE_LEVEL + 1, P.DISPLAY_PATH CONCAT ' > ' CONCAT C.EMPLOYEE_NAME FROM ORG_PATH P INNER JOIN EMPLOYEE_HIERARCHY C ON C.MANAGER_ID = P.EMPLOYEE_ID WHERE P.TREE_LEVEL < 20 ) SELECT EMPLOYEE_ID, EMPLOYEE_NAME, TREE_LEVEL, DISPLAY_PATH FROM ORG_PATH ORDER BY TREE_LEVEL, DISPLAY_PATH;
Names make a friendly display path but are not necessarily unique or suitable for deterministic tree ordering. A production report can carry two paths: a readable name path and a sortable key path made from consistently formatted identifiers. Always order in the outer query. The order in which Db2 generates recursive rows is an implementation detail, not a breadth-first or depth-first presentation guarantee.
A cycle occurs when traversal returns to a node already visited on the same route. The simplest case is an employee whose manager is the same employee. A direct inequality can reject that error, but it cannot detect A to B to C to A. A maximum level eventually stops the loop, yet it still creates repeated rows and unnecessary work before stopping. A visited-key path detects the repeated node as soon as it appears.
1234567891011121314151617181920212223242526272829303132333435363738WITH ORG_SAFE ( EMPLOYEE_ID, EMPLOYEE_NAME, MANAGER_ID, TREE_LEVEL, VISITED_KEYS ) AS ( SELECT E.EMPLOYEE_ID, E.EMPLOYEE_NAME, E.MANAGER_ID, 0, CAST( '/' CONCAT VARCHAR(E.EMPLOYEE_ID) CONCAT '/' AS VARCHAR(3000) ) FROM EMPLOYEE_HIERARCHY E WHERE E.EMPLOYEE_ID = :START_EMPLOYEE UNION ALL SELECT C.EMPLOYEE_ID, C.EMPLOYEE_NAME, C.MANAGER_ID, P.TREE_LEVEL + 1, P.VISITED_KEYS CONCAT VARCHAR(C.EMPLOYEE_ID) CONCAT '/' FROM ORG_SAFE P INNER JOIN EMPLOYEE_HIERARCHY C ON C.MANAGER_ID = P.EMPLOYEE_ID WHERE P.TREE_LEVEL < :MAX_LEVEL AND C.EMPLOYEE_ID <> C.MANAGER_ID AND LOCATE( '/' CONCAT VARCHAR(C.EMPLOYEE_ID) CONCAT '/', P.VISITED_KEYS ) = 0 ) SELECT EMPLOYEE_ID, EMPLOYEE_NAME, MANAGER_ID, TREE_LEVEL FROM ORG_SAFE ORDER BY TREE_LEVEL, EMPLOYEE_ID;
Delimiters are essential. Searching for 12 without boundaries could incorrectly match 112. The visited path is route-specific, so two legitimate branches can still reach the same node when the data represents a graph rather than a strict tree. Decide whether the required result is a set of routes or a set of unique nodes. If unique nodes are needed, deduplicate after expansion only when doing so preserves the business meaning.
Do not treat “the data should terminate” as a stopping policy. Db2 for z/OS does not use the SQL Server-style MAXRECURSION query hint. Instead, carry an integer counter, increment it by a constant, and compare it with a constant or host variable in the iterative fullselect. Choose a ceiling above the deepest valid business hierarchy but low enough to contain malformed data. An organization expected to have at most 12 reporting levels might use 20; a bill of materials may require a different measured limit.
A counter is a safety ceiling, not complete cycle detection. Combine it with visited-key logic and data-quality controls. Also remember that depth is not the only growth factor. If every row has ten children, three levels can produce 1,110 descendant rows and six levels can produce more than a million. Restrict the anchor and prune branches even when the maximum depth is small.
IBM places deliberate restrictions on fullselects that participate in a recursion cycle. Keeping the recursive member focused on “find the next rows” makes its behavior easier to reason about. For the exact rules of your release, check the Db2 for z/OS SQL Reference, especially after changing application compatibility levels.
123456789101112131415161718192021WITH ORG_TOTALS ( EMPLOYEE_ID, MANAGER_ID, TREE_LEVEL ) AS ( SELECT EMPLOYEE_ID, MANAGER_ID, 0 FROM EMPLOYEE_HIERARCHY WHERE EMPLOYEE_ID = :START_EMPLOYEE UNION ALL SELECT C.EMPLOYEE_ID, C.MANAGER_ID, P.TREE_LEVEL + 1 FROM ORG_TOTALS P INNER JOIN EMPLOYEE_HIERARCHY C ON C.MANAGER_ID = P.EMPLOYEE_ID WHERE P.TREE_LEVEL < 20 ) SELECT TREE_LEVEL, COUNT(*) AS EMPLOYEE_COUNT FROM ORG_TOTALS GROUP BY TREE_LEVEL ORDER BY TREE_LEVEL;
COUNT and GROUP BY are valid here because the outer query runs after ORG_TOTALS has expanded the hierarchy. This separation is a reusable design rule: recurse to discover rows, then summarize, rank, enrich, and format the finished result.
Without an outer ORDER BY, Db2 can return rows in any order. Ordering by TREE_LEVEL and a node key produces a level-grouped, breadth-like report. Ordering by a sortable path keeps descendants near their parent and produces a depth-like display. A path based on names can shift when names change and can collide when names repeat, so stable fixed-width keys are safer for deterministic reports.
Keep traversal and presentation separate. Do not infer that the engine visits rows in the same order shown by a previous execution. Access paths can change after RUNSTATS, a bind, maintenance, or a Db2 upgrade. The final ORDER BY is the only SQL guarantee of result sequence.
A practical debugging method is to lower MAX_LEVEL to one and inspect the direct children. Increase it one level at a time while returning TREE_LEVEL, parent key, child key, and VISITED_KEYS. This exposes a reversed join, an overly broad anchor, an incorrect delimiter, or a data cycle quickly. Once the traversal is correct, restore the business limit and remove diagnostic columns that consumers do not need.
Db2 did not recognize the expected counter increment and limiting predicate used to guard against infinite recursion. Review the iterative member and add an integer counter plus a direct comparison to a constant or host variable. Do not silence the warning without proving termination.
The recursion cycle contains an unsupported construct or invalid reference pattern. Common causes include an aggregate, GROUP BY, HAVING, outer join, subquery, multiple recursive references, or ordering inside the cycle. Move post-processing to the outer query and simplify the iterative member.
A member uses SELECT DISTINCT or the recursion cycle does not use an ALL set operator. Use SELECT or SELECT ALL and connect members with UNION ALL. Add intentional deduplication outside recursion only when the requirement calls for it.
The anchor expression established a type too short or otherwise incompatible with the recursive expression. Cast the anchor path to an adequately sized VARCHAR and give numeric calculations sufficient precision and scale.
Imagine an organization chart made from cards. First, you put one manager's card on the table. That is the anchor. Then you follow a rule: “For every new card, find all cards that say this person is their manager.” That rule is the recursive member. UNION ALL puts the first card and every card you discover into one result.
You also carry two notes. One says how many steps you have taken, so you stop after a safe number. The other lists every card already seen on your current route, so a bad chart cannot send you in a circle. At the end, you arrange the cards with ORDER BY because the order in which you found them is not guaranteed.
Yes. Multiple seeds produce a forest or several independent traversals. Carry a ROOT_ID through the recursive result when every generated row must remain associated with its originating root.
No. UNION ALL preserves generated rows and avoids duplicate-elimination work. Prevent cycles by checking visited keys. If valid graph routes reach the same node more than once, decide whether those routes are meaningful before deduplicating the outer result.
Fixed self-joins can be simpler when depth is guaranteed to be very small and constant. Frequently queried, mostly static hierarchies can benefit from a maintained path or closure table, although those designs make updates more complex. Choose with measured workload evidence.
Use the Db2 for z/OS SQL Reference topic for common-table-expression syntax and restrictions, plus IBM's task topic “Creating recursive SQL by using common table expressions.” Confirm that the documentation version matches the subsystem release and application compatibility level.
1. What does the anchor member of a recursive CTE do?
2. Which set operator connects the anchor and recursive members?
3. Why is a visited-key path stronger than only a depth limit?
4. Where should an aggregate such as SUM normally be applied?
5. What guarantees a stable presentation order for recursive results?
Understand the statement-scoped named result that provides the foundation for recursion
Apply recursive traversal to organization charts, paths, and bills of materials
Support parent-child joins and verify recursive query performance with EXPLAIN