Sunday, July 19, 2026

How We Eliminated DB2 table Contention Across 50 Batch Jobs Using ESP Renewable Resources

During our month-end batch processing cycle, we had nearly 50 batch jobs that updated the same row in a DB2 table.

Individually, these jobs completed within seconds. However, when multiple jobs ran concurrently, they frequently encountered table contention because they were attempting to update the same row simultaneously. As a result, some jobs would abend.
 
Every month-end cycle, we typically experienced 3 to 5 job abends due to this contention. Since the month-end processing window was already tight, every abend introduced delays in the month end cycle and increased operational effort for reruns.
 
The Initial Approach: Mutual Exclusion with NOTWITH
 
To prevent these jobs from running at the same time, we initially considered modifying the schedule so that each job was mutually exclusive with every other job.
 
ESP provides the NOTWITH statement for defining mutually exclusive jobs. This seemed like a viable solution, but implementation quickly became cumbersome.
 
For example:
 
JOB JOB1
   RUN WORKDAYS
   REL JOBXXX
   NOTWITH(JOB2, JOB3, JOB4, ... JOB50)
ENDJOB
 
JOB JOB2
    RUN WORKDAYS
    REL JOBYYY
    NOTWITH(JOB1, JOB3, JOB4, ... JOB50)
ENDJOB
 
With nearly 50 jobs involved, each job needed to reference the other 49 jobs in its NOTWITH list. Maintaining such a configuration would have been difficult, error-prone, and far from elegant.
 
A Better Solution: ESP Renewable Resource
 
Instead of managing a large network of mutual exclusions, we leveraged ESP's RESOURCE functionality.
 
The RESOURCE statement allows jobs to request resources before submission. We defined a virtual renewable resource called MY_TABLE with a maximum count of 1.
 
This effectively represented our DB2 table as a shared resource that only one job could access at a time.
 
Each job was updated as follows:
 
JOB JOB1
    RUN WORKDAYS
    REL JOBXXX
    RESOURCE (1,MY_TABLE)
ENDJOB
 
JOB JOB2
    RUN WORKDAYS
    REL JOBYYY
    RESOURCE (1,MY_TABLE)
ENDJOB
 
...
 
JOB JOB50
    RUN WORKDAYS
    REL JOBZZZ
    RESOURCE (1,MY_TABLE)
ENDJOB
 
How It Works
 
When ESP submits a job that requires a renewable resource, it temporarily allocates the requested resource from the available resource pool.
 
Since MY_TABLE was defined with a count of 1, only one job could obtain the resource at any given time.

  • If the resource is available, the job starts immediately.
  • If the resource is already in use, ESP holds the job until the resource becomes available.
  • Once the job completes, regardless of whether it ends successfully or abends, the resource is automatically returned to the pool.
 
In essence, each job borrows the resource for the duration of its execution, ensuring exclusive access to the shared table while preventing contention.
 
The Results
 
This approach delivered several benefits:

  • Eliminated table update contention between batch jobs.
  • Removed the need for complex and difficult-to-maintain NOTWITH definitions.
  • Simplified scheduling logic significantly.
  • Reduced month-end job abends caused by concurrent updates.
 

Thursday, July 16, 2026

What Happens When You Run SELECT COUNT(*) on a Large DB2 Table?

To better understand what happens behind the scenes when executing a SELECT COUNT(*) query, I analyzed the two largest tables in our DB2 environment. The goal was to determine whether DB2 counts rows by scanning the table itself or by leveraging one of the available indexes.

Scenario 1: Indexes Larger Than the Table

Table_1 had two indexes, both of which were poorly optimized. A large number of table columns had been included in the indexes, resulting in index sizes that exceeded the size of the base table.

Table_1 Statistics

Metric

Value

Row Count

120,632,075

Average Row Length

95 bytes

Used Pages

3,093,397

Space Utilized

13,631,040 KB

 
Index Statistics
 

Index

Allocated Space (KB)

Leaf Pages

Levels

Avg. Key Length

Index_1

15,730,560

3,655,518

6

101

Index_2

13,633,200

3,165,273

5

84

 
DB2 Optimizer Decision
 
When the following query was executed:
 
SELECT COUNT(*) FROM Table_1 WITH UR;
 
DB2 evaluated the available statistics and determined that scanning the table space was more efficient than scanning either index. Since both indexes were larger than the table itself, the optimizer chose a table scan to perform the row count.
 
Scenario 2: Index Smaller Than the Table
 
Table_2 contained seven indexes. Unlike the first scenario, several of these indexes were significantly smaller than the table space, making them potential candidates for an index-only count operation.
 
Table_2 Statistics

Metric

Value

Row Count

1,006,831,073

Average Row Length

30 bytes

Used Pages

8,078,681

Space Utilized

32,506,560 KB

 
Index Statistics
 

Index

Allocated Space (KB)

Leaf Pages

Levels

Avg. Key Length

Index_1

30,412,080

7,507,065

5

18

Index_2

30,412,080

7,507,065

5

18

Index_3

30,412,080

7,507,065

5

18

Index_4

7,341,840

1,726,625

4

5

Index_5

7,341,840

1,731,545

4

11

Index_6

30,412,080

7,507,065

5

18

Index_7

39,849,840

9,773,348

5

24

 
DB2 Optimizer Decision
 
When the following query was executed:
 
SELECT COUNT(*) FROM Table_2 WITH UR;
 
DB2 analyzed the table and index statistics and selected Index_4 as the access path for the count operation. Because Index_4 was substantially smaller than the table space and contained fewer pages to scan, counting the index entries was more efficient than scanning the entire table.
 
Key Takeaway
 
When executing SELECT COUNT(*), DB2 does not automatically scan the underlying table. Instead, the optimizer evaluates available statistics and chooses the most cost-effective access path.

  • If all indexes are as large as—or larger than—the table, DB2 may perform a table space scan.
  • If a smaller, efficient index exists, DB2 may perform an index scan and count index entries instead.

Wednesday, July 15, 2026

Ignoring COBOL File Status Checks Led to Silent Data Loss

 At first glance, it looked like a straightforward COBOL batch program.

It read a sequential file from beginning to end. For each record, it performed a random lookup in a KSDS file. If a matching record existed, it updated the record. If no match was found, it inserted a new record into the KSDS file.

Hidden deep within the program was a critical flaw: after every write operation, the program never checked the file status code to verify whether the write had succeeded.

Everything worked perfectly—until the KSDS file reached its maximum size limit of 4 GB.

Once that limit was reached, every attempt to add a new record failed. The system dutifully generated the message:

IEC070I 034(004)-220

However, because the program never validated the write status, it continued processing records as if every insert had been successful.  

The result?

New records were silently discarded while the batch job completed normally, giving everyone the illusion that everything was working as intended.

The issue remained undetected for months, quietly preventing new data from being added to the KSDS file until someone finally traced the missing records back to the unnoticed write failures.

Key Takeaway : Never assume a file operation succeeds.

After every file read, write, rewrite, delete, or open operation:

Check the file status code.
Handle error conditions appropriately.
Log and escalate failures when necessary.

Friday, July 10, 2026

Coordinating Db2 and MQ Updates Using RRS in a COBOL Batch Program

When a batch COBOL program performs INSERT, UPDATE, or DELETE operations on a Db2 table and also executes MQPUT operations to an IBM MQ queue, the commit/rollback processing becomes more complex.

In such a scenario, issuing a Db2 COMMIT commits only the Db2 changes; it does not commit the MQ messages. Similarly, issuing MQCMIT commits only the MQ operations and has no effect on pending Db2 updates. Each resource manager commits its own work independently but does not coordinate with other resource managers.

This is where Resource Recovery Services (RRS) becomes essential.

RRS acts as a central transaction coordinator for resource managers such as Db2 and IBM MQ. When an application requests a commit or rollback, RRS coordinates all participating resource managers using the standard two-phase commit protocol, ensuring that updates across all resources are committed or rolled back as a single unit of work.

The following sample COBOL program updates both Db2 and MQ resources and uses the RRS services to commit the changes atomically.

Unlike a traditional batch Db2 COBOL program, an application that uses RRS must explicitly establish and terminate the Db2 connection and thread within the program itself.

When we compile the program, we need to add "ATTACH(RRSAF)" option in the DB2 precompiler step. This instructs Db2 to use the RRS Attachment Facility (RRSAF) for transaction coordination.

After doing all the DB2, MQ updates, COBOL program need to invoke RSS API "SRRCMIT" to initiate the two phase COMMIT process.     

The following RSS Adapter/stub modules must be link-edited to the COBOL module:

CSQBRSTB - MQ RRS Adapter available in MQ SCSQLOAD library
DSNRLI   - DB2 RRS Adapter available in Db2 SDSNLOAD library
ATRSCSS  - available in SYS1.CSSLIB library

Since the COBOL program explicitly manages the Db2 connection through RRSAF, it can be executed like a regular Batch COBOL program.

Below is the sample run JCL.

//STEP01 EXEC PGM=DB2MQPGM,                              
//STEPLIB  DD DSN=xxxx.DB2.SDSNEXIT,DISP=SHR          
//         DD DSN=xxxx.DB2.SDSNLOAD,DISP=SHR          
//         DD DSN=xxxx.MQ.SCSQANLE,DISP=SHR            
//         DD DSN=xxxx.MQ.SCSQAUTH,DISP=SHR            
//         DD DSN=USER.LOADLIB,DISP=SHR                  
//*                                                      
//SYSOUT   DD SYSOUT=*                                    

Below is the sample COBOL program that invoke RRS services to commit the changes in both DB2 and MQ.

       IDENTIFICATION DIVISION.
       PROGRAM-ID.    DB2MQPGM.
       DATA DIVISION.
       WORKING-STORAGE SECTION.

       01 RRSAF-FIELDS.
      * DB2 subsystem name for IDENTIFY
           05 SSNM          PIC X(04).
      * Correlation ID for SIGNON
           05 CORRID        PIC X(12).
      * Accounting token for SIGNON
           05 ACCTTKN       PIC X(22).
      * Accounting interval for SIGNON
           05 ACCTINT       PIC X(06).
      * DB2 plan name for CREATE THREAD
           05 PLAN          PIC X(08).
      * Collection ID for CREATE THREAD.
      * IF PLAN contains a plan name, not used.
           05 COLLID        PIC X(18).
      * Controls SIGNON after CREATE THREAD
           05 REUSE         PIC X(08).
      *  Action that application takes based
      *  on return code from RRSAF
           05 CONTROL1      PIC X(08).
      ****************** VARIABLES SET BY DB2 *******************************
      *  DB2 startup ECB
           05 STARTECB      PIC X(04).
      *  DB2 termination ECB
           05 TERMECB       PIC X(04).
      *  Address of environment info block
           05 EIBPTR        PIC X(04).
      *  Address of release info block
           05 RIBPTR        PIC X(04).
      ****************************** CONSTANTS ******************************
      * CONTROL value: Everything OK
           05 CONTINUE1     PIC X(08) VALUE 'CONTINUE'.
      * Name of RRSAF servicS
           05 IDFYFN        PIC X(18) VALUE 'IDENTIFY'.
           05 SGNONFN       PIC X(18) VALUE 'SIGNON'.
           05 CRTHRDFN      PIC X(18) VALUE 'CREATE THREAD'.
           05 TRMTHDFN      PIC X(18) VALUE 'TERMINATE THREAD'.
           05 TMIDFYFN      PIC X(18) VALUE 'TERMINATE IDENTIFY'.
           05 XLATFN        PIC X(18) VALUE 'TRANSLATE'.
           05 RETCODE       PIC X(04).
           05 REASCODE      PIC X(04).

       01  WS-WORK-AREAS.
           05  WS-ERROR-CODE            PIC S9(04)  VALUE ZEROS COMP.
           05  PROCESS-REC-COUNT        PIC S9(4) COMP.

       01 WS-SQLCODE                      PIC ----9.
           EXEC SQL
             INCLUDE SQLCA
           END-EXEC.

        01 OUT-MSG-LEN              PIC S9(9) COMP.

        01 MQ-Q-NAME              PIC X(48)      VALUE SPACES.
        01 MQ-HOBJ                PIC S9(9) COMP VALUE 0.
        01 MQ-OPEN-OPTS           PIC S9(9) COMP VALUE 0.
        01 MQ-CLOSE-OPTS          PIC S9(9) COMP VALUE 0.

        01 MQ-CONSTANTS.
            COPY CMQV.
        01 MQ-OBJ-DESC.
            COPY CMQODV.
        01 MQ-MSG-DESC.
            COPY CMQMDV.
        01 MQ-PUT-MSG-OPTS.
            COPY CMQPMOV.
        01 MQ-GET-MSG-OPTS.
            COPY CMQGMOV.

      * -------------------------------------------------------
      * Queue manager connection name and handle...
      * -------------------------------------------------------
        01 MQ-QM-NAME               PIC X(48)      VALUE SPACES.
        01 MQ-HCONN                 PIC S9(9) COMP-5.
        01 MQ-PROC                  PIC X(08).

      * -------------------------------------------------------
      * Queue manager op return codes and like things...
      * -------------------------------------------------------
        01 MQ-RETURN-STUFF.
            10 MQ-RC                PIC S9(9) COMP VALUE 0.
            10 MQ-RC-N9             PIC 9(9)       VALUE 0.
            10 MQ-RSN               PIC S9(9) COMP VALUE 0.
            10 MQ-RSN-N9            PIC 9(9)       VALUE 0.
            10 MQ-MSG               PIC X(256)     VALUE SPACES.

       PROCEDURE DIVISION.
       0000-MAINLINE.

           PERFORM 1000-INITIALIZATION
              THRU 1000-INITIALIZATION-EXIT.

           PERFORM 2000-PROCESS
              THRU 2000-EXIT
             VARYING PROCESS-REC-COUNT FROM 1 BY 1
             UNTIL PROCESS-REC-COUNT > 100

           PERFORM 3000-TERMINATION
              THRU 3000-TERMINATION-EXIT.

       0000-MAINLINE-EXIT.
           GOBACK.

       1000-INITIALIZATION.

           MOVE 'XXXX'     TO MQ-QM-NAME.
           MOVE 'TEST.MQ'  TO MQ-Q-NAME.

           CALL 'MQCONN' USING
                MQ-QM-NAME
                MQ-HCONN
                MQ-RC
                MQ-RSN

           IF MQ-RC = MQCC-FAILED
              MOVE 'MQCONN'    TO MQ-PROC
              GO TO 999-ERROR
           END-IF.

           MOVE MQOO-OUTPUT            TO MQ-OPEN-OPTS
           ADD  MQOO-FAIL-IF-QUIESCING TO MQ-OPEN-OPTS
           MOVE MQOT-Q    TO MQOD-OBJECTTYPE
           MOVE MQ-Q-NAME TO MQOD-OBJECTNAME

           CALL 'MQOPEN' USING
               MQ-HCONN
               MQ-OBJ-DESC
               MQ-OPEN-OPTS
               MQ-HOBJ
               MQ-RC
               MQ-RSN

           IF MQ-RC = MQCC-FAILED
              MOVE 'MQOPEN'    TO MQ-PROC
              GO TO 999-ERROR
           END-IF.

      ****************************** IDENTIFY **********************
           MOVE 'DB2A' TO SSNM
           CALL  'DSNRLI' USING
                  IDFYFN SSNM RIBPTR EIBPTR TERMECB STARTECB
                  RETCODE REASCODE
           IF RETCODE NOT = LOW-VALUES
              DISPLAY IDFYFN RETCODE REASCODE
              CALL  'DSNRLI' USING XLATFN SQLCA RETCODE REASCODE
              MOVE SQLCODE   TO WS-SQLCODE
              DISPLAY ' IDENTIFY ' WS-SQLCODE SQLERRM
           END-IF
      ***************************** SIGNON *************************
           CALL  'DSNRLI' USING
                  SGNONFN CORRID ACCTTKN ACCTINT
                  RETCODE REASCODE
           IF RETCODE NOT = LOW-VALUES
              DISPLAY SGNONFN RETCODE REASCODE
              CALL  'DSNRLI' USING XLATFN SQLCA RETCODE REASCODE
              MOVE SQLCODE   TO WS-SQLCODE
              DISPLAY 'SIGNON    ' WS-SQLCODE SQLERRM
           END-IF
      *************************** CREATE THREAD ********************
           MOVE 'TESTPLAN' TO PLAN
           CALL  'DSNRLI' USING
                  CRTHRDFN PLAN COLLID REUSE
                  RETCODE REASCODE.
           IF RETCODE NOT = LOW-VALUES
              DISPLAY CRTHRDFN RETCODE REASCODE
              CALL  'DSNRLI' USING XLATFN SQLCA RETCODE REASCODE
              MOVE SQLCODE   TO WS-SQLCODE
              DISPLAY 'CRE THRD  ' WS-SQLCODE SQLERRM
           END-IF.

       1000-INITIALIZATION-EXIT.
           EXIT.

       2000-PROCESS.

           EXEC SQL
               INSERT INTO TEST_TABLE
               ..............
               ..............  
           END-EXEC
 
           IF SQLCODE NOT = ZERO
              MOVE SQLCODE   TO WS-SQLCODE
              DISPLAY ' INSERT FAILED ' WS-SQLCODE
              PERFORM 9000-ABEND THRU 9000-ABEND-EXIT
           END-IF.

           COMPUTE MQPMO-OPTIONS
                 = MQPMO-FAIL-IF-QUIESCING
                 + MQPMO-SYNCPOINT

           MOVE LENGTH OF DATA-TO-BE-INSERTED
              TO OUT-MSG-LEN

           CALL 'MQPUT' USING
                MQ-HCONN
                MQ-HOBJ
                MQ-MSG-DESC
                MQ-PUT-MSG-OPTS
                OUT-MSG-LEN
                DATA-TO-BE-INSERTED
                MQ-RC
                MQ-RSN

           IF MQ-RC = MQCC-FAILED
              MOVE 'MQPUT'     TO MQ-PROC
              GO TO 999-ERROR
           END-IF.

       2000-EXIT.
           EXIT.


       3000-TERMINATION.

           CALL 'SRRCMIT' USING RETCODE
           DISPLAY 'SRRCMIT : ' RETCODE
      *    call 'SRRBACK' USING RETCODE
      *    DISPLAY 'SRRBACK : ' RETCODE

           CALL 'MQCLOSE' USING
               MQ-HCONN
               MQ-HOBJ
               MQ-CLOSE-OPTS
               MQ-RC
               MQ-RSN.

           IF MQ-RC = MQCC-FAILED
              DISPLAY 'MQCLOSE'
              DISPLAY 'MQ-RC: ' MQ-RC
           END-IF.

            CALL 'MQDISC' USING
                 MQ-HCONN
                 MQ-RC
                 MQ-RSN.

           IF MQ-RC = MQCC-FAILED
              DISPLAY 'MQDISC'
              DISPLAY 'MQ-RC: ' MQ-RC
           END-IF.

      ************************ TERMINATE THREAD ***********************
            CALL  'DSNRLI' USING TRMTHDFN
                   RETCODE REASCODE
            IF RETCODE NOT = LOW-VALUES
               DISPLAY TRMTHDFN RETCODE REASCODE
               CALL  'DSNRLI' USING XLATFN SQLCA RETCODE REASCODE
               MOVE SQLCODE   TO WS-SQLCODE
               DISPLAY 'TRM THRD  ' WS-SQLCODE SQLERRM
            END-IF.
      ************************ TERMINATE IDENTIFY *********************
            CALL  'DSNRLI' USING TMIDFYFN
                   RETCODE REASCODE.
            IF RETCODE NOT = LOW-VALUES
               DISPLAY TMIDFYFN RETCODE REASCODE
               CALL  'DSNRLI' USING XLATFN SQLCA RETCODE REASCODE
               MOVE SQLCODE   TO WS-SQLCODE
               DISPLAY 'TRM IDNTY ' WS-SQLCODE SQLERRM
            END-IF.

       3000-TERMINATION-EXIT.
           EXIT.


       9000-ABEND.

           CALL 'ILBOABN0' USING WS-ERROR-CODE.

       9000-ABEND-EXIT.
           EXIT.

       999-ERROR.
           MOVE MQ-RSN    TO MQ-RSN-N9
           DISPLAY 'MQ-PROC: ' MQ-PROC
           DISPLAY 'MQ-RSN : ' MQ-RSN-N9
           GOBACK.

Wednesday, July 8, 2026

When a Simple Action Turned into a Major TSO Logon Problem

 One fine day, several Mainframe users found themselves unable to log in to their TSO sessions. The issue appeared in different forms:

  • For some users, the TSO logon JCL failed with JCL errors during the login process.
  • For others, the TSO logon job was being submitted with another user's LOGON PROC, causing the job to fail.

This issue did not affect all Mainframe users; only a subset of users experienced these login problems.

The TSO LOGON PROCLIB contains a dedicated LOGON PROC for each Mainframe user.

Prior to the start of the issue, a member of the Mainframe Infrastructure team compressed the TSO LOGON PROCLIB using CA PDSMAN. This action may have been taken because the PROCLIB was nearing capacity while new user entries were being added.

Following the compression activity, some users began experiencing the TSO logon issues described above.

The question is: How could compressing the TSO LOGON PROCLIB using CA PDSMAN result in these TSO logon failures ?

Further investigation revealed that the installation was using CA PMO within a Sysplex environment consisting of both PROD and DEV LPARs.

One of CA PMO's functions is to cache the directory entries of frequently accessed PDS datasets, enabling faster retrieval of PDS members when they are subsequently  accessed.  

As a result, the directory information for the TSO LOGON PROCLIB was cached in both the DEV and PROD LPARs.

The CA PDSMAN compression job was executed on the DEV LPAR. CA PMO on the DEV system correctly detected the compression activity and refreshed its cache with the updated directory entries for the TSO LOGON PROCLIB.

CA PMO Documentation states PMO/XSYS is required to propagate PDS updates across LPARs. But PMO/XSYS was NOT ACTIVE in both DEV and PROD LPARs

Hence, the PROD LPAR was not aware that the PROCLIB had been compressed. Consequently, the CA PMO cache on the PROD LPAR continued to reference the pre-compression directory entries.

Because PDS compression reorganizes members and updates directory information, the stale CA PMO cache on the PROD LPAR no longer matched the actual state of the PROCLIB. This mismatch caused TSO logon processing to retrieve incorrect or invalid PROC directory information, resulting in symptoms such as:

  • JCL errors during TSO logon.
  • TSO logon jobs being submitted with the wrong user's LOGON PROC.

In summary, the root cause of the issue was a stale CA PMO directory cache on the PROD LPAR following the CA PDSMAN compression of the TSO LOGON PROCLIB in the DEV LPAR. To fix the issue, CA PMO cache in PROD LPAR was refreshed.