A common table expression, usually shortened to CTE, gives a name to an intermediate query result for the duration of one SQL statement. CTEs can simplify a long query, remove repeated subqueries, and traverse hierarchies when they refer back to themselves. This tutorial explains ordinary and recursive CTEs in DB2 for z/OS, including the anchor member, recursive member, UNION ALL, termination, cycle protection, and practical hierarchy queries.
DB2 also uses the word WITH in statement isolation clauses such as WITH UR, WITH CS, WITH RS, and WITH RR. Those clauses do not create CTEs. Their position and job are different, so this page separates the two meanings before showing how both can appear in the same statement.
SQL grammar assigns several jobs to common words. In this topic, the most important reading skill is noticing where WITH appears. A CTE begins before the main fullselect and is followed by a name, an optional column list, AS, and a parenthesized query. An isolation clause follows the query and contains a two-letter isolation code. The first form creates a name that the SQL can reference; the second controls how DB2 coordinates the read with concurrent units of work.
| SQL form | Usual position | Purpose |
|---|---|---|
| WITH PAYROLL AS (...) SELECT ... | Beginning of the statement | Defines a CTE: a named query result used by the statement |
| ... SELECT ... WITH UR | Near the end of the query | Selects statement isolation; does not define a table expression |
| ... SELECT ... WITH RR | Near the end of the query | Requests repeatable-read isolation for the statement |
123456789WITH ACTIVE_DEPTS AS ( SELECT DEPTNO, DEPTNAME FROM DEPARTMENT WHERE ACTIVE_FLAG = 'Y' ) SELECT DEPTNO, DEPTNAME FROM ACTIVE_DEPTS ORDER BY DEPTNO WITH CS;
This statement contains both forms. WITH ACTIVE_DEPTS AS (...) at the front defines the CTE. WITH CS at the end requests cursor-stability isolation. ACTIVE_DEPTS can be named in the FROM clause; CS cannot. Conversely, the CTE does not override the package isolation. Reading from left to right and identifying each clause by its position prevents a common source of confusion.
An ordinary, non-recursive CTE behaves like a temporary named query inside one statement. It does not add an object to the DB2 catalog, require CREATE authority, survive COMMIT, or hold data for a later statement. It is closer to a named nested table expression than to a declared global temporary table. The optimizer remains free to merge the CTE into the surrounding query or materialize an intermediate result when that is more efficient. Therefore, do not assume that writing a CTE forces a particular access path or guarantees that its query runs exactly once.
12345678910111213141516WITH DEPARTMENT_TOTALS (WORKDEPT, EMPLOYEE_COUNT, TOTAL_PAY) AS ( SELECT WORKDEPT, COUNT(*), SUM(SALARY) FROM EMPLOYEE GROUP BY WORKDEPT ) SELECT D.DEPTNO, D.DEPTNAME, T.EMPLOYEE_COUNT, T.TOTAL_PAY FROM DEPARTMENT D INNER JOIN DEPARTMENT_TOTALS T ON T.WORKDEPT = D.DEPTNO WHERE T.EMPLOYEE_COUNT >= 5 ORDER BY T.TOTAL_PAY DESC;
DEPARTMENT_TOTALS gives a meaningful name to the grouped result. Its explicit column list names the three output columns independently of the expressions inside the fullselect. The main SELECT then joins that result to DEPARTMENT as if it were a table. This structure lets a reader understand the aggregation first and the business filtering second.
One WITH clause can define multiple CTEs separated by commas. A later CTE can reference one defined earlier in the list, which makes it possible to build a query in stages. The CTE names exist only inside the containing statement. Choose names that do not accidentally hide an unqualified table name, and qualify base tables when ambiguity is possible.
12345678910111213WITH DEPARTMENT_TOTALS AS ( SELECT WORKDEPT, SUM(SALARY) AS TOTAL_PAY FROM EMPLOYEE GROUP BY WORKDEPT ), LARGE_DEPARTMENTS AS ( SELECT WORKDEPT, TOTAL_PAY FROM DEPARTMENT_TOTALS WHERE TOTAL_PAY > 1000000 ) SELECT WORKDEPT, TOTAL_PAY FROM LARGE_DEPARTMENTS ORDER BY TOTAL_PAY DESC;
CTEs improve organization, but they should still represent useful logical steps. Splitting every expression into a separate CTE can make a short query harder to follow. Use one when a result is reused, when a stage has a clear business meaning, or when recursion is needed. Use EXPLAIN rather than guessing whether a rewritten query performs better.
A CTE becomes recursive when its definition refers to its own name. DB2 starts with one or more seed rows, uses those rows to find the next set, then repeats the recursive step with each newly produced set. This continues until the recursive member returns no rows. The conceptual process is iteration, although the SQL remains declarative: the optimizer and database engine decide how to execute it.
1234567891011121314151617181920212223WITH ORG_TREE (EMP_ID, EMP_NAME, MANAGER_ID, TREE_LEVEL) AS ( SELECT E.EMP_ID, E.EMP_NAME, E.MANAGER_ID, 0 FROM EMPLOYEE_ORG E WHERE E.EMP_ID = 100 UNION ALL SELECT C.EMP_ID, C.EMP_NAME, C.MANAGER_ID, P.TREE_LEVEL + 1 FROM ORG_TREE P INNER JOIN EMPLOYEE_ORG C ON C.MANAGER_ID = P.EMP_ID WHERE P.TREE_LEVEL < 20 ) SELECT EMP_ID, EMP_NAME, MANAGER_ID, TREE_LEVEL FROM ORG_TREE ORDER BY TREE_LEVEL, EMP_ID;
The anchor finds employee 100 and labels that row level zero. The recursive member treats rows from ORG_TREE as parents and joins EMPLOYEE_ORG to find direct reports. Those reports become input for another iteration, producing grandchildren and deeper descendants. The condition P.TREE_LEVEL < 20 is a defensive maximum-depth limit. If the organization has no deeper rows, normal termination occurs earlier because the join produces an empty set.
UNION ALL preserves every row generated by the anchor and each iteration. It also avoids the duplicate-elimination work associated with UNION. More importantly, duplicate elimination is not a substitute for cycle detection. Two legitimate employees can share similar attributes, while one bad relationship can revisit the same employee through a longer path. Recursive logic should identify visited keys directly rather than hoping UNION will make malformed data safe.
You will often see examples from other products written as WITH RECURSIVE tree AS (...). Within the DB2 family, exact grammar differs by platform and release. Db2 for z/OS commonly expresses recursion with WITH and a self-referencing CTE; it does not require the RECURSIVE keyword in the standard z/OS pattern. Db2 LUW and other SQL implementations can show WITH RECURSIVE explicitly.
The important idea is not the extra keyword: it is the self-reference from the recursive member back to the CTE. When moving SQL between Db2 for z/OS, Db2 LUW, Db2 for i, or a different database, verify the target SQL reference. Do not add or remove RECURSIVE solely because an internet example uses a different product. The anchor, UNION ALL, recursive member, compatible result columns, and termination behavior remain the concepts to recognize.
A well-formed tree naturally terminates at leaf rows because no children match. Production data is not always a well-formed tree. An employee can accidentally manage itself, A can point to B while B points back to A, or a longer relationship can loop through several rows. Without a safeguard, the recursive member can keep rediscovering keys, consume work file space, and eventually fail after excessive processing.
1234567891011121314151617181920212223242526272829WITH ORG_TREE (EMP_ID, EMP_NAME, MANAGER_ID, TREE_LEVEL, VISITED_PATH) AS ( SELECT E.EMP_ID, E.EMP_NAME, E.MANAGER_ID, 0, CAST('/' CONCAT DIGITS(E.EMP_ID) CONCAT '/' AS VARCHAR(2000)) FROM EMPLOYEE_ORG E WHERE E.EMP_ID = 100 UNION ALL SELECT C.EMP_ID, C.EMP_NAME, C.MANAGER_ID, P.TREE_LEVEL + 1, P.VISITED_PATH CONCAT DIGITS(C.EMP_ID) CONCAT '/' FROM ORG_TREE P INNER JOIN EMPLOYEE_ORG C ON C.MANAGER_ID = P.EMP_ID WHERE P.TREE_LEVEL < 20 AND LOCATE( '/' CONCAT DIGITS(C.EMP_ID) CONCAT '/', P.VISITED_PATH ) = 0 ) SELECT EMP_ID, EMP_NAME, MANAGER_ID, TREE_LEVEL FROM ORG_TREE ORDER BY TREE_LEVEL, EMP_ID;
VISITED_PATH records each key with slash delimiters. Before accepting a child, LOCATE checks whether that delimited key already exists in the path. The exact CAST length and key formatting must fit the application's maximum depth and key type. This example combines cycle detection with a depth cap because independent safeguards provide better protection than either one alone.
Isolation controls what a reader can observe while other units of work insert, update, or delete rows. If a statement does not specify an isolation clause, the package or plan bind option supplies the isolation level. A trailing clause can request an allowed statement-level choice. Isolation does not change the rows generated by CTE logic in a single-user world; it changes the concurrency guarantees under which DB2 reads the base data.
| Clause | Name | Read stability | Tradeoff |
|---|---|---|---|
| WITH UR | Uncommitted read | Can see uncommitted and rolled-back changes | Least read locking, weakest correctness guarantees |
| WITH CS | Cursor stability | Protects the current cursor row; earlier rows can change | Common balance for transactional browsing |
| WITH RS | Read stability | Qualifying rows remain stable; new qualifying rows can appear | More repeatability and locking; phantoms remain possible |
| WITH RR | Repeatable read | Rows and qualifying ranges remain stable; prevents phantoms | Strongest consistency with the greatest contention risk |
Uncommitted read can observe a value written by another unit of work before that writer commits. If the writer rolls back, the value seen by the report never becomes official. A multi-row result can also combine states that never existed as one committed snapshot. UR can be useful for approximate monitoring, exploratory counts, and reports where waiting is more harmful than seeing transient data. It is a poor choice for balances, inventory commitments, compliance decisions, or any result that drives an irreversible action.
Cursor stability protects the row currently positioned under a cursor according to DB2 locking rules, but rows read earlier can change after the cursor moves. Repeating the search within the same unit of work can therefore return changed values or a different set. CS usually offers a practical compromise for online browsing because it limits how long many read locks are retained while still avoiding dirty reads.
Read stability retains protection for rows that qualified when read, so another unit of work cannot change or delete those qualifying rows until the reader's unit of work ends. However, another transaction can insert a new row that satisfies the predicate. Repeating the query can therefore reveal a phantom. RS is useful when existing qualifying rows must not change during a business operation but preventing every new qualifying row would impose unnecessary contention.
Repeatable read protects the qualifying result and prevents phantom rows, giving the strongest of these four guarantees. The cost is broader or longer-lived locking and a greater chance that writers wait behind the reader. Large scans at RR can have a serious concurrency impact. Keep the unit of work short, index search predicates appropriately, and choose RR because the business rule requires it, not because stronger sounds safer.
123456789101112131415161718WITH ORG_TREE (EMP_ID, MANAGER_ID, TREE_LEVEL) AS ( SELECT EMP_ID, MANAGER_ID, 0 FROM EMPLOYEE_ORG WHERE EMP_ID = 100 UNION ALL SELECT C.EMP_ID, C.MANAGER_ID, P.TREE_LEVEL + 1 FROM ORG_TREE P INNER JOIN EMPLOYEE_ORG C ON C.MANAGER_ID = P.EMP_ID WHERE P.TREE_LEVEL < 20 ) SELECT EMP_ID, MANAGER_ID, TREE_LEVEL FROM ORG_TREE ORDER BY TREE_LEVEL, EMP_ID WITH RS;
Again, the first WITH introduces ORG_TREE; the final WITH RS selects read stability. Choosing RS does not fix a cycle, and adding cycle detection does not choose an isolation level. They address separate risks: recursive safeguards control traversal of the data, while isolation controls concurrent changes to data being read.
Use a CTE when naming an intermediate result clarifies the statement, when the same logical result is referenced more than once, or when the query must walk an unknown number of hierarchy levels. A nested table expression can be simpler for a short result used once. A view is appropriate when many statements need a stable, authorized abstraction. A temporary table can be preferable when several separate statements must share staged data, when indexes on staged results are valuable, or when you need explicit control over when the intermediate result is populated.
Recursive SQL is especially valuable when hierarchy depth varies. If the business model guarantees exactly three levels, fixed self-joins might be easier to tune and understand. For an org chart, category tree, bill of materials, or dependency graph with variable depth, recursion avoids guessing how many joins to write. Always inspect EXPLAIN output and test representative depth and fan-out because a tiny sample hierarchy can hide the cost of a broad production tree.
Imagine sorting a box of family photographs. A normal CTE is a sticky note that says “photos from this summer,” so you can use that pile by name while finishing one project. When the project is over, the sticky note and pile name disappear. A recursive CTE starts with one person, the anchor, then repeatedly asks, “Who are this person's children?” The recursive member keeps asking for the next generation until nobody new is found.
A cycle safeguard is a checklist of people already visited, so a mixed-up photo saying that a child is also their own grandparent does not make you walk in circles forever. WITH UR, CS, RS, and RR are different rules about whether someone else may rearrange the photos while you look. UR lets you peek before they finish; RR asks everyone to leave the whole relevant arrangement alone until you are done.
1. Where does the WITH clause that defines a CTE appear?
2. What are the two essential members of a recursive CTE?
3. Why should a hierarchy query include cycle and depth safeguards?
4. Which isolation level allows a query to read uncommitted changes?
5. What is the main tradeoff when moving from CS toward RS or RR?