Appendix C: DB2 SQL Reference for COBOL

This appendix covers what COBOL programmers need to know about DB2 SQL — the statements you embed in programs, the host variable declarations that bridge COBOL and DB2 data types, the SQLCA fields you check after every call, and the SQLCODEs you will encounter in production. It is not a general SQL tutorial; it assumes you understand relational concepts and focuses on the COBOL-specific mechanics.

All examples target DB2 for z/OS (V12/V13). Syntax differences for DB2 LUW (Linux/Unix/Windows) are noted where they affect COBOL programmers.


Host Variable Declarations

Host variables are COBOL data items used in SQL statements, prefixed with a colon (:) inside EXEC SQL blocks. They must be declared in the Working-Storage or Local-Storage Section, within an EXEC SQL BEGIN DECLARE SECTION / EXEC SQL END DECLARE SECTION block (recommended but not strictly required with the DB2 coprocessor).

       WORKING-STORAGE SECTION.

       EXEC SQL BEGIN DECLARE SECTION END-EXEC.

       01  HV-EMPLOYEE-ID         PIC S9(09) COMP.
       01  HV-FIRST-NAME          PIC X(30).
       01  HV-LAST-NAME           PIC X(30).
       01  HV-SALARY              PIC S9(07)V99 COMP-3.
       01  HV-SALARY-NULL         PIC S9(04) COMP.
       01  HV-HIRE-DATE           PIC X(10).
       01  HV-TIMESTAMP-VAL       PIC X(26).
       01  HV-DESCRIPTION.
           49  HV-DESCRIPTION-LEN PIC S9(04) COMP.
           49  HV-DESCRIPTION-TXT PIC X(1000).

       EXEC SQL END DECLARE SECTION END-EXEC.

       EXEC SQL INCLUDE SQLCA END-EXEC.

DB2 to COBOL Data Type Mapping

DB2 Type COBOL Declaration Notes
SMALLINT PIC S9(04) COMP 2-byte binary
INTEGER PIC S9(09) COMP 4-byte binary
BIGINT PIC S9(18) COMP 8-byte binary
DECIMAL(p,s) PIC S9(p-s)V9(s) COMP-3 Packed decimal
FLOAT (double) COMP-2 8-byte floating point
REAL (single) COMP-1 4-byte floating point
CHAR(n) PIC X(n) Fixed-length character
VARCHAR(n) Level-49 structure (see below) Variable-length character
DATE PIC X(10) Format: 'YYYY-MM-DD'
TIME PIC X(08) Format: 'HH.MM.SS'
TIMESTAMP PIC X(26) Format: 'YYYY-MM-DD-HH.MM.SS.NNNNNN'
CLOB LOB locator or host variable Requires special handling
BLOB LOB locator Requires special handling

VARCHAR Host Variables

DB2 VARCHAR columns map to a two-field structure at level 49:

       01  HV-ADDRESS.
           49  HV-ADDRESS-LEN     PIC S9(04) COMP.
           49  HV-ADDRESS-TXT     PIC X(200).

DB2 recognizes the level-49 structure as a VARCHAR host variable. On SELECT, DB2 sets HV-ADDRESS-LEN to the actual length of the data. On INSERT/UPDATE, you must set HV-ADDRESS-LEN before executing the SQL statement.

Null Indicator Variables

Every nullable column needs an indicator variable — a PIC S9(04) COMP field specified after the host variable with a colon separator:

           EXEC SQL
               SELECT SALARY
                 INTO :HV-SALARY :HV-SALARY-NULL
                 FROM EMPLOYEE
                WHERE EMPLOYEE_ID = :HV-EMPLOYEE-ID
           END-EXEC
Indicator Value Meaning
0 Value is not null; host variable contains valid data
-1 Value is null; host variable contents are undefined
-2 Value is null (result of numeric conversion error)
> 0 String was truncated; indicator = original length

Always check the indicator before using the host variable. Using a host variable when its indicator is -1 is a logic error — the data is garbage.

           IF HV-SALARY-NULL < 0
               MOVE 0 TO WS-DISPLAY-SALARY
           ELSE
               MOVE HV-SALARY TO WS-DISPLAY-SALARY
           END-IF

To insert or update a null value:

           MOVE -1 TO HV-SALARY-NULL
           EXEC SQL
               UPDATE EMPLOYEE
                  SET SALARY = :HV-SALARY :HV-SALARY-NULL
                WHERE EMPLOYEE_ID = :HV-EMPLOYEE-ID
           END-EXEC

DML Statements

SELECT INTO (Single-Row Fetch)

           EXEC SQL
               SELECT FIRST_NAME, LAST_NAME, SALARY,
                      HIRE_DATE
                 INTO :HV-FIRST-NAME,
                      :HV-LAST-NAME,
                      :HV-SALARY :HV-SALARY-NULL,
                      :HV-HIRE-DATE
                 FROM EMPLOYEE
                WHERE EMPLOYEE_ID = :HV-EMPLOYEE-ID
           END-EXEC

           EVALUATE SQLCODE
               WHEN 0
                   CONTINUE
               WHEN +100
                   SET WS-NOT-FOUND TO TRUE
               WHEN OTHER
                   PERFORM 9500-DB2-ERROR
           END-EVALUATE

SELECT INTO must return exactly one row. If it returns zero rows, SQLCODE = +100. If it returns more than one row, SQLCODE = -811. Both are common production conditions that your code must handle.

INSERT

      * Single-row INSERT
           EXEC SQL
               INSERT INTO EMPLOYEE
                   (EMPLOYEE_ID, FIRST_NAME, LAST_NAME,
                    SALARY, HIRE_DATE)
               VALUES
                   (:HV-EMPLOYEE-ID,
                    :HV-FIRST-NAME,
                    :HV-LAST-NAME,
                    :HV-SALARY :HV-SALARY-NULL,
                    CURRENT DATE)
           END-EXEC

      * INSERT from SELECT
           EXEC SQL
               INSERT INTO EMPLOYEE_ARCHIVE
                   (EMPLOYEE_ID, FIRST_NAME, LAST_NAME,
                    ARCHIVE_DATE)
               SELECT EMPLOYEE_ID, FIRST_NAME, LAST_NAME,
                      CURRENT DATE
                 FROM EMPLOYEE
                WHERE TERMINATION_DATE < :HV-CUTOFF-DATE
           END-EXEC

After INSERT from SELECT, SQLERRD(3) in the SQLCA contains the number of rows inserted.

UPDATE

      * Positioned UPDATE (with cursor)
           EXEC SQL
               UPDATE EMPLOYEE
                  SET SALARY = :HV-NEW-SALARY
                WHERE CURRENT OF CSR-EMPLOYEE
           END-EXEC

      * Searched UPDATE
           EXEC SQL
               UPDATE EMPLOYEE
                  SET SALARY = SALARY * 1.05
                WHERE DEPARTMENT_ID = :HV-DEPT-ID
                  AND SALARY < :HV-SALARY-CAP
           END-EXEC

SQLERRD(3) contains the number of rows updated. If it is 0 and SQLCODE is 0, the WHERE clause matched no rows — this is not an error, but it may be a logic issue your program should detect.

DELETE

      * Positioned DELETE
           EXEC SQL
               DELETE FROM EMPLOYEE
                WHERE CURRENT OF CSR-EMPLOYEE
           END-EXEC

      * Searched DELETE
           EXEC SQL
               DELETE FROM EMPLOYEE_ARCHIVE
                WHERE ARCHIVE_DATE < :HV-PURGE-DATE
           END-EXEC

SQLERRD(3) contains the number of rows deleted.

MERGE (Upsert)

           EXEC SQL
               MERGE INTO CUSTOMER_MASTER AS T
               USING (VALUES (:HV-CUST-ID,
                              :HV-CUST-NAME,
                              :HV-CUST-ADDR))
                   AS S(CUST_ID, CUST_NAME, CUST_ADDR)
               ON T.CUSTOMER_ID = S.CUST_ID
               WHEN MATCHED THEN
                   UPDATE SET CUSTOMER_NAME = S.CUST_NAME,
                              CUSTOMER_ADDR = S.CUST_ADDR,
                              LAST_UPDATE   = CURRENT TIMESTAMP
               WHEN NOT MATCHED THEN
                   INSERT (CUSTOMER_ID, CUSTOMER_NAME,
                           CUSTOMER_ADDR, CREATED_DATE)
                   VALUES (S.CUST_ID, S.CUST_NAME,
                           S.CUST_ADDR, CURRENT DATE)
           END-EXEC

MERGE replaces the common pattern of "SELECT to check existence, then INSERT or UPDATE." It is atomic and significantly more efficient, especially in batch programs processing large volumes.


Cursors

Declaring Cursors

      * Basic read-only cursor
           EXEC SQL
               DECLARE CSR-EMPLOYEE CURSOR FOR
               SELECT EMPLOYEE_ID, FIRST_NAME, LAST_NAME,
                      SALARY
                 FROM EMPLOYEE
                WHERE DEPARTMENT_ID = :HV-DEPT-ID
                ORDER BY LAST_NAME, FIRST_NAME
           END-EXEC

      * Cursor for update
           EXEC SQL
               DECLARE CSR-EMP-UPDATE CURSOR FOR
               SELECT EMPLOYEE_ID, SALARY
                 FROM EMPLOYEE
                WHERE DEPARTMENT_ID = :HV-DEPT-ID
                  AND SALARY < :HV-SALARY-CAP
                FOR UPDATE OF SALARY
           END-EXEC

      * WITH HOLD cursor (survives COMMIT)
           EXEC SQL
               DECLARE CSR-LARGE-BATCH CURSOR WITH HOLD FOR
               SELECT EMPLOYEE_ID, FIRST_NAME, LAST_NAME
                 FROM EMPLOYEE
                WHERE STATUS = 'A'
                ORDER BY EMPLOYEE_ID
           END-EXEC

      * Scrollable cursor
           EXEC SQL
               DECLARE CSR-SCROLL SCROLL CURSOR FOR
               SELECT EMPLOYEE_ID, LAST_NAME
                 FROM EMPLOYEE
                WHERE DEPARTMENT_ID = :HV-DEPT-ID
                ORDER BY LAST_NAME
           END-EXEC

DECLARE is declarative, not executable. It does not generate any runtime code. The cursor is not "created" at the DECLARE — it is opened at the OPEN. DECLARE must appear before the first reference to the cursor in the source, but it does not need to be in the Procedure Division at the point of execution.

Opening and Fetching

           EXEC SQL OPEN CSR-EMPLOYEE END-EXEC
           IF SQLCODE NOT = 0
               PERFORM 9500-DB2-ERROR
           END-IF

           PERFORM UNTIL SQLCODE = +100
               EXEC SQL
                   FETCH CSR-EMPLOYEE
                    INTO :HV-EMPLOYEE-ID,
                         :HV-FIRST-NAME,
                         :HV-LAST-NAME,
                         :HV-SALARY :HV-SALARY-NULL
               END-EXEC
               EVALUATE SQLCODE
                   WHEN 0
                       PERFORM 2100-PROCESS-EMPLOYEE
                   WHEN +100
                       CONTINUE
                   WHEN OTHER
                       PERFORM 9500-DB2-ERROR
               END-EVALUATE
           END-PERFORM

           EXEC SQL CLOSE CSR-EMPLOYEE END-EXEC

Multi-Row FETCH

       WORKING-STORAGE SECTION.
       01  WS-FETCH-COUNT         PIC S9(09) COMP VALUE 100.
       01  WS-ROWS-FETCHED        PIC S9(09) COMP.
       01  WS-EMP-ARRAY.
           05  WS-EMP-ROW OCCURS 100 TIMES.
               10  WS-ARR-EMP-ID  PIC S9(09) COMP.
               10  WS-ARR-LNAME   PIC X(30).
               10  WS-ARR-SALARY  PIC S9(07)V99 COMP-3.

      * In Procedure Division:
           EXEC SQL
               FETCH CSR-EMPLOYEE
               FOR :WS-FETCH-COUNT ROWS
               INTO :WS-ARR-EMP-ID,
                    :WS-ARR-LNAME,
                    :WS-ARR-SALARY
           END-EXEC
           MOVE SQLERRD(3) TO WS-ROWS-FETCHED

Multi-row FETCH retrieves up to N rows in a single DB2 call. This dramatically reduces the overhead of row-at-a-time processing. In batch programs processing millions of rows, multi-row FETCH with an array size of 100-1000 can reduce elapsed time by 30-50%.

SQLERRD(3) tells you how many rows were actually fetched. When the result set is exhausted, SQLCODE = +100 and SQLERRD(3) contains the count of the final (partial) batch.

Scrollable Cursor FETCH

           EXEC SQL FETCH FIRST CSR-SCROLL
               INTO :HV-EMPLOYEE-ID, :HV-LAST-NAME END-EXEC

           EXEC SQL FETCH NEXT CSR-SCROLL
               INTO :HV-EMPLOYEE-ID, :HV-LAST-NAME END-EXEC

           EXEC SQL FETCH PRIOR CSR-SCROLL
               INTO :HV-EMPLOYEE-ID, :HV-LAST-NAME END-EXEC

           EXEC SQL FETCH LAST CSR-SCROLL
               INTO :HV-EMPLOYEE-ID, :HV-LAST-NAME END-EXEC

           EXEC SQL FETCH ABSOLUTE :HV-ROW-NUM CSR-SCROLL
               INTO :HV-EMPLOYEE-ID, :HV-LAST-NAME END-EXEC

           EXEC SQL FETCH RELATIVE -5 CSR-SCROLL
               INTO :HV-EMPLOYEE-ID, :HV-LAST-NAME END-EXEC

Scrollable cursors are useful for paging interfaces (CICS BMS or web services) but carry significant overhead. DB2 materializes the entire result set. Use them only when you truly need bidirectional navigation.


COMMIT and ROLLBACK

           EXEC SQL COMMIT END-EXEC

           EXEC SQL ROLLBACK END-EXEC

Always check SQLCODE after COMMIT. A COMMIT failure (rare but possible, e.g., SQLCODE -911 deadlock during commit) means your changes did not persist.

Commit Frequency in Batch

       01  WS-COMMIT-COUNTER      PIC S9(09) COMP VALUE 0.
       01  WS-COMMIT-INTERVAL     PIC S9(09) COMP VALUE 1000.

      * In processing loop:
           ADD 1 TO WS-COMMIT-COUNTER
           IF WS-COMMIT-COUNTER >= WS-COMMIT-INTERVAL
               EXEC SQL COMMIT END-EXEC
               IF SQLCODE NOT = 0
                   PERFORM 9500-DB2-ERROR
               END-IF
               MOVE 0 TO WS-COMMIT-COUNTER
           END-IF

Committing every 500-5000 rows is typical for batch programs. Too infrequent: long-running locks, massive log usage, and long recovery times if the program abends. Too frequent: commit overhead becomes significant. The right interval depends on your workload — 1000 is a sensible starting point.

If using WITH HOLD cursors, the cursor position is preserved across COMMIT. Without WITH HOLD, all cursors are closed at COMMIT, and you must reposition.


EXPLAIN

           EXEC SQL
               EXPLAIN PLAN SET QUERYNO = 100 FOR
               SELECT E.EMPLOYEE_ID, E.LAST_NAME, D.DEPT_NAME
                 FROM EMPLOYEE E
                 JOIN DEPARTMENT D
                   ON E.DEPARTMENT_ID = D.DEPARTMENT_ID
                WHERE E.SALARY > :HV-SALARY-THRESHOLD
           END-EXEC

EXPLAIN populates the PLAN_TABLE with the access path DB2 chose. In practice, you more commonly run EXPLAIN through tools (DB2 EXPLAIN, Data Studio, or SPUFI) rather than embedding it in COBOL. But understanding what EXPLAIN tells you — table scans vs. index access, sort operations, join methods — is essential for performance tuning.


Key SQL Functions

Scalar Functions

Function Example Result
COALESCE(a, b) COALESCE(SALARY, 0) Returns first non-null value
VALUE(a, b) VALUE(SALARY, 0) Synonym for COALESCE
NULLIF(a, b) NULLIF(STATUS, 'X') NULL if a = b, else a
CASE See below Conditional expression
CHAR(date, fmt) CHAR(HIRE_DATE, ISO) Date as character string
DATE(expr) DATE('2026-03-17') String to date
TIMESTAMP(expr) TIMESTAMP('2026-03-17-10.30.00') String to timestamp
CURRENT DATE Today's date
CURRENT TIME Current time
CURRENT TIMESTAMP Current timestamp
YEAR(date) YEAR(HIRE_DATE) Extract year
MONTH(date) MONTH(HIRE_DATE) Extract month
DAY(date) DAY(HIRE_DATE) Extract day
DAYS(date) DAYS(CURRENT DATE) - DAYS(HIRE_DATE) Date to integer (for arithmetic)
SUBSTR(str, pos, len) SUBSTR(NAME, 1, 10) Substring
LENGTH(str) LENGTH(DESCRIPTION) String length
STRIP(str) STRIP(NAME, BOTH) Remove blanks
TRIM(str) TRIM(NAME) Remove blanks (SQL standard)
UPPER(str) UPPER(LAST_NAME) Uppercase
LOWER(str) LOWER(LAST_NAME) Lowercase
DIGITS(num) DIGITS(SALARY) Numeric to character (no sign, no decimal)
DECIMAL(expr, p, s) DECIMAL(AMOUNT, 9, 2) Cast to decimal
INTEGER(expr) INTEGER(SALARY) Cast to integer
CAST(expr AS type) CAST(EMP_ID AS VARCHAR(10)) General cast

CASE Expression

SELECT EMPLOYEE_ID,
       CASE
           WHEN SALARY > 100000 THEN 'HIGH'
           WHEN SALARY > 50000  THEN 'MEDIUM'
           ELSE 'LOW'
       END AS SALARY_BAND
  FROM EMPLOYEE

Aggregate Functions

Function Purpose
COUNT(*) Count all rows
COUNT(DISTINCT col) Count distinct values
SUM(col) Sum
AVG(col) Average
MIN(col) Minimum
MAX(col) Maximum
STDDEV(col) Standard deviation
VARIANCE(col) Variance

SQLCA Fields

The SQLCA (SQL Communication Area) is your window into what DB2 did. Include it with EXEC SQL INCLUDE SQLCA END-EXEC.

Field Type Purpose
SQLCODE S9(09) COMP Return code (0 = success, +100 = not found, negative = error)
SQLSTATE X(05) Standardized 5-character state code
SQLERRMC X(70) Error message tokens (separated by X'FF')
SQLERRD(1) S9(09) COMP DBMS-dependent
SQLERRD(2) S9(09) COMP DBMS-dependent
SQLERRD(3) S9(09) COMP Number of rows affected (INSERT/UPDATE/DELETE/FETCH)
SQLERRD(4) S9(09) COMP Estimated cost (after PREPARE)
SQLERRD(5) S9(09) COMP Position of error in SQL statement
SQLERRD(6) S9(09) COMP DBMS-dependent
SQLWARN0 X(01) 'W' if any warning flag is set
SQLWARN1 X(01) 'W' if string truncation occurred
SQLWARN2 X(01) 'W' if null values eliminated from aggregate
SQLWARN3 X(01) 'W' if number of result columns != number of host variables
SQLWARN4 X(01) 'W' if UPDATE/DELETE without WHERE (db-wide)
SQLWARN5 X(01) 'W' if SQL statement is not valid for this environment
SQLWARN6 X(01) 'W' if date arithmetic adjusted
SQLWARN7 X(01) 'W' if decimal truncation

Standard SQLCODE Check Pattern

       9500-DB2-ERROR.
           MOVE SQLCODE     TO WS-DISP-SQLCODE
           MOVE SQLERRMC    TO WS-DISP-SQLERRMC
           MOVE SQLERRD(3)  TO WS-DISP-ROWS

           DISPLAY 'DB2 ERROR - SQLCODE: ' WS-DISP-SQLCODE
           DISPLAY 'SQLERRMC: ' WS-DISP-SQLERRMC
           DISPLAY 'ROWS AFFECTED: ' WS-DISP-ROWS

           EXEC SQL ROLLBACK END-EXEC

           MOVE 16 TO RETURN-CODE
           STOP RUN.

In production, replace DISPLAY with proper error logging — write to an error file, update an error table, or call a standard error-handling subprogram.


Common SQLCODEs

These are the SQLCODEs you will encounter most frequently in production COBOL programs. Learn them by heart.

Positive (Warnings / Expected Conditions)

SQLCODE SQLSTATE Meaning Action
+0 00000 Successful execution Continue
+100 02000 Row not found (SELECT INTO, FETCH) or no more rows Handle as normal end-of-data; not an error

Negative (Errors)

SQLCODE SQLSTATE Meaning Common Cause Action
-180 22007 Invalid date/time/timestamp value String is not a valid date; e.g., '2026-02-30' Fix the data or the formatting logic
-181 22007 Value not valid for date/time/timestamp Similar to -180, subtly different context Check date arithmetic results
-305 22002 Null value with no indicator variable SELECT returned NULL but no indicator variable was coded Add indicator variable to the INTO clause
-501 24501 Cursor not open FETCH or CLOSE on an unopened cursor; or cursor closed by COMMIT without WITH HOLD Check cursor state; add WITH HOLD if committing mid-cursor
-803 23505 Duplicate key on INSERT or UPDATE Unique index or primary key violation Check for pre-existing row; use MERGE if appropriate
-805 51002 DBRM not found in plan or package Package not bound, or plan/package mismatch Rebind the plan or package; check collection ID
-811 21000 SELECT INTO returned more than one row Missing or insufficient WHERE clause predicates Add predicates or use a cursor
-818 51003 Timestamp mismatch between precompile and bind Program recompiled but not rebound Rebind the package
-904 57011 Resource unavailable Tablespace in STOP or COPY-pending status; or resource limit exceeded Check tablespace status with -DISPLAY DATABASE; wait for utility completion
-911 40001 Deadlock or timeout, rollback occurred Two programs locked resources in conflicting order Retry the transaction; review locking strategy
-913 57033 Deadlock or timeout, NO rollback Similar to -911 but application must handle rollback Issue ROLLBACK explicitly, then retry or fail

Additional Important SQLCODEs

SQLCODE Meaning Notes
-117 Number of host variables doesn't match columns Count your INTO variables
-204 Object not found Table/view doesn't exist or wrong qualifier
-206 Column not found Typo in column name or wrong table
-302 Host variable assignment error Data conversion failure; check types and sizes
-304 Value out of range for host variable Numeric overflow
-532 Delete restricted by referential constraint Foreign key prevents delete
-551 Authorization failure No privilege on the object
-802 Arithmetic overflow or divide by zero Check your data and calculations
-922 Authorization failure (connection) Plan execute authority missing
-923 DB2 connection not established DB2 subsystem not active
-924 DB2 connection error Internal connection problem

SQLCODE vs. SQLSTATE

SQLCODE is IBM-specific and DB2-specific. SQLSTATE is SQL standard (ISO/ANSI) and portable across database vendors. In practice, most COBOL shops use SQLCODE because the codes are more specific and are deeply embedded in existing documentation and procedures. If you need portability, check SQLSTATE instead.

SQLSTATE Class Meaning
00 Success
01 Warning
02 No data (row not found)
21 Cardinality violation
22 Data exception
23 Constraint violation
24 Invalid cursor state
40 Transaction rollback
42 Syntax or access rule violation
51 Invalid application state
57 Resource not available

Static vs. Dynamic SQL

Static SQL

All SQL in the examples above is static — the SQL text is known at compile time, precompiled into a DBRM, and bound into a package. Static SQL has predictable performance (the access path is determined at BIND time) and is the standard for production batch and CICS programs.

Dynamic SQL

       01  HV-SQL-STMT            PIC X(500).
       01  HV-EMP-ID              PIC S9(09) COMP.
       01  HV-LAST-NAME           PIC X(30).

      * EXECUTE IMMEDIATE (no result set)
           MOVE 'DELETE FROM TEMP_TABLE WHERE STATUS = ''X'''
               TO HV-SQL-STMT
           EXEC SQL
               EXECUTE IMMEDIATE :HV-SQL-STMT
           END-EXEC

      * PREPARE and EXECUTE (with parameters)
           MOVE 'SELECT LAST_NAME FROM EMPLOYEE WHERE
      -         'EMPLOYEE_ID = ?'
               TO HV-SQL-STMT
           EXEC SQL
               PREPARE STMT1 FROM :HV-SQL-STMT
           END-EXEC
           EXEC SQL
               EXECUTE STMT1 INTO :HV-LAST-NAME
               USING :HV-EMP-ID
           END-EXEC

      * PREPARE with cursor
           EXEC SQL
               PREPARE STMT2 FROM :HV-SQL-STMT
           END-EXEC
           EXEC SQL
               DECLARE CSR-DYNAMIC CURSOR FOR STMT2
           END-EXEC
           EXEC SQL OPEN CSR-DYNAMIC USING :HV-EMP-ID
           END-EXEC

Dynamic SQL is necessary when the SQL text varies at runtime (ad-hoc queries, variable predicates, utility operations). The access path is determined at PREPARE time (not BIND time), so performance is less predictable. Use parameter markers (?) instead of string concatenation to prevent SQL injection and allow access path caching.


DCLGEN (Declaration Generator)

DCLGEN generates COBOL host variable declarations from DB2 table definitions:

//DCLGEN   EXEC PGM=DSNTIAD,PARM='...'

Or in SPUFI / Data Studio:

DCLGEN TABLE(EMPLOYEE)
       LIBRARY(MY.DCLGEN.LIB(EMPLOYEE))
       ACTION(REPLACE)
       LANGUAGE(COBOL)
       STRUCTURE(DCL-EMPLOYEE)

This produces a copybook with the table's column declarations mapped to COBOL host variables. Always use DCLGEN rather than hand-coding host variables — it guarantees that your declarations match the actual table structure.

Include the generated copybook:

           EXEC SQL INCLUDE EMPLOYEE END-EXEC

Multi-Row INSERT

       01  WS-INS-COUNT           PIC S9(09) COMP VALUE 100.
       01  WS-INS-ARRAY.
           05  WS-INS-ROW OCCURS 100 TIMES.
               10  WS-INS-EMP-ID  PIC S9(09) COMP.
               10  WS-INS-LNAME   PIC X(30).
               10  WS-INS-SALARY  PIC S9(07)V99 COMP-3.

           EXEC SQL
               INSERT INTO EMPLOYEE_STAGING
                   (EMPLOYEE_ID, LAST_NAME, SALARY)
               VALUES (:WS-INS-EMP-ID,
                       :WS-INS-LNAME,
                       :WS-INS-SALARY)
               FOR :WS-INS-COUNT ROWS
               NOT ATOMIC CONTINUE ON SQLEXCEPTION
           END-EXEC

NOT ATOMIC CONTINUE ON SQLEXCEPTION means DB2 attempts to insert all rows and reports failures individually rather than rolling back the entire batch. Check SQLERRD(3) for the number of successful inserts.

Multi-row INSERT, like multi-row FETCH, dramatically reduces the overhead of row-at-a-time processing. Use both in batch programs.


Useful DB2 Commands for COBOL Developers

These are not SQL — they are DB2 subsystem commands issued from the operator console, SPUFI, or DSN session:

Command Purpose
-DISPLAY DATABASE(dbname) SPACENAME(tsname) Check tablespace status
-DISPLAY THREAD(*) Show active threads
-DISPLAY BUFFERPOOL(BPname) DETAIL Buffer pool statistics
-START DATABASE(dbname) SPACENAME(tsname) ACCESS(RW) Start a stopped tablespace
BIND PACKAGE(collection) MEMBER(dbrm) ACT(REP) Bind a package
REBIND PACKAGE(collection.package) Rebind with current statistics
FREE PACKAGE(collection.package) Free a package
RUNSTATS TABLESPACE(dbname.tsname) TABLE(ALL) INDEX(ALL) Update catalog statistics

Running RUNSTATS after significant data changes is essential. Without current statistics, the DB2 optimizer makes poor access path decisions, and your program runs ten times slower than it should.


SQLCA Display Subroutine

Every COBOL/DB2 shop should have a standard error-reporting subroutine. Here is a production-quality template:

       9500-DB2-ERROR.
           MOVE SQLCODE TO WS-DISP-SQLCODE
           STRING 'SQLCODE=' DELIMITED BY SIZE
                  WS-DISP-SQLCODE DELIMITED BY SIZE
                  ' SQLSTATE=' DELIMITED BY SIZE
                  SQLSTATE DELIMITED BY SIZE
                  ' SQLERRMC=' DELIMITED BY SIZE
                  SQLERRMC DELIMITED BY SPACES
               INTO WS-ERROR-MSG
               WITH POINTER WS-MSG-PTR
           END-STRING
           DISPLAY WS-ERROR-MSG
           DISPLAY 'SQLERRD(3)=' SQLERRD(3)
                   ' SQLERRD(5)=' SQLERRD(5)

           IF SQLWARN0 = 'W'
               DISPLAY 'WARNINGS: '
                   SQLWARN1 SQLWARN2 SQLWARN3
                   SQLWARN4 SQLWARN5 SQLWARN6
                   SQLWARN7
           END-IF

           EXEC SQL ROLLBACK END-EXEC

           MOVE 16 TO RETURN-CODE
           STOP RUN.

SQLERRD(5) gives the offset within the SQL statement where the error was detected — invaluable for debugging syntax errors in dynamic SQL or long static SQL statements.