A common table expression, or CTE, gives a useful name to a result table inside one SQL statement. Instead of placing every join, aggregation, filter, and calculation in one deeply nested expression, you can define a result with WITH and then refer to that result by name. IBM describes a CTE as being like a temporary view that is defined and used for the duration of a statement.
This tutorial concentrates on writing ordinary, non-recursive CTEs correctly in Db2 for z/OS. You will learn the basic form, explicit column names, multiple dependent CTEs, valid use with an INSERT fullselect, optimizer choices, alternatives, and common errors. Recursive CTEs build on the same syntax but add special rules and deserve separate treatment.
Before writing a CTE, be comfortable with SELECT, FROM, WHERE, GROUP BY, joins, and aliases. A CTE does not replace those clauses; it organizes them. The ordinary form begins with WITH, followed by a table-like name, optional result-column names, AS, and a parenthesized fullselect. The main fullselect follows the closing parenthesis.
1234567891011121314WITH ACTIVE_EMPLOYEES AS ( SELECT EMPNO, FIRSTNME, LASTNAME, WORKDEPT FROM EMP WHERE EMP_STATUS = 'A' ) SELECT EMPNO, FIRSTNME, LASTNAME, WORKDEPT FROM ACTIVE_EMPLOYEES ORDER BY WORKDEPT, LASTNAME;
Read this statement in two stages. ACTIVE_EMPLOYEES defines the rows and columns that form the intermediate result. The main SELECT consumes that named result. The name is visible only within this statement: another SELECT submitted afterward cannot refer to ACTIVE_EMPLOYEES. No CREATE statement runs, no catalog row is added, and there is nothing to DROP.
You can name output columns inside the fullselect with AS aliases, or place an explicit column list immediately after the CTE name. An explicit list is a compact declaration of the CTE's result shape. It is especially helpful when the fullselect contains aggregate functions, arithmetic, constants, scalar functions, or other expressions whose generated names would be unclear.
12345678910111213141516WITH DEPARTMENT_PAY (DEPT_NO, EMPLOYEE_COUNT, TOTAL_SALARY, AVERAGE_SALARY) AS ( SELECT WORKDEPT, COUNT(*), SUM(SALARY), DECIMAL(AVG(SALARY), 11, 2) FROM EMP GROUP BY WORKDEPT ) SELECT DEPT_NO, EMPLOYEE_COUNT, TOTAL_SALARY, AVERAGE_SALARY FROM DEPARTMENT_PAY WHERE EMPLOYEE_COUNT >= 5 ORDER BY TOTAL_SALARY DESC;
The four names after DEPARTMENT_PAY correspond by position to the four expressions in its fullselect. The number of names must match the number of result columns, and each name in that list must be unique. The data types still come from the corresponding expressions; writing AVERAGE_SALARY in the list does not change its type. The explicit DECIMAL cast controls the type and scale while the column list controls the name.
You could instead write COUNT(*) AS EMPLOYEE_COUNT and similar aliases inside the SELECT. Both forms can be readable. Prefer one consistent style within a CTE. The explicit list is particularly useful when readers should see the whole result contract before reading a long fullselect. Inline aliases can be easier when each name should sit beside the expression that creates it.
One WITH clause can introduce multiple CTEs. Separate definitions with commas; do not repeat WITH before every definition. A later ordinary CTE can reference a CTE defined earlier in the same list. That rule makes ordering important: define source-like results first, transformations second, and the final result last. Each CTE name in the statement must be unique.
12345678910111213141516171819202122232425262728293031WITH PAID_ORDERS AS ( SELECT O.ORDER_ID, O.CUSTOMER_ID, O.ORDER_DATE, O.ORDER_TOTAL FROM CUSTOMER_ORDER O WHERE O.PAYMENT_STATUS = 'PAID' ), CUSTOMER_TOTALS AS ( SELECT CUSTOMER_ID, COUNT(*) AS ORDER_COUNT, SUM(ORDER_TOTAL) AS LIFETIME_VALUE FROM PAID_ORDERS GROUP BY CUSTOMER_ID ), HIGH_VALUE_CUSTOMERS AS ( SELECT CUSTOMER_ID, ORDER_COUNT, LIFETIME_VALUE FROM CUSTOMER_TOTALS WHERE ORDER_COUNT >= 3 AND LIFETIME_VALUE >= 10000 ) SELECT C.CUSTOMER_ID, C.CUSTOMER_NAME, H.ORDER_COUNT, H.LIFETIME_VALUE FROM HIGH_VALUE_CUSTOMERS H INNER JOIN CUSTOMER C ON C.CUSTOMER_ID = H.CUSTOMER_ID ORDER BY H.LIFETIME_VALUE DESC;
PAID_ORDERS has no CTE dependency, so it appears first. CUSTOMER_TOTALS consumes it, and HIGH_VALUE_CUSTOMERS consumes CUSTOMER_TOTALS. The main SELECT adds the customer name only after the qualifying customer IDs are known. This ordering creates a readable top-to-bottom explanation of the data transformation.
Dependency order is not permission to split every clause into a separate CTE. Too many tiny layers force readers to jump between names to reconstruct a simple query. Give each layer a coherent business meaning. If a CTE merely renames SELECT * from another CTE without clarifying or transforming anything, it is probably unnecessary.
IBM notes that a CTE can be referenced many times in its statement and that those references share the CTE's result table. That logical behavior is valuable when two parts of a fullselect must use the same result definition. It also avoids copying a long subquery and risking subtle differences when one copy is later changed.
123456789101112131415WITH TEAM_PAY AS ( SELECT WORKDEPT, SUM(SALARY) AS TOTAL_PAY FROM EMP GROUP BY WORKDEPT ) SELECT A.WORKDEPT, A.TOTAL_PAY, B.WORKDEPT AS COMPARED_DEPT, B.TOTAL_PAY AS COMPARED_PAY FROM TEAM_PAY A INNER JOIN TEAM_PAY B ON B.TOTAL_PAY > A.TOTAL_PAY WHERE A.WORKDEPT = :HV_DEPT ORDER BY B.TOTAL_PAY;
TEAM_PAY appears twice under different correlation names. Logically, both references use the result defined by the same CTE. Do not turn that statement-level rule into a physical promise that Db2 executes the text once or always stores one copy. The optimizer decides how to implement the query while preserving the required result.
A CTE is SQL syntax, not a command to create a work file. Db2 may merge a table expression by incorporating its logic into the surrounding query. Merging can expose predicates and joins to broader optimization. Db2 may instead materialize an intermediate result, typically in a work file, when semantics require it or the estimated cost favors it. Operations such as duplicate-removing set operations, some complex joins, grouping, sorting, and reuse can affect the available choices, but there is no reliable rule that every CTE is materialized or every simple CTE is merged.
This distinction matters when tuning. A developer might expect a CTE predicate to reduce rows before a join, yet Db2 can legally transform the query. Another developer might expect a reused CTE to be calculated only once, yet physical processing remains an access-path decision. Use EXPLAIN to inspect query blocks, access methods, join order, estimated cardinalities, and work-file indicators. IBM's PLAN_TABLE documentation identifies TABLE_TYPE values used to distinguish non-materialized query blocks from work-file materialization in relevant access paths.
These features can all name or contain intermediate query logic, but their scope and operational behavior differ. Select the smallest tool that matches the required lifetime and reuse.
Use a CTE when one statement benefits from a named intermediate result, several parts of a fullselect must share one definition, host variables help construct a result, or recursion is required. It keeps related logic beside the statement and requires no persistent object. It cannot be reused by the next statement.
A derived table, also called a nested table expression, is a parenthesized fullselect in the FROM clause. It is concise when used once and close to its consumer. A CTE is often easier to read when the expression is long, referenced more than once, or one of several named stages. Neither spelling alone guarantees a better access path.
A view is a catalog object that many statements and users can reference. Choose it for a durable abstraction, centralized security model, or shared interface. Creating and changing it requires object management and authorization. A CTE is preferable when general reuse is unnecessary and the definition belongs with one statement.
A temporary table suits a multi-statement process. The application can populate it, read it in later statements, change the staged rows, and sometimes define supporting indexes according to the selected temporary-table design. That explicit lifecycle can be useful, but it adds statements, logging and commit considerations, and operational choices. A CTE keeps the work declarative and lets Db2 optimize one complete statement.
IBM documents common table expressions wherever a fullselect is accepted, including SELECT, SELECT INTO, INSERT, and CREATE VIEW examples. A practical data-change pattern is an INSERT whose source fullselect reads from a CTE. In Db2 for z/OS INSERT syntax, the WITH clause follows the target and target-column list, then the source fullselect consumes the CTE.
12345678910111213141516171819202122INSERT INTO MONTHLY_DEPT_SUMMARY (SUMMARY_MONTH, DEPT_NO, ORDER_COUNT, SALES_TOTAL) WITH MONTH_ORDERS AS ( SELECT DEPT_NO, ORDER_ID, ORDER_TOTAL FROM CUSTOMER_ORDER WHERE ORDER_DATE >= :HV_MONTH_START AND ORDER_DATE < :HV_NEXT_MONTH ), DEPT_TOTALS AS ( SELECT DEPT_NO, COUNT(*) AS ORDER_COUNT, SUM(ORDER_TOTAL) AS SALES_TOTAL FROM MONTH_ORDERS GROUP BY DEPT_NO ) SELECT :HV_MONTH_START, DEPT_NO, ORDER_COUNT, SALES_TOTAL FROM DEPT_TOTALS;
This single statement derives monthly rows and inserts them into the summary table. The CTE is the source, not the target. Validate target-column types, nullability, generated-column rules, duplicate-key behavior, and authorization exactly as you would for any INSERT fullselect.
Do not copy a WITH UPDATE or WITH DELETE example from Db2 LUW, another database, or a newer grammar and assume it is valid on your Db2 for z/OS function level. IBM's common-table-expression guidance specifically presents the feature as an alternative to a view when positioned updates or deletes are not used. If an UPDATE or DELETE must identify rows through complex logic, verify the target statement's z/OS SQL reference and use its supported subquery, searched condition, MERGE, or data-change-table-reference forms. Platform name and release matter.
Good CTE names make SQL read like a data explanation. Put foundational row selection before aggregation, aggregation before business qualification, and enrichment joins near the point where their columns are needed. Select only useful columns rather than carrying SELECT * through every stage. A narrow result is easier to understand and can reduce unnecessary data movement if the access path materializes it.
Readability and performance are partners, not automatic synonyms. A clear structure makes predicates, join keys, and cardinality changes easier to review, but each CTE boundary can also hide an accidental many-to-many join, an unnecessary DISTINCT, or an aggregation performed before the right filter. Annotate unusual business rules in source control, not every obvious SQL keyword. Then confirm behavior with EXPLAIN and production-like measurements.
If an explicit list has three names but the fullselect returns four columns, the CTE result cannot be mapped. Duplicate names in the explicit list are also invalid. Count expressions carefully, make names unique, and remember that SELECT * can change when the underlying table definition changes.
1234567891011121314151617-- Incorrect: two names for three result columns WITH DEPT_PAY (DEPT_NO, TOTAL_PAY) AS ( SELECT WORKDEPT, COUNT(*), SUM(SALARY) FROM EMP GROUP BY WORKDEPT ) SELECT * FROM DEPT_PAY; -- Correct WITH DEPT_PAY (DEPT_NO, EMPLOYEE_COUNT, TOTAL_PAY) AS ( SELECT WORKDEPT, COUNT(*), SUM(SALARY) FROM EMP GROUP BY WORKDEPT ) SELECT DEPT_NO, EMPLOYEE_COUNT, TOTAL_PAY FROM DEPT_PAY;
A non-recursive CTE cannot depend on a later definition. Move the dependency earlier. Also ensure every CTE name is defined only once. If a CTE name matches a catalog table name, it can hide that unqualified object within the statement; qualify the base table with its schema or choose a less confusing CTE name.
Every CTE fullselect is enclosed in parentheses. A comma separates adjacent CTEs, but there is no comma between the last CTE and the main fullselect. Format each definition consistently so punctuation is visible.
Joining a CTE to a table that exposes the same column name can produce ambiguity. Assign correlation names and qualify references, such as C.CUSTOMER_ID and T.CUSTOMER_ID. This also tells reviewers which source owns each value.
A CTE does not remove duplicates unless its fullselect explicitly does so. Before adding DISTINCT, inspect join predicates and expected relationship cardinality. An incomplete join often multiplies rows, and DISTINCT can hide the defect while adding sort or hashing cost.
Check whether predicates remain indexable, whether statistics describe the data, whether a result was materialized, and whether estimated row counts match reality. Compare against a derived-table or direct-join form only after confirming logical equivalence. The remedy may be statistics, indexing, predicate design, or join correction rather than removing WITH.
Verification has two parts. First prove correctness with known test data. For each CTE, temporarily make that CTE the source of the final SELECT and inspect its rows. Check boundary dates, null handling, duplicate business keys, empty departments, negative amounts, and host-variable extremes. Reconcile aggregates to a trusted query or independently calculated total.
Second, prove operational suitability. Run EXPLAIN in the target environment and review access paths with your site's established tooling. Confirm matching index use, join order, estimated cardinality, sorts, materialization, parallelism, and work-file demand. Execute with representative volumes and parameter values. A query that is fast for one department can behave differently for all departments.
The IBM Db2 13 for z/OS documentation pages titled “Common table expressions”, “INSERT statement”, and “Using EXPLAIN to determine UNION, INTERSECT, and EXCEPT activity and query rewrite” are primary references for the scope, INSERT grammar, and merge-versus-materialization concepts covered here. Always select the documentation version that matches the subsystem release and active function level.
Imagine making lunch from a crowded kitchen. First you put all the washed vegetables in a bowl and label it WASHED_VEGETABLES. Then you take that bowl, add cheese, and label the new bowl SALAD. Finally you put SALAD on the table. Those labels are like CTE names: they help you describe each part of one lunch-making job.
When lunch is finished, the labels do not become permanent cupboards. Db2 may not even use real bowls; it can combine steps if that is faster, as long as the same lunch appears. A view is more like a recipe posted for everyone to reuse. A temporary table is more like a container kept around while several separate jobs use its contents.
1. How long does a common table expression exist?
2. Which CTE may ORDERS_WITH_CUSTOMERS reference in the same WITH clause?
3. When is an explicit CTE column list especially useful?
4. Does writing a CTE guarantee that Db2 creates a work file for it?
5. Which is a documented Db2 for z/OS use of a CTE with data modification?
6. What is the best first response when a readable CTE rewrite performs differently?
Compare CTEs with parenthesized fullselects used directly in a FROM clause
Choose a durable catalog abstraction when query logic must be reused broadly
Stage and reuse rows across multiple statements in an application process
Verify optimizer decisions, cardinality estimates, joins, and work-file activity