Saturday, July 25, 2026

𝗢𝗽𝘁𝗶𝗺𝗶𝘇𝗶𝗻𝗴 𝗮 𝗠𝗶𝗰𝗿𝗼 𝗙𝗼𝗰𝘂𝘀 𝗖𝗢𝗕𝗢𝗟 𝗣𝗿𝗼𝗴𝗿𝗮𝗺: 𝗥𝗲𝗱𝘂𝗰𝗶𝗻𝗴 𝗥𝘂𝗻𝘁𝗶𝗺𝗲 𝗳𝗿𝗼𝗺 𝟲𝟬 𝘁𝗼 𝟭𝟱 𝗠𝗶𝗻𝘂𝘁𝗲𝘀

There was a Micro Focus COBOL program running on a Unix platform that processed millions of records. Every time we modified this program for a business requiremnt and tested it, it ran for about 1 hour, making the development and testing process both time-consuming and frustrating.

To improve the situation, I decided to investigate why the program's execution time was so high.

After analyzing the code, I found that for every input record, the program performed a dynamic read against a VSAM indexed file using a partial key and continued reading until the key changed. Since this operation was executed for every input record, it contributed significantly to the overall processing time.

What caught my attention was that the VSAM file contained only about 10,000 records. Given its relatively small size, I determined that loading the entire file into memory would be far more efficient than repeatedly performing indexed file reads. To minimize memory usage, I stored only the key fields and the specific portion of each record required for processing.

However, there was an additional challenge. The program needed to process all records that matched a partial key. Since binary search works only when the search key uniquely identifies an entry, duplicate partial keys required a different approach. To address this, I designed two working-storage tables:

WS-TABLE1 contained unique partial keys along with the starting and ending positions of their corresponding records in a second table.

WS-TABLE2 contained the full key and the data required for processing.

01 WS-TABLE1.
   05 WS-TBL1-CNT                    PIC S9(08) COMP.
      10 WS-TBL1-ENTRY OCCURS 1 TO 10000 TIMES
     DEPENDING ON WS-TBL1-CNT
ASCENDING KEY WS-PARTIAL-KEY
INDEXED BY TBL1-IX.
         15 WS-PARTIAL-KEY           PIC X(06).
         15 WS-START-POS-IN-TBL2     PIC S9(08) COMP.
         15 WS-END-POS-IN-TBL2       PIC S9(08) COMP.

01 WS-TABLE2.
   05 WS-TBL2-CNT                    PIC S9(08) COMP.
      10 WS-TBL2-ENTRY OCCURS 1 TO 10000 TIMES
     DEPENDING ON WS-TBL2-CNT.
         15 WS-FULL-KEY              PIC X(10). 
         15 WS-DATA-TO-PROCESSED     PIC X(20).

The program logic was modified to use the following processing approach:

1. Load the VSAM file into the two in-memory tables during program initialization.
2. Perform a binary search on WS-TABLE1 using the partial key.
3. Once a match was found, retrieve the start and end positions stored in the matching entry.
4. Use those positions to access and process all corresponding records in WS-TABLE2.

By replacing the repeated VSAM indexed reads with an in-memory binary search solution, the program's execution time was reduced from approximately 60 minutes to just 15 minutes.

This optimization resulted in a 75% reduction in runtime, saving about 45 minutes per execution and significantly improving both development testing cycles.

𝗔 𝗦𝗶𝗺𝗽𝗹𝗲 𝗧𝗲𝗰𝗵𝗻𝗶𝗾𝘂𝗲 𝘁𝗼 𝗘𝗻𝘀𝘂𝗿𝗲 𝗢𝗻𝗹𝘆 𝗢𝗻𝗲 𝗝𝗼𝗯 𝗥𝘂𝗻𝘀 𝗮𝘁 𝗮 𝗧𝗶𝗺𝗲 𝗶𝗻 𝗮 𝗚𝗿𝗼𝘂𝗽 𝗼𝗳 𝗣𝗮𝗿𝗮𝗹𝗹𝗲𝗹 𝗝𝗼𝗯𝘀

While reviewing our batch processing cycle, I noticed that several jobs contained the following DD statement in their final job step:

//ENQUEUE DD DSN=DUMMY.ENQUEUE.DSN,DISP=OLD

Interestingly, the dataset referenced by this DD statement was completely empty and was not accessed or processed by any program within the job. This raised the question: Why was this dataset included in multiple jobs?

After further analysis, I discovered that this was being used as a simple serialization mechanism. Although these jobs could potentially be scheduled to run in parallel, they were intentionally prevented from doing so to avoid database contention issues.
 
How It Works

The key lies in the DISP=OLD parameter. When a job is selected for execution, z/OS allocates all datasets required by a job step before the step begins execution. Because the dataset DUMMY.ENQUEUE.DSN is requested with DISP=OLD, the system reserves it for exclusive use.

As a result:

  • The first job that acquires the dataset proceeds normally.
  • Any other job that also requests the same dataset with DISP=OLD must wait until the dataset is released.
  • Since the DD statement is present in the last step of each job, the dataset remains allocated until that step completes, effectively ensuring that only one job from the group runs at a time.
This creates a simple enqueue mechanism using dataset allocation.
 
Alternative Approaches
 
Mainframe Job schedulers provide built-in facilities for managing job dependencies, resource constraints, and mutual exclusion requirements. These features are usually more flexible and easier to maintain than relying on dataset allocation techniques.
 
However, the application developers chose to implement serialization using DISP=OLD on a dummy dataset. This may have been due to historical reasons, or perhaps a lack of awareness of the scheduler's resource management capabilities.

Monday, July 20, 2026

Why Binary Fields Never Cause an S0C7 Abend, but Zoned Decimal and Packed Decimal Can

In Mainframe, usually numeric calculations are performed using Binary, Zoned-decimal and packed decimals

One interesting fact is that binary fields never cause an S0C7 data exception, whereas zoned decimal and packed decimal fields can. Let's explore why.
 
Binary fields will NEVER cause S0C7 abend
 
This is demonstrated using the below COBOL code.
 
WORKING-STORAGE SECTION.                            
01 JUNK                            PIC X(04).       
01 WS-BINARY        REDEFINES JUNK PIC 9(08) COMP.  
 
PROCEDURE DIVISION.                                 
 
   MOVE 'ABCD'      TO JUNK.                         
   DISPLAY  'WS-BINARY: ' JUNK WS-BINARY            
   ADD 1 TO WS-BINARY                               
   DISPLAY  'WS-BINARY: ' JUNK WS-BINARY            
                                                    
   MOVE '#$%='      TO JUNK.                        
   DISPLAY  'WS-BINARY: ' JUNK WS-BINARY            
   ADD 1 TO WS-BINARY                               
   DISPLAY  'WS-BINARY: ' JUNK WS-BINARY            
 
The above code does NOT produce S0C7. Output of the above code snippet as follows
 
WS-BINARY: 50766788  
WS-BINARY: 50766789  
WS-BINARY: 69589118
WS-BINARY: 69589119  
 
Case 1: Value "ABCD"
 
'ABCD' is indirectly moved to WS-BINARY. The hexadecimal equivalent of 'ABCD' is X’C1C2C3C4’.  If you put this HEX decimal value in calculator, you will get 3,250,766,788. Since picture clause of WS-BINARY is 9(8), the last 8 digit 50,766,788 is displayed in the first DISPLAY statement.
 
When you add 1 to this value(3,250,766,788), it become 3,250,766,789 and second DISPLAY statement displays last 8 digits which is 50,766,789.
 
 
Case 2: Value "#$%="
 
'#$%=' is indirectly moved to WS-BINARY. The hexadecimal equivalent of '#$%='  is X’7B5B6C7E’.  If you put this HEX decimal value in calculator, you will get 2,069,589,118. Since picture clause of  WS-BINARY is 9(8), the last 8 digit 69,589,118 is displayed in the third DISPLAY statement.
 
When you add 1 to this value(2,069,589,118), it become 2,069,589,119 and fourth DISPLAY statement displays last 8 digits which is  69,589,119.
 
 
Understanding Zoned Decimal and Packed Decimal
 
Before discussing how an S0C7 Data Exception occurs with decimal data, it is important to understand how zoned decimal and packed decimal values are stored internally.
 
Typical zoned decimal declarations in COBOL are shown below:

 
01 WS-NUMBER1 PIC 9(5).     -> Unsigned zoned decimal
01 WS-NUMBER1 PIC 9(5)V99.  -> Unsigned zoned decimal
01 WS-NUMBER2 PIC S9(5)V99. -> Signed zoned decimal
 
Internal representation of Zoned Decimal in Memory
 
In a zoned decimal field:
  • Each decimal digit occupies one byte.
  • Each byte has 2 nibbles, and each nibble has 4 bits.
  • The high-order 4 bits (zone portion) of each byte will always have b’1111’ (hex F) except the last byte.
  • The low-order 4 bits (decimal portion) contain the actual numeric digit (0–9).
  • In the last byte, first 4 bits will store the sign and second 4 bits will store the last numeric digit.
 

Value

Internal Hex Representation

Unsigned 12345

X'F1F2F3F4F5'

+12345

X'F1F2F3F4A5'

X'F1F2F3F4C5'

X'F1F2F3F4E5'

-12345

X'F1F2F3F4B5'

X'F1F2F3F4D5'

 
 
Sign is stored in the zone portion (high-order 4 bits) of the last byte. Valid signs are given below
 

Sign

Zone Nibble

Unsigned

F

Positive

A,C,E

Negative

B,D

 
Positive signs A, E and negative signs “B” are called non-preferred signs.
 
System always uses “C” for positive signs and “D” for negative signs, but still accepts A,B,E signs
 
When fields with signs A/B/E are involved in the calculations, ultimately sign will be converted to either “C” for positive and “D” for negative
 
Demonstrating Zoned Decimal Sign Handling
 
Let us see how different sign behaves for a zoned decimal field using the below code snippet.
 
WORKING-STORAGE SECTION.          
01 WS-9                PIC S9(05).               
01 WS-X REDEFINES WS-9 PIC X(05). 
 
PROCEDURE DIVISION.               
PARA1.                            
     MOVE X'F1F2F3F4F5' TO WS-X   
     PERFORM CHECK-SIGN           
     MOVE X'F1F2F3F4A5' TO WS-X   
     PERFORM CHECK-SIGN           
     MOVE X'F1F2F3F4C5' TO WS-X   
     PERFORM CHECK-SIGN           
     MOVE X'F1F2F3F4E5' TO WS-X   
     PERFORM CHECK-SIGN           
                                  
     MOVE X'F1F2F3F4B5' TO WS-X   
     PERFORM CHECK-SIGN           
     MOVE X'F1F2F3F4D5' TO WS-X   
     PERFORM CHECK-SIGN           
     GOBACK.   
                
CHECK-SIGN.                       
     IF WS-9 > 0                  
        DISPLAY WS-9 ' POSITIVE'  
     ELSE                         
         IF WS-9 = 0                
            DISPLAY WS-9 ' ZEROS'   
         ELSE                       
            DISPLAY WS-9 ' NEGATIVE'
         END-IF                     
      END-IF.                       
      ADD 1              TO WS-9    
      DISPLAY 'WS-X : ' WS-X.       
 
 
 

Output of the program is given below

Hex value for the number displayed

12345 POSITIVE  

X'F1F2F3F4F5'

WS-X : 1234F   

X'F1F2F3F4C6'

1234v POSITIVE 

X'F1F2F3F4A5'

WS-X : 1234F   

X'F1F2F3F4C6'

1234E POSITIVE 

X'F1F2F3F4C5'

WS-X : 1234F   

X'F1F2F3F4C6'

1234V POSITIVE 

X'F1F2F3F4E5'

WS-X : 1234F   

X'F1F2F3F4C6'

1234§ NEGATIVE 

X'F1F2F3F4B5'

WS-X : 1234M   

X'F1F2F3F4D4'

1234N NEGATIVE 

X'F1F2F3F4D5'

WS-X : 1234M   

X'F1F2F3F4D4'


Typical packed decimal declarations in COBOL are shown below:
 
01 WS-NUMBER1 PIC 9(5) COMP-3.     -> Unsigned packed decimal

01 WS-NUMBER1 PIC 9(5)V99 COMP-3.  -> Unsigned packed decimal
01 WS-NUMBER2 PIC S9(5)V99 COMP-3. -> Signed packed decimal

Internal representation of packed Decimal in Memory
 
In a packed decimal field:
  • Each byte has two decimal digits except the last byte
  • In the last byte, first 4 bits will store the numeric digit and second 4 bits will store the sign.
  • If an odd number of digits exists, the unused high-order nibble is padded with zero.
 

Value

Internal Hex Representation

Unsigned 12345

X'12345F'

+12345

X'12345A'

X'12345C’

X'12345E’

-12345

X'12345B'

X'12345D’

+123456

X’0123456C’

 
Similar to zoned decimal, system always uses “C” for positive signs and “D” for negative signs for packed decimals, but still accepts A,B,E signs
 
Moving Invalid Data into a Zoned Decimal Field
 
Let us know test moving junk values to a zoned decimal.
 
WORKING-STORAGE SECTION.                           
01 JUNK                            PIC X(04).      
01 WS-ZONED-DECIMAL REDEFINES JUNK PIC 9(04).    
 
PROCEDURE DIVISION.                                
                                                   
   MOVE 'ABCD'      TO JUNK.                       
   DISPLAY  'WS-ZONED-DECIMAL: ' WS-ZONED-DECIMAL  
   ADD 1 TO WS-ZONED-DECIMAL                       
   DISPLAY  'WS-ZONED-DECIMAL: ' WS-ZONED-DECIMAL  
                                                   
   MOVE X'1A1B1C1D' TO JUNK.                       
   DISPLAY  'WS-ZONED-DECIMAL: ' WS-ZONED-DECIMAL  
   ADD 1 TO WS-ZONED-DECIMAL                       
   DISPLAY  'WS-ZONED-DECIMAL: ' WS-ZONED-DECIMAL  
   GOBACK.                                         
 
Output of the above code snippet as follows:

 
WS-ZONED-DECIMAL: ABCD 
WS-ZONED-DECIMAL: 1235 
WS-ZONED-DECIMAL: X'1A1B1C1D' 
CEE3207S The system detected a data exception (System Completion Code=0C7).
 
Code Explanation

Mainframe does not have any instruction to perform arithmetic operations on zoned decimals. So, it first converts zoned-decimals to packed-decimal and then performs arithmetic operations on the packed-decimal.
 
The first move indirectly populated ‘ABCD’(hex value X’C1C2C3C4’) to WS-ZONED-DECIMAL.
 
Before adding one to WS-ZONED-DECIMAL, Value X’C1C2C3C4’ need to be converted to packed-decimal.
 
How Zoned Decimal to Packed Decimal Conversion Works
 
During the conversion process:
  • The digit portion of each zoned-decimal byte is treated as a numeric digit.
  • The zone bits are ignored except in the rightmost byte.
  • The zone bits of the last byte become the sign nibble.
  • Digits are packed together from left to right.
  • If the packed-decimal result contains an unfilled nibble, it is padded with zero on the left. 
No validation occurs during the conversion itself.
 
Example 1: ‘ABCD’ - X'C1C2C3C4'
 
Value X’C1C2C3C4’ becomes X‘01234C’ after zoned-decimal to packed-decimal conversion.
 
Therefore, the second DISPLAY statement shows 1235, because the temporary packed-decimal representation (X'01234C') was successfully incremented by one during the arithmetic operation.
 
Example 2: X'1A1B1C1D'
 
Value X'1A1B1C1D' becomes X‘0ABCD1’ after zoned-decimal to packed-decimal conversion.
 
Since X‘0ABCD1’ does not adhere to packed-decimal representation, when we tried to add one to it, system produced S0C7 abend


When Does a Packed-Decimal (COMP-3) Field Cause an S0C7 Abend?

As discussed earlier, a packed-decimal field must conform to the valid packed-decimal format. If the field contains invalid digits or an invalid sign nibble and an arithmetic operation is attempted on it, the processor detects the invalid data and raises an S0C7 (Data Exception) abend.

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.