Static FETCH loops are only half of production SQL PL. Native procedures also PREPARE strings, return result-set cursors, stage rows in declared global temporary tables, pass arrays, and sometimes CALL themselves. This page ties those DB2 for z/OS techniques together so a BEGIN … END body can work with names and shapes that are not known until run time.
DECLARE CURSOR belongs in the compound statement after SQL variables and before handlers. Each cursor name must be unique in the body. OPEN materializes the result table; FETCH INTO SQL variables or parameters; CLOSE releases the cursor. The cursor is only referenced from that compound (unless it becomes a result set).
1234567891011121314151617DECLARE V_FIRST VARCHAR(12); DECLARE V_LAST VARCHAR(15); DECLARE V_AT_END INT DEFAULT 0; DECLARE C1 CURSOR FOR SELECT FIRSTNME, LASTNAME FROM DSN8C10.EMP WHERE WORKDEPT = P_DEPT; DECLARE CONTINUE HANDLER FOR NOT FOUND SET V_AT_END = 1; OPEN C1; FETCH C1 INTO V_FIRST, V_LAST; WHILE V_AT_END = 0 DO -- process one row FETCH C1 INTO V_FIRST, V_LAST; END WHILE; CLOSE C1;
FOR UPDATE OF on the DECLARE CURSOR select, then UPDATE … WHERE CURRENT OF C1, still works when you need positioned updates. Scrollable cursors (SCROLL, SENSITIVE, and so on) follow the same DECLARE CURSOR rules as embedded SQL; result-set cursors that are returned to a client have extra limits (returned cursors are not scrollable in the usual WITH RETURN path).
12345678910111213CREATE PROCEDURE HR.LIST_DEPT (IN P_DEPT CHAR(3)) LANGUAGE SQL DYNAMIC RESULT SETS 1 READS SQL DATA BEGIN DECLARE C_OUT CURSOR WITH RETURN FOR SELECT EMPNO, LASTNAME, SALARY FROM DSN8C10.EMP WHERE WORKDEPT = P_DEPT; OPEN C_OUT; -- do not CLOSE; caller FETCHes the result set END
Nested CALL: opening another cursor with the same name while one is already open can promote the old cursor to a result-set cursor and open a new working cursor — IBM documents that pattern for procedures that return sets from a recursive or nested call. Prefer distinct names until you are copying that pattern on purpose.
Dynamic SQL is a character string you build, then ask Db2 to prepare at run time. Use it when the table name, a variable IN-list of columns, or a piece of DDL is not fixed at CREATE PROCEDURE time. IBM’s CREATEDEPTTABLE example concatenates a department number into a table name, PREPARE DROP/CREATE/INSERT, and EXECUTE.
| Statement | Role |
|---|---|
| PREPARE s FROM v_sql | Compile the string; s is the statement name |
| EXECUTE s [USING …] | Run a prepared non-SELECT (or singleton) with markers |
| EXECUTE IMMEDIATE v_sql | Prepare+run once; no parameter markers |
| DECLARE c CURSOR FOR s / OPEN c USING | Dynamic SELECT walked with FETCH |
1234567891011DECLARE STMT VARCHAR(200); DECLARE TABLE_NAME VARCHAR(30); SET TABLE_NAME = 'DEPT_' || P_DEPT || '_T'; SET STMT = 'DROP TABLE ' || TABLE_NAME; PREPARE S1 FROM STMT; EXECUTE S1; SET STMT = 'INSERT INTO ' || TABLE_NAME || ' SELECT EMPNO, LASTNAME FROM DSN8C10.EMP WHERE WORKDEPT = ?'; PREPARE S3 FROM STMT; EXECUTE S3 USING P_DEPT;
Critical rule: the string cannot contain SQL variable or parameter names as substitutions the way a COBOL precompiler would. If you write WHERE WORKDEPT = P_DEPT inside the quotes, Db2 looks for a column or the letters P_DEPT, not the parameter. Values go through parameter markers (?) and USING on EXECUTE or OPEN.
Object names (table, column) cannot be parameter markers in standard SQL. Those you concatenate — only after you validate them against an allow-list. Concatenating a user-typed table name is an injection hole. Prefer static SQL whenever the statement shape is known.
Dynamic SELECT: DECLARE C1 CURSOR FOR S1; PREPARE S1 FROM STMT; OPEN C1 USING P_DEPT; FETCH … You can also OPEN using an array variable and an index when the prepared text casts a parameter marker to an array type — IBM shows CAST(? AS INTARRAY)[?] with OPEN C2 USING INTA, INTV.
DEFER PREPARE and DYNAMICRULES on CREATE PROCEDURE / bind options affect when PREPARE runs and which authorization ID dynamic SQL uses. Match your shop’s package defaults before you assume CURRENT SQLID is who dynamic DDL runs as.
When a procedure needs a private work file — explode a bill of materials, stage keys before a join, collect rows for a result set — use a declared global temporary table (DGTT) or a created global temporary table (CGTT) defined once in the catalog.
12345678910111213DECLARE GLOBAL TEMPORARY TABLE SESSION.WORK_EMP (EMPNO CHAR(6) NOT NULL, SALARY DECIMAL(9,2)) ON COMMIT PRESERVE ROWS; INSERT INTO SESSION.WORK_EMP SELECT EMPNO, SALARY FROM DSN8C10.EMP WHERE WORKDEPT = P_DEPT; FOR W AS SELECT EMPNO, SALARY FROM SESSION.WORK_EMP DO SET V_TOTAL = V_TOTAL + W.SALARY; END FOR;
SESSION qualifier is the usual schema for DGTTs. ON COMMIT DELETE ROWS versus PRESERVE ROWS decides whether a COMMIT inside the CALL (or COMMIT ON RETURN) wipes the work table. CGTT (CREATE GLOBAL TEMPORARY TABLE) is a cataloged description; each session still gets its own empty instance. Neither is a substitute for a real history table.
An array is an ordered set of elements of one built-in type. On z/OS you typically CREATE TYPE an array type, then DECLARE SQL variables or parameters of that type. Arrays are for routine parameters, SQL variables, and some function returns — not for a column of a base table.
12345678CREATE TYPE HR.INTARRAY AS INTEGER ARRAY[100]; -- inside a procedure: DECLARE INTA HR.INTARRAY; DECLARE I INT DEFAULT 1; SET INTA[1] = 10; SET INTA[2] = 20; SET I = CARDINALITY(INTA);
| Function | Role |
|---|---|
| CARDINALITY | How many elements are present |
| MAX_CARDINALITY | Ordinary-array maximum from CREATE TYPE |
| ARRAY_AGG | Build an array from a query INTO a variable |
| UNNEST | Present array elements as a table in a SELECT |
| TRIM_ARRAY / ARRAY_DELETE | Drop trailing or selected elements |
You cannot SELECT a column directly into an array. Use ARRAY_AGG in a SELECT INTO or SET, then optionally UNNEST the array as a table in another query. TRIM_ARRAY removes elements from the end of an ordinary array; ARRAY_FIRST / ARRAY_NEXT walk associative indexes.
The GETWEEKENDS IBM sample is the teaching picture: IN array of dates, OUT array of weekend dates, WHILE over CARDINALITY, SET OUT[i] = IN[j]. That is SQL PL arrays without a cursor.
A native SQL procedure may CALL itself (or CALL a sibling that CALLs back). Each CALL is a nested stored-procedure invocation and counts toward Db2’s maximum nest depth. There is no infinite call stack. Always code a base case:
123456789101112131415161718192021CREATE PROCEDURE HR.WALK_MGR (IN P_EMPNO CHAR(6), IN P_DEPTH INTEGER, OUT P_TOP CHAR(6)) LANGUAGE SQL READS SQL DATA BEGIN DECLARE V_MGR CHAR(6); IF P_DEPTH <= 0 OR P_EMPNO IS NULL THEN SET P_TOP = P_EMPNO; ELSE SELECT MGRNO INTO V_MGR FROM DSN8C10.EMP WHERE EMPNO = P_EMPNO; IF V_MGR IS NULL OR V_MGR = P_EMPNO THEN SET P_TOP = P_EMPNO; ELSE CALL HR.WALK_MGR(V_MGR, P_DEPTH - 1, P_TOP); END IF; END IF; END
For bill-of-materials explosions, a WHILE plus DGTT of “keys still to visit” is usually clearer and does not burn nest depth. Recursive common table expressions are a different SQL feature (query recursion), not SQL PL CALL recursion — use the tool that matches the problem. ASUTIME LIMIT still cancels a runaway recursive procedure.
A cursor is your finger moving down a printed list. WITH RETURN means you leave the list on the table for the person who called you, instead of putting it away. Dynamic SQL is writing the homework question on a blank sheet at the last minute, then asking the teacher (Db2) to read it — but you must draw blank boxes (?) for the numbers instead of scribbling variable names in the sentence. A temporary table is a whiteboard only your class can see. An array is a numbered ice-cube tray: slot 1, slot 2, slot 3. Recursion is calling yourself on the phone; you must hang up when the list is empty or the phone company (nest limit) cuts you off.
1. In a native SQL procedure, statement strings for PREPARE must not contain:
2. WITH RETURN on DECLARE CURSOR means:
3. Ordinary SQL PL arrays on z/OS are indexed starting at:
4. A recursive native SQL procedure is limited by:
5. DECLARE GLOBAL TEMPORARY TABLE in a procedure: