> "The simplest data structure in COBOL — the table — is also the most frequently misused. Get table handling right, and your programs will be fast, readable, and reliable. Get it wrong, and you'll be debugging subscript errors at 2 AM during a...
In This Chapter
- 18.1 Foundations: The OCCURS Clause
- 18.2 Multi-Dimensional Tables
- 18.3 The SEARCH Statement: Serial Search
- 18.4 SEARCH ALL: Binary Search
- 18.5 Variable-Length Tables: OCCURS DEPENDING ON
- 18.6 Loading Tables
- 18.7 Table Lookup Patterns
- 18.8 GlobalBank Case Study: Interest Rate Table
- 18.9 MedClaim Case Study: Fee Schedule Tables
- 18.10 Defensive Programming for Tables
- 18.11 Table Initialization and Cleanup Patterns
- 18.12 Real-World Table Design Considerations
- 18.13 Advanced Table Techniques
- 18.12 The Student Mainframe Lab
- 18.13 Common Mistakes and Debugging
- 18.14 Performance Considerations
- 18.15 Complete Worked Example: MedClaim Diagnosis Code Lookup System
- 18.16 Table Handling in the Enterprise Context
- 18.17 Chapter Summary
Chapter 18: Table Handling and Searching
"The simplest data structure in COBOL — the table — is also the most frequently misused. Get table handling right, and your programs will be fast, readable, and reliable. Get it wrong, and you'll be debugging subscript errors at 2 AM during a production crisis." — Maria Chen, Senior Developer, GlobalBank
Every production COBOL system of any complexity relies on tables. Whether you are looking up an account type from a two-character code, translating a procedure code into a description, or finding the correct interest rate for a given term and credit tier, you are performing a table operation. In your first COBOL course, you likely encountered the OCCURS clause and perhaps wrote a simple loop to search through a small array. In this chapter, we go far deeper.
We will master single-dimension and multi-dimensional tables, understand the critical difference between subscripts and indexes, use the SEARCH and SEARCH ALL statements for efficient lookups, handle variable-length tables with OCCURS DEPENDING ON, and develop robust patterns for loading tables from files, hardcoded values, and copybooks. Along the way, we will build realistic table-driven programs for both GlobalBank and MedClaim, reinforcing the Defensive Programming theme with thorough boundary checking.
18.1 Foundations: The OCCURS Clause
In COBOL, tables are defined using the OCCURS clause on a data item in the DATA DIVISION. Unlike languages such as C or Java where arrays are allocated dynamically, COBOL tables have their maximum size determined at compile time and their storage allocated within the record structure.
Basic Syntax
01 ACCOUNT-TYPE-TABLE.
05 ACCOUNT-ENTRY OCCURS 20 TIMES.
10 ACCT-TYPE-CODE PIC X(2).
10 ACCT-TYPE-DESC PIC X(30).
10 ACCT-MIN-BALANCE PIC 9(7)V99.
This defines a table with exactly 20 entries, each containing a two-character code, a 30-character description, and a minimum balance amount. The total storage consumed is 20 times (2 + 30 + 9) = 820 bytes.
💡 Key Concept: The
OCCURSclause can appear on any level from 02 through 49 but never on a level 01 or 77 item. The clause tells the compiler to allocate multiple instances of the data item and its subordinate fields.
Rules and Restrictions
Several important rules govern the OCCURS clause:
- Level restriction: OCCURS cannot be used on 01, 66, 77, or 88 level items.
- VALUE clause: In standard COBOL, you cannot use VALUE on an item that has OCCURS or on a subordinate of an OCCURS item (though many compilers relax this as an extension).
- REDEFINES interaction: An item with OCCURS cannot also have REDEFINES, but an item with REDEFINES can contain subordinate items with OCCURS.
- Nesting: OCCURS can be nested within OCCURS up to seven levels deep per the COBOL standard.
Subscripts vs. Indexes: A Critical Distinction
To reference individual elements of a table, you use either subscripts or indexes. Though they may seem interchangeable to a beginner, the distinction is important both conceptually and for performance.
Subscripts are ordinary data items (typically PIC 9 or PIC S9 COMP) used in parentheses after the table item name:
WORKING-STORAGE SECTION.
01 WS-SUB PIC 99 VALUE 1.
PROCEDURE DIVISION.
MOVE "SA" TO ACCT-TYPE-CODE(WS-SUB)
DISPLAY ACCT-TYPE-DESC(5)
A subscript is a logical position number starting at 1. The compiler must calculate the memory offset each time you use the subscript: offset = (subscript - 1) * element-length. This multiplication happens at runtime.
Indexes are defined with the INDEXED BY clause and are internally maintained as byte displacements from the start of the table:
01 ACCOUNT-TYPE-TABLE.
05 ACCOUNT-ENTRY OCCURS 20 TIMES
INDEXED BY ACCT-IDX.
10 ACCT-TYPE-CODE PIC X(2).
10 ACCT-TYPE-DESC PIC X(30).
10 ACCT-MIN-BALANCE PIC 9(7)V99.
Because the index already contains the byte displacement, the compiler does not need to perform the multiplication at runtime. You manipulate indexes with the SET statement:
SET ACCT-IDX TO 1
SET ACCT-IDX UP BY 1
SET ACCT-IDX DOWN BY 3
📊 Performance Note: On mainframe hardware, the difference between subscripts and indexes is measurable in high-volume loops. In a table with millions of lookups per batch cycle — common at GlobalBank — the elimination of repeated multiplication can reduce CPU time by 5-15%. Always prefer indexes for tables that will be searched heavily.
You cannot use an index directly in a MOVE or DISPLAY statement. To convert between indexes and ordinary data items:
SET WS-SUB TO ACCT-IDX *> Index to subscript
SET ACCT-IDX TO WS-SUB *> Subscript to index
The SET Statement in Detail
The SET statement is the primary mechanism for working with index values:
| Statement | Effect |
|---|---|
SET idx TO 5 |
Sets index to point to element 5 |
SET idx UP BY 1 |
Advances index to next element |
SET idx DOWN BY 2 |
Backs index up by two elements |
SET idx TO idx-2 |
Copies one index to another |
SET ws-num TO idx |
Converts index to integer |
SET idx TO ws-num |
Sets index from integer |
⚠️ Common Pitfall: Never use MOVE or ADD/SUBTRACT with index names. The compiler will not catch this in all cases, and the results will be unpredictable because indexes contain byte displacements, not element numbers. Always use SET.
Understanding Index Internals
To truly appreciate why indexes outperform subscripts, consider what happens at the machine instruction level. When you write ACCT-TYPE-DESC(WS-SUB), the compiler must generate code that:
- Loads the value of WS-SUB from memory
- Subtracts 1 (because tables are 1-based but offsets are 0-based)
- Multiplies the result by the element size (in this case, 41 bytes per entry)
- Adds the base address of the table
- Uses the resulting address to access the data
The multiplication in step 3 is the expensive operation, especially on older mainframe hardware where multiply instructions take multiple CPU cycles. When you have a loop that performs thousands or millions of table accesses, those multiply operations accumulate.
With an index, step 3 is eliminated. The index already contains the byte displacement, so the access becomes:
- Load the index value (which is already a byte displacement)
- Add the base address of the table
- Use the resulting address to access the data
The SET statement handles the displacement calculation when you change the index value, so the multiplication only happens once per index change rather than once per access.
When Subscripts Are Acceptable
Despite the performance advantage of indexes, there are cases where subscripts are the better choice:
- Debugging: Subscripts can be displayed directly (
DISPLAY WS-SUB), while indexes require conversion via SET before display. - Arithmetic: Subscripts participate in arithmetic naturally (
ADD 1 TO WS-SUB), while indexes requireSET idx UP BY 1. - Pass to subprograms: Subscripts are ordinary data items that can be passed through CALL...USING. Indexes cannot be passed directly.
- Small tables with infrequent access: For a 10-entry table accessed once per transaction, the performance difference is immeasurable. Readability may favor subscripts.
The general guideline at GlobalBank is: use indexes for tables that will be searched with SEARCH or SEARCH ALL, and use subscripts for tables that are only accessed via computed positions or in low-volume loops.
The USAGE IS INDEX Clause
In addition to indexes defined via INDEXED BY, you can create standalone index data items:
01 WS-SAVED-INDEX USAGE IS INDEX.
These can store index values for later use:
SET WS-SAVED-INDEX TO ACCT-IDX
*> ... do other processing ...
SET ACCT-IDX TO WS-SAVED-INDEX
*> Restore the saved position
This is particularly useful when you need to remember a position in one table while searching another table, or when implementing a "bookmark" for resuming a search.
18.2 Multi-Dimensional Tables
Real-world business data often has a matrix structure. Interest rates vary by term and credit tier. Fee schedules vary by procedure code and provider type. COBOL supports multi-dimensional tables through nested OCCURS clauses.
Two-Dimensional Tables
01 INTEREST-RATE-TABLE.
05 RATE-TERM-GROUP OCCURS 6 TIMES
INDEXED BY TERM-IDX.
10 RATE-TERM-MONTHS PIC 9(3).
10 RATE-TIER-ENTRY OCCURS 5 TIMES
INDEXED BY TIER-IDX.
15 RATE-CREDIT-TIER PIC X(1).
15 RATE-ANNUAL-PCT PIC 9(2)V9(4).
This defines a 6-by-5 matrix: six term groups (e.g., 12 months, 24 months, 36 months, 48 months, 60 months, 84 months), each containing five credit tiers (e.g., A through E). To access a specific rate:
DISPLAY RATE-ANNUAL-PCT(3, 2)
*> Displays the rate for the 3rd term, 2nd credit tier
DISPLAY RATE-ANNUAL-PCT(TERM-IDX, TIER-IDX)
*> Using indexes
Three-Dimensional Tables
GlobalBank's branch fee schedule varies by region, branch type, and service category:
01 BRANCH-FEE-TABLE.
05 FEE-REGION OCCURS 4 TIMES
INDEXED BY REGION-IDX.
10 FEE-REGION-CODE PIC X(2).
10 FEE-BRANCH-TYPE OCCURS 3 TIMES
INDEXED BY BTYPE-IDX.
15 FEE-BTYPE-CODE PIC X(1).
15 FEE-SERVICE-CAT OCCURS 8 TIMES
INDEXED BY SCAT-IDX.
20 FEE-SVC-CODE PIC X(3).
20 FEE-SVC-AMOUNT PIC 9(5)V99.
Accessing a specific fee requires three subscripts or indexes:
MOVE FEE-SVC-AMOUNT(2, 1, 5) TO WS-DISPLAY-FEE
*> Region 2, Branch Type 1, Service Category 5
⚠️ Design Warning: While COBOL allows up to seven levels of nesting in OCCURS clauses, tables beyond three dimensions become extremely difficult to read and maintain. If you find yourself needing more than three dimensions, consider whether a file-based lookup or a relational database call would be more appropriate. Derek Washington learned this lesson the hard way when he built a five-dimensional rate table that no one else on the team could understand.
Memory Calculation for Multi-Dimensional Tables
Always calculate the total memory for your table to ensure it fits within WORKING-STORAGE limits:
For the branch fee table above: - Innermost entry: 3 (code) + 7 (amount) = 10 bytes - Service category level: 8 entries x 10 = 80 bytes - Branch type level: 1 (code) + 80 = 81 bytes per branch type - Branch type total: 3 x 81 = 243 bytes - Region level: 2 (code) + 243 = 245 bytes per region - Total: 4 x 245 = 980 bytes
This is modest. But consider a table with OCCURS 100 at each of three levels: 100 x 100 x 100 x entry-size can quickly consume megabytes.
18.3 The SEARCH Statement: Serial Search
The SEARCH statement performs a linear (serial) search through a table. It requires that the table be defined with INDEXED BY.
Basic Syntax
SEARCH ACCOUNT-ENTRY
AT END
MOVE "UNKNOWN" TO WS-ACCT-DESCRIPTION
WHEN ACCT-TYPE-CODE(ACCT-IDX) = WS-INPUT-CODE
MOVE ACCT-TYPE-DESC(ACCT-IDX) TO WS-ACCT-DESCRIPTION
END-SEARCH
How SEARCH Works
- The search begins at the current position of the index. If the index is set to 5, the search starts at element 5, not element 1.
- The WHEN condition is tested. If true, the associated action is performed and the search ends.
- If the condition is false, the index is automatically incremented by 1.
- Steps 2-3 repeat until either a WHEN condition is satisfied or the index exceeds the table size.
- If the index exceeds the table size without a WHEN match, the AT END clause executes.
🔴 Critical Rule: You must
SETthe index to 1 before each SEARCH unless you intentionally want to start mid-table. Forgetting this is one of the most common COBOL bugs:
SET ACCT-IDX TO 1 *> ESSENTIAL!
SEARCH ACCOUNT-ENTRY
AT END
MOVE "NOT FOUND" TO WS-RESULT
WHEN ACCT-TYPE-CODE(ACCT-IDX) = WS-LOOKUP-CODE
MOVE ACCT-TYPE-DESC(ACCT-IDX) TO WS-RESULT
END-SEARCH
Multiple WHEN Clauses
A SEARCH can have multiple WHEN clauses. They are tested in order for each element:
SET ACCT-IDX TO 1
SEARCH ACCOUNT-ENTRY
AT END
PERFORM 9100-NOT-FOUND
WHEN ACCT-TYPE-CODE(ACCT-IDX) = WS-LOOKUP-CODE
AND ACCT-MIN-BALANCE(ACCT-IDX) > ZERO
PERFORM 3100-PROCESS-ACTIVE-TYPE
WHEN ACCT-TYPE-CODE(ACCT-IDX) = WS-LOOKUP-CODE
PERFORM 3200-PROCESS-INACTIVE-TYPE
END-SEARCH
This pattern first checks if the code matches AND the account has a positive minimum balance. If both are true, it processes as active. If only the code matches (but the balance is zero), the second WHEN catches it.
SEARCH with VARYING
You can have the SEARCH increment an additional index or data item alongside the primary index:
SET ACCT-IDX TO 1
SET WS-COUNTER TO 1
SEARCH ACCOUNT-ENTRY
VARYING WS-COUNTER
AT END
DISPLAY "NOT FOUND AFTER " WS-COUNTER " CHECKS"
WHEN ACCT-TYPE-CODE(ACCT-IDX) = WS-LOOKUP-CODE
DISPLAY "FOUND AT POSITION " WS-COUNTER
END-SEARCH
18.4 SEARCH ALL: Binary Search
When you have a large table and it is sorted by a key field, you can use SEARCH ALL for a binary search. Binary search is dramatically faster than serial search for large tables — O(log n) versus O(n).
Requirements for SEARCH ALL
Binary search has strict prerequisites:
- The table must have an
ASCENDING KEYorDESCENDING KEYclause. - The table must actually be sorted by that key before the search.
- The WHEN clause must test for equality (
=) on the key field. - Only one WHEN clause is allowed.
- Compound conditions in the WHEN clause can only use AND (not OR).
Defining a Table for Binary Search
01 PROCEDURE-CODE-TABLE.
05 PROC-ENTRY OCCURS 5000 TIMES
ASCENDING KEY IS PROC-CODE
INDEXED BY PROC-IDX.
10 PROC-CODE PIC X(5).
10 PROC-DESCRIPTION PIC X(50).
10 PROC-BASE-FEE PIC 9(7)V99.
10 PROC-CATEGORY PIC X(3).
Using SEARCH ALL
SEARCH ALL PROC-ENTRY
AT END
MOVE "UNKNOWN PROCEDURE" TO WS-PROC-DESC
ADD 1 TO WS-ERRORS-COUNT
WHEN PROC-CODE(PROC-IDX) = WS-INPUT-PROC-CODE
MOVE PROC-DESCRIPTION(PROC-IDX) TO WS-PROC-DESC
MOVE PROC-BASE-FEE(PROC-IDX) TO WS-PROC-FEE
END-SEARCH
💡 Key Difference: Unlike SEARCH, you do not need to SET the index before SEARCH ALL. The binary search algorithm manages the index internally, calculating midpoints and adjusting automatically.
Compound Key Searches
You can search on multiple keys simultaneously:
01 FEE-SCHEDULE-TABLE.
05 FEE-ENTRY OCCURS 10000 TIMES
ASCENDING KEY IS FEE-PROV-TYPE
FEE-PROC-CODE
INDEXED BY FEE-IDX.
10 FEE-PROV-TYPE PIC X(2).
10 FEE-PROC-CODE PIC X(5).
10 FEE-ALLOWED-AMT PIC 9(7)V99.
10 FEE-EFF-DATE PIC 9(8).
PROCEDURE DIVISION.
SEARCH ALL FEE-ENTRY
AT END
PERFORM 9200-FEE-NOT-FOUND
WHEN FEE-PROV-TYPE(FEE-IDX) = WS-PROV-TYPE
AND FEE-PROC-CODE(FEE-IDX) = WS-PROC-CODE
MOVE FEE-ALLOWED-AMT(FEE-IDX) TO WS-ALLOWED
END-SEARCH
The table must be sorted by FEE-PROV-TYPE as the major key and FEE-PROC-CODE as the minor key.
Performance Comparison
The following table illustrates the dramatic difference between serial and binary search:
| Table Size | Serial Search (avg comparisons) | Binary Search (max comparisons) |
|---|---|---|
| 100 | 50 | 7 |
| 1,000 | 500 | 10 |
| 5,000 | 2,500 | 13 |
| 10,000 | 5,000 | 14 |
| 50,000 | 25,000 | 16 |
📊 At MedClaim, the procedure code table contains approximately 12,000 entries. James Okafor measured the difference: a batch run performing 2 million lookups took 45 seconds with serial search versus 3 seconds with binary search. The team mandated SEARCH ALL for all procedure code lookups after that benchmark.
18.5 Variable-Length Tables: OCCURS DEPENDING ON
Sometimes you do not know at compile time how many entries a table will contain. The OCCURS DEPENDING ON (ODO) clause creates a variable-length table:
01 BRANCH-TABLE.
05 BRANCH-COUNT PIC 9(4) COMP.
05 BRANCH-ENTRY OCCURS 1 TO 500 TIMES
DEPENDING ON BRANCH-COUNT
ASCENDING KEY IS BRANCH-ID
INDEXED BY BRANCH-IDX.
10 BRANCH-ID PIC X(6).
10 BRANCH-NAME PIC X(40).
10 BRANCH-REGION PIC X(2).
10 BRANCH-STATUS PIC X(1).
Key Rules for ODO
- The DEPENDING ON data item (BRANCH-COUNT) must be defined before the OCCURS item in the record.
- The DEPENDING ON item must be a numeric integer.
- The value of the DEPENDING ON item determines the current logical size of the table.
- The compiler allocates storage for the maximum (500 in this case).
- The DEPENDING ON item must always contain a valid value within the specified range.
Defensive Programming with ODO
Variable-length tables are a frequent source of bugs. The DEPENDING ON value can become corrupted, leading to reads beyond the logical end of the table or writes that overlay other data:
IF BRANCH-COUNT < 1 OR BRANCH-COUNT > 500
DISPLAY "ERROR: BRANCH-COUNT OUT OF RANGE: "
BRANCH-COUNT
MOVE 500 TO BRANCH-COUNT
PERFORM 9900-ABNORMAL-HANDLING
END-IF
⚠️ Defensive Programming Pattern: Always validate the DEPENDING ON value after loading it from external input and before any table operation. At GlobalBank, Maria Chen insists that every program validate ODO counts immediately after file reads.
ODO and Record Length
When an ODO table appears in a record definition (especially for files), the record length varies with the DEPENDING ON value. This affects I/O operations:
FD BRANCH-FILE
RECORD IS VARYING IN SIZE
FROM 4 TO 24504
DEPENDING ON WS-BRANCH-REC-LEN.
18.6 Loading Tables
Tables must be populated before they can be searched. There are three common approaches: hardcoded values, file-based loading, and copybook loading.
Hardcoded Tables with REDEFINES
For small, stable tables (like state codes or account types), hardcoding is appropriate:
01 ACCT-TYPE-VALUES.
05 FILLER PIC X(32) VALUE "CHCHECKING ACCOUNT ".
05 FILLER PIC X(32) VALUE "SASAVINGS ACCOUNT ".
05 FILLER PIC X(32) VALUE "CDCERTIFICATE OF DEPOSIT ".
05 FILLER PIC X(32) VALUE "MMMONEYMARKET ACCOUNT ".
05 FILLER PIC X(32) VALUE "IRAINDIVIDUAL RETIREMENT ACCT ".
05 FILLER PIC X(32) VALUE "LNLOAN ACCOUNT ".
05 FILLER PIC X(32) VALUE "MTMORTGAGE ACCOUNT ".
05 FILLER PIC X(32) VALUE "CCCREDIT CARD ACCOUNT ".
01 ACCT-TYPE-TABLE REDEFINES ACCT-TYPE-VALUES.
05 ACCT-ENTRY OCCURS 8 TIMES
INDEXED BY ACCT-IDX.
10 ACCT-TYPE-CODE PIC X(2).
10 ACCT-TYPE-DESC PIC X(30).
The first two characters of each FILLER are the code; the remaining 30 are the description. The REDEFINES overlays the table structure on the raw values.
💡 Readability Tip: Note the formatting pattern — each FILLER value is the same length (32 characters), making it easy to verify alignment visually. Maria Chen requires this alignment in all GlobalBank table definitions.
Loading Tables from Files
For large or frequently changing tables, loading from a file is the standard approach:
PROCEDURE DIVISION.
0100-MAIN.
PERFORM 1000-INITIALIZE
PERFORM 2000-PROCESS
PERFORM 9000-TERMINATE
STOP RUN.
1000-INITIALIZE.
OPEN INPUT PROC-CODE-FILE
PERFORM 1100-LOAD-PROC-TABLE
CLOSE PROC-CODE-FILE
OPEN INPUT CLAIM-FILE
OPEN OUTPUT REPORT-FILE.
1100-LOAD-PROC-TABLE.
MOVE ZERO TO PROC-TABLE-COUNT
READ PROC-CODE-FILE INTO WS-PROC-INPUT
AT END SET WS-PROC-EOF TO TRUE
END-READ
PERFORM UNTIL WS-PROC-EOF
ADD 1 TO PROC-TABLE-COUNT
IF PROC-TABLE-COUNT > 5000
DISPLAY "ERROR: PROC TABLE OVERFLOW"
PERFORM 9900-ABEND
END-IF
MOVE WS-INPUT-CODE TO
PROC-CODE(PROC-TABLE-COUNT)
MOVE WS-INPUT-DESC TO
PROC-DESCRIPTION(PROC-TABLE-COUNT)
MOVE WS-INPUT-FEE TO
PROC-BASE-FEE(PROC-TABLE-COUNT)
READ PROC-CODE-FILE INTO WS-PROC-INPUT
AT END SET WS-PROC-EOF TO TRUE
END-READ
END-PERFORM
DISPLAY "LOADED " PROC-TABLE-COUNT
" PROCEDURE CODES"
.
Table Loading with Sort Verification
If you plan to use SEARCH ALL, you must verify the table is sorted:
1200-VERIFY-SORT-ORDER.
MOVE "Y" TO WS-SORT-VALID-FLAG
PERFORM VARYING WS-CHK-IDX FROM 2 BY 1
UNTIL WS-CHK-IDX > PROC-TABLE-COUNT
OR WS-SORT-VALID-FLAG = "N"
IF PROC-CODE(WS-CHK-IDX) <
PROC-CODE(WS-CHK-IDX - 1)
DISPLAY "SORT ERROR AT ENTRY "
WS-CHK-IDX
": " PROC-CODE(WS-CHK-IDX)
" < " PROC-CODE(WS-CHK-IDX - 1)
MOVE "N" TO WS-SORT-VALID-FLAG
END-IF
END-PERFORM
IF WS-SORT-VALID-FLAG = "N"
DISPLAY "TABLE NOT SORTED - CANNOT USE "
"BINARY SEARCH"
PERFORM 9900-ABEND
END-IF.
Copybook-Based Table Definitions
In enterprise environments, table definitions are often stored in copybooks so they can be shared across programs:
*> CPYTBACT.cpy - Account Type Table Copybook
01 ACCOUNT-TYPE-TABLE.
05 ACCT-TABLE-COUNT PIC 9(3) COMP.
05 ACCT-ENTRY OCCURS 1 TO 50 TIMES
DEPENDING ON ACCT-TABLE-COUNT
ASCENDING KEY IS ACCT-TYPE-CODE
INDEXED BY ACCT-IDX.
10 ACCT-TYPE-CODE PIC X(2).
10 ACCT-TYPE-DESC PIC X(30).
10 ACCT-MIN-BALANCE PIC 9(7)V99.
10 ACCT-MONTHLY-FEE PIC 9(5)V99.
10 ACCT-ACTIVE-FLAG PIC X(1).
88 ACCT-IS-ACTIVE VALUE "A".
88 ACCT-IS-INACTIVE VALUE "I".
Then in any program:
WORKING-STORAGE SECTION.
COPY CPYTBACT.
18.7 Table Lookup Patterns
Let us examine the three primary lookup patterns and when to use each.
Pattern 1: Direct Indexing
When the lookup key can be converted directly to a subscript position, direct indexing is the fastest approach — O(1) time:
*> Month name lookup — month number IS the subscript
01 MONTH-NAME-TABLE.
05 MONTH-VALUES.
10 FILLER PIC X(9) VALUE "JANUARY ".
10 FILLER PIC X(9) VALUE "FEBRUARY ".
10 FILLER PIC X(9) VALUE "MARCH ".
10 FILLER PIC X(9) VALUE "APRIL ".
10 FILLER PIC X(9) VALUE "MAY ".
10 FILLER PIC X(9) VALUE "JUNE ".
10 FILLER PIC X(9) VALUE "JULY ".
10 FILLER PIC X(9) VALUE "AUGUST ".
10 FILLER PIC X(9) VALUE "SEPTEMBER".
10 FILLER PIC X(9) VALUE "OCTOBER ".
10 FILLER PIC X(9) VALUE "NOVEMBER ".
10 FILLER PIC X(9) VALUE "DECEMBER ".
05 MONTH-ENTRY REDEFINES MONTH-VALUES.
10 MONTH-NAME PIC X(9) OCCURS 12 TIMES.
PROCEDURE DIVISION.
IF WS-MONTH-NUM >= 1 AND <= 12
MOVE MONTH-NAME(WS-MONTH-NUM)
TO WS-DISPLAY-MONTH
ELSE
MOVE "INVALID" TO WS-DISPLAY-MONTH
END-IF
Direct indexing is ideal for: - Month numbers, day-of-week codes - State/province codes that can be mapped to sequential numbers - Small code sets where position is inherent (e.g., error code 1-50)
Pattern 2: Serial Search (SEARCH)
Use serial search when: - The table is small (under 50-100 entries) - The table is unsorted or sorted by a key you are not searching on - You need complex search conditions (OR, ranges, partial matches) - The most frequently requested items can be placed first
*> GlobalBank: Branch lookup by region code
SET BRANCH-IDX TO 1
SEARCH BRANCH-ENTRY
AT END
MOVE SPACES TO WS-BRANCH-NAME
SET WS-BRANCH-NOT-FOUND TO TRUE
WHEN BRANCH-REGION(BRANCH-IDX) = WS-LOOKUP-REGION
AND BRANCH-STATUS(BRANCH-IDX) = "A"
MOVE BRANCH-NAME(BRANCH-IDX)
TO WS-BRANCH-NAME
SET WS-BRANCH-FOUND TO TRUE
END-SEARCH
Pattern 3: Binary Search (SEARCH ALL)
Use binary search when: - The table has more than 50-100 entries - The table is sorted by the search key - You are testing for equality on the key - Performance is critical
*> MedClaim: Diagnosis code lookup
SEARCH ALL DIAG-ENTRY
AT END
SET WS-DIAG-NOT-FOUND TO TRUE
MOVE "UNKNOWN DIAGNOSIS" TO WS-DIAG-DESC
WHEN DIAG-CODE(DIAG-IDX) = WS-INPUT-DIAG
SET WS-DIAG-FOUND TO TRUE
MOVE DIAG-DESCRIPTION(DIAG-IDX)
TO WS-DIAG-DESC
MOVE DIAG-CATEGORY(DIAG-IDX)
TO WS-DIAG-CAT
END-SEARCH
Decision Framework
Is the key convertible to a subscript position?
YES --> Direct indexing (O(1))
NO --> Is the table sorted by the search key?
YES --> Is the table > 50 entries?
YES --> SEARCH ALL (O(log n))
NO --> Either works; SEARCH ALL
is fine
NO --> SEARCH (O(n))
Consider sorting the table first
if it will be searched many times
18.8 GlobalBank Case Study: Interest Rate Table
Let us build a complete, production-quality interest rate lookup system for GlobalBank. The bank offers certificates of deposit (CDs) with rates that vary by term (in months) and credit tier.
Table Design
IDENTIFICATION DIVISION.
PROGRAM-ID. GBRATELU.
*============================================================
* GlobalBank Interest Rate Table Lookup
* Loads CD rates from file, performs lookups for
* customer transactions.
*============================================================
DATA DIVISION.
WORKING-STORAGE SECTION.
* Interest Rate Table - 2 dimensions
01 RATE-TABLE-AREA.
05 RT-NUM-TERMS PIC 9(2) COMP VALUE ZERO.
05 RT-NUM-TIERS PIC 9(2) COMP VALUE 5.
05 RT-TERM-GROUP OCCURS 12 TIMES
INDEXED BY RT-TERM-IDX.
10 RT-TERM-MONTHS PIC 9(3).
10 RT-TIER-ENTRY OCCURS 5 TIMES
INDEXED BY RT-TIER-IDX.
15 RT-TIER-CODE PIC X(1).
15 RT-ANNUAL-RATE PIC 9(2)V9(4).
15 RT-MIN-DEPOSIT PIC 9(9)V99.
01 WS-LOOKUP-FIELDS.
05 WS-CUST-TERM PIC 9(3).
05 WS-CUST-TIER PIC X(1).
05 WS-FOUND-RATE PIC 9(2)V9(4).
05 WS-FOUND-MIN PIC 9(9)V99.
05 WS-LOOKUP-STATUS PIC X(1).
88 WS-RATE-FOUND VALUE "Y".
88 WS-RATE-NOT-FOUND VALUE "N".
88 WS-TERM-NOT-FOUND VALUE "T".
88 WS-TIER-NOT-FOUND VALUE "R".
The Lookup Logic
3000-LOOKUP-RATE.
* First, find the matching term
SET WS-RATE-NOT-FOUND TO TRUE
SET RT-TERM-IDX TO 1
SEARCH RT-TERM-GROUP
AT END
SET WS-TERM-NOT-FOUND TO TRUE
WHEN RT-TERM-MONTHS(RT-TERM-IDX) =
WS-CUST-TERM
PERFORM 3100-FIND-TIER
END-SEARCH
.
3100-FIND-TIER.
* Within the found term, find the credit tier
SET RT-TIER-IDX TO 1
SEARCH RT-TIER-ENTRY
AT END
SET WS-TIER-NOT-FOUND TO TRUE
WHEN RT-TIER-CODE(RT-TERM-IDX,
RT-TIER-IDX) =
WS-CUST-TIER
SET WS-RATE-FOUND TO TRUE
MOVE RT-ANNUAL-RATE(RT-TERM-IDX,
RT-TIER-IDX)
TO WS-FOUND-RATE
MOVE RT-MIN-DEPOSIT(RT-TERM-IDX,
RT-TIER-IDX)
TO WS-FOUND-MIN
END-SEARCH
.
This demonstrates searching a two-dimensional table: first find the row (term), then within that row search for the column (tier). Notice how the SEARCH in paragraph 3100 uses both indexes — RT-TERM-IDX is set by the outer SEARCH and remains stable while RT-TIER-IDX is advanced by the inner SEARCH.
Complete Worked Example: Loading the Rate Table from a File
Let us see the full program flow — from file definition through loading to lookup — in one cohesive example. This is the pattern Maria Chen requires for all new table-driven programs at GlobalBank:
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT RATE-FILE
ASSIGN TO "CDRATES.DAT"
ORGANIZATION IS LINE SEQUENTIAL
FILE STATUS IS WS-RATE-FILE-STATUS.
DATA DIVISION.
FILE SECTION.
FD RATE-FILE.
01 RATE-INPUT-REC.
05 RI-TERM-MONTHS PIC 9(3).
05 RI-TIER-CODE PIC X(1).
05 RI-ANNUAL-RATE PIC 9(2)V9(4).
05 RI-MIN-DEPOSIT PIC 9(9)V99.
WORKING-STORAGE SECTION.
01 WS-RATE-FILE-STATUS PIC X(2).
01 WS-RATE-EOF-FLAG PIC X(1) VALUE "N".
88 WS-RATE-EOF VALUE "Y".
01 WS-PREV-TERM PIC 9(3) VALUE ZERO.
01 WS-CURR-TIER-NUM PIC 9 VALUE ZERO.
01 RATE-TABLE-AREA.
05 RT-NUM-TERMS PIC 9(2) COMP VALUE 0.
05 RT-TERM-GROUP OCCURS 12 TIMES
INDEXED BY RT-TERM-IDX.
10 RT-TERM-MONTHS PIC 9(3).
10 RT-NUM-TIERS PIC 9(2) COMP.
10 RT-TIER-ENTRY OCCURS 5 TIMES
INDEXED BY RT-TIER-IDX.
15 RT-TIER-CODE PIC X(1).
15 RT-ANNUAL-RATE PIC 9(2)V9(4).
15 RT-MIN-DEPOSIT PIC 9(9)V99.
PROCEDURE DIVISION.
1000-LOAD-RATE-TABLE.
OPEN INPUT RATE-FILE
IF WS-RATE-FILE-STATUS NOT = "00"
DISPLAY "RATE FILE OPEN ERROR: "
WS-RATE-FILE-STATUS
PERFORM 9900-ABEND
END-IF
MOVE ZERO TO RT-NUM-TERMS
MOVE ZERO TO WS-PREV-TERM
READ RATE-FILE
AT END SET WS-RATE-EOF TO TRUE
END-READ
PERFORM UNTIL WS-RATE-EOF
* New term group?
IF RI-TERM-MONTHS NOT = WS-PREV-TERM
ADD 1 TO RT-NUM-TERMS
IF RT-NUM-TERMS > 12
DISPLAY "TOO MANY TERM GROUPS"
PERFORM 9900-ABEND
END-IF
MOVE RI-TERM-MONTHS TO
RT-TERM-MONTHS(RT-NUM-TERMS)
MOVE ZERO TO
RT-NUM-TIERS(RT-NUM-TERMS)
MOVE RI-TERM-MONTHS TO WS-PREV-TERM
END-IF
* Add tier to current term group
ADD 1 TO RT-NUM-TIERS(RT-NUM-TERMS)
MOVE WS-CURR-TIER-NUM TO WS-CURR-TIER-NUM
IF RT-NUM-TIERS(RT-NUM-TERMS) > 5
DISPLAY "TOO MANY TIERS FOR TERM "
RI-TERM-MONTHS
PERFORM 9900-ABEND
END-IF
MOVE RI-TIER-CODE TO
RT-TIER-CODE(RT-NUM-TERMS,
RT-NUM-TIERS(RT-NUM-TERMS))
MOVE RI-ANNUAL-RATE TO
RT-ANNUAL-RATE(RT-NUM-TERMS,
RT-NUM-TIERS(RT-NUM-TERMS))
MOVE RI-MIN-DEPOSIT TO
RT-MIN-DEPOSIT(RT-NUM-TERMS,
RT-NUM-TIERS(RT-NUM-TERMS))
READ RATE-FILE
AT END SET WS-RATE-EOF TO TRUE
END-READ
END-PERFORM
CLOSE RATE-FILE
DISPLAY "LOADED " RT-NUM-TERMS
" TERM GROUPS"
.
This pattern highlights several important practices: checking file status after OPEN, counting entries for each dimension separately, validating overflow in both dimensions, and using the term count to track the current row while adding tiers within that row.
Displaying Table Contents for Verification
After loading, it is good practice to display the table contents during testing:
1500-DISPLAY-RATE-TABLE.
DISPLAY "=== CD INTEREST RATE TABLE ==="
DISPLAY "TERM TIER RATE MIN DEPOSIT"
DISPLAY "----- ---- ------ -----------"
PERFORM VARYING RT-TERM-IDX FROM 1 BY 1
UNTIL RT-TERM-IDX > RT-NUM-TERMS
PERFORM VARYING RT-TIER-IDX FROM 1 BY 1
UNTIL RT-TIER-IDX >
RT-NUM-TIERS(RT-TERM-IDX)
DISPLAY
RT-TERM-MONTHS(RT-TERM-IDX)
" "
RT-TIER-CODE(RT-TERM-IDX,
RT-TIER-IDX)
" "
RT-ANNUAL-RATE(RT-TERM-IDX,
RT-TIER-IDX)
" "
RT-MIN-DEPOSIT(RT-TERM-IDX,
RT-TIER-IDX)
END-PERFORM
END-PERFORM
.
🧪 Try It Yourself: Rate Table Enhancement
Modify the rate table program to add the following features: 1. After loading, verify that the tiers within each term are in ascending order by tier code 2. Add a "best rate" function that finds the highest rate across all terms for a given tier 3. Add an "all rates" display for a given tier, showing rates across all terms 4. Handle a lookup for a term that is between two defined terms (e.g., if 12 and 24 are defined, a request for 18 months returns the 12-month rate)
Error Recovery Patterns
In production, tables sometimes fail to load correctly — the file might be empty, corrupted, or contain unexpected data. GlobalBank requires fallback handling:
1600-LOAD-WITH-FALLBACK.
PERFORM 1000-LOAD-RATE-TABLE
IF RT-NUM-TERMS = ZERO
DISPLAY "WARNING: RATE TABLE EMPTY"
DISPLAY "LOADING DEFAULT RATES"
PERFORM 1700-LOAD-DEFAULT-RATES
END-IF
IF RT-NUM-TERMS < 3
DISPLAY "WARNING: RATE TABLE INCOMPLETE"
DISPLAY "ONLY " RT-NUM-TERMS " TERM GROUPS"
DISPLAY "EXPECTED AT LEAST 3"
PERFORM 9100-SEND-ALERT
END-IF
.
1700-LOAD-DEFAULT-RATES.
* Hardcoded emergency defaults
MOVE 1 TO RT-NUM-TERMS
MOVE 12 TO RT-TERM-MONTHS(1)
MOVE 1 TO RT-NUM-TIERS(1)
MOVE "A" TO RT-TIER-CODE(1, 1)
MOVE 03.5000 TO RT-ANNUAL-RATE(1, 1)
MOVE 1000.00 TO RT-MIN-DEPOSIT(1, 1)
DISPLAY "DEFAULT RATE LOADED: 12mo/A/3.50%"
.
This fallback pattern ensures the program can always process transactions, even if the rate file is unavailable, by falling back to conservative default rates. The alert mechanism (paragraph 9100) notifies operations staff to investigate the file problem.
18.9 MedClaim Case Study: Fee Schedule Tables
MedClaim's fee schedule is a critical business table that determines how much the insurance company will pay for each medical procedure. The table varies by provider type and procedure code.
Table Structure
01 FEE-SCHEDULE-AREA.
05 FS-ENTRY-COUNT PIC 9(5) COMP.
05 FS-ENTRY OCCURS 1 TO 15000 TIMES
DEPENDING ON FS-ENTRY-COUNT
ASCENDING KEY IS
FS-PROV-TYPE
FS-PROC-CODE
INDEXED BY FS-IDX.
10 FS-PROV-TYPE PIC X(2).
10 FS-PROC-CODE PIC X(5).
10 FS-ALLOWED-AMT PIC 9(7)V99.
10 FS-EFF-DATE PIC 9(8).
10 FS-END-DATE PIC 9(8).
10 FS-MOD-FACTORS.
15 FS-MOD-1 PIC X(2).
15 FS-MOD-2 PIC X(2).
Loading and Validating
1200-LOAD-FEE-SCHEDULE.
MOVE ZERO TO FS-ENTRY-COUNT
MOVE SPACES TO WS-PREV-KEY
READ FEE-SCHED-FILE INTO WS-FEE-INPUT
AT END SET WS-FEE-EOF TO TRUE
END-READ
PERFORM UNTIL WS-FEE-EOF
ADD 1 TO FS-ENTRY-COUNT
* Boundary check
IF FS-ENTRY-COUNT > 15000
DISPLAY "FEE SCHEDULE TABLE OVERFLOW"
DISPLAY "MAX ENTRIES: 15000"
PERFORM 9900-ABEND
END-IF
* Move fields
MOVE WS-FI-PROV-TYPE TO
FS-PROV-TYPE(FS-ENTRY-COUNT)
MOVE WS-FI-PROC-CODE TO
FS-PROC-CODE(FS-ENTRY-COUNT)
MOVE WS-FI-ALLOWED TO
FS-ALLOWED-AMT(FS-ENTRY-COUNT)
MOVE WS-FI-EFF-DATE TO
FS-EFF-DATE(FS-ENTRY-COUNT)
MOVE WS-FI-END-DATE TO
FS-END-DATE(FS-ENTRY-COUNT)
* Sort order validation
STRING WS-FI-PROV-TYPE WS-FI-PROC-CODE
DELIMITED BY SIZE
INTO WS-CURR-KEY
END-STRING
IF WS-CURR-KEY < WS-PREV-KEY
DISPLAY "FEE SCHEDULE NOT IN SORT ORDER"
DISPLAY "AT ENTRY: " FS-ENTRY-COUNT
PERFORM 9900-ABEND
END-IF
MOVE WS-CURR-KEY TO WS-PREV-KEY
READ FEE-SCHED-FILE INTO WS-FEE-INPUT
AT END SET WS-FEE-EOF TO TRUE
END-READ
END-PERFORM
DISPLAY "FEE SCHEDULE LOADED: "
FS-ENTRY-COUNT " ENTRIES"
.
Fee Lookup with Date Validation
3200-LOOKUP-FEE.
SEARCH ALL FS-ENTRY
AT END
SET WS-FEE-NOT-FOUND TO TRUE
MOVE ZERO TO WS-ALLOWED-AMOUNT
WHEN FS-PROV-TYPE(FS-IDX) =
WS-CLAIM-PROV-TYPE
AND FS-PROC-CODE(FS-IDX) =
WS-CLAIM-PROC-CODE
* Found the code - now check date range
IF WS-CLAIM-SERVICE-DATE >=
FS-EFF-DATE(FS-IDX)
AND WS-CLAIM-SERVICE-DATE <=
FS-END-DATE(FS-IDX)
SET WS-FEE-FOUND TO TRUE
MOVE FS-ALLOWED-AMT(FS-IDX)
TO WS-ALLOWED-AMOUNT
ELSE
SET WS-FEE-EXPIRED TO TRUE
MOVE ZERO TO WS-ALLOWED-AMOUNT
END-IF
END-SEARCH
.
🔗 Cross-Reference: This fee schedule lookup pattern connects directly to the claim adjudication pipeline covered in Chapter 30 (Batch Processing Patterns) and the VSAM-based version in Chapter 25.
18.10 Defensive Programming for Tables
Tables are among the most error-prone areas of COBOL programming. Here are essential defensive practices.
Subscript/Index Boundary Checking
Many COBOL compilers offer a compile-time option to generate runtime boundary checks (e.g., IBM's SSRANGE compiler option). This should always be enabled in development and testing:
//COBOL.SYSIN DD *
CBL SSRANGE
IDENTIFICATION DIVISION.
However, SSRANGE adds overhead and is sometimes disabled in production. Explicit boundary checking is a stronger defense:
3500-SAFE-TABLE-ACCESS.
IF WS-TABLE-INDEX < 1
OR WS-TABLE-INDEX > WS-TABLE-COUNT
DISPLAY "TABLE INDEX OUT OF RANGE: "
WS-TABLE-INDEX
DISPLAY "VALID RANGE: 1 TO "
WS-TABLE-COUNT
PERFORM 9900-ABEND
END-IF
MOVE TABLE-FIELD(WS-TABLE-INDEX)
TO WS-OUTPUT-FIELD
.
Uninitialized Table Detection
Before first use, verify a table has been loaded:
01 WS-TABLE-FLAGS.
05 WS-PROC-TABLE-LOADED PIC X(1) VALUE "N".
88 PROC-TABLE-IS-LOADED VALUE "Y".
*> In lookup paragraph:
IF NOT PROC-TABLE-IS-LOADED
DISPLAY "PROC TABLE NOT LOADED - ABENDING"
PERFORM 9900-ABEND
END-IF
Table Overflow Protection
Always check before adding entries:
IF WS-TABLE-COUNT >= WS-TABLE-MAX
DISPLAY "TABLE OVERFLOW: " WS-TABLE-NAME
DISPLAY "CURRENT COUNT: " WS-TABLE-COUNT
DISPLAY "MAXIMUM: " WS-TABLE-MAX
PERFORM 9900-ABEND
END-IF
ADD 1 TO WS-TABLE-COUNT
Empty Table Handling
Handle the case where a table load produces zero entries:
IF PROC-TABLE-COUNT = ZERO
DISPLAY "WARNING: PROC TABLE IS EMPTY"
DISPLAY "CHECK INPUT FILE: " WS-PROC-FILE-NAME
PERFORM 9900-ABEND
END-IF
✅ Best Practice Checklist for Table Handling: 1. Always validate subscripts/indexes before access 2. Always SET index TO 1 before SEARCH 3. Always verify sort order before SEARCH ALL 4. Always check for table overflow during loading 5. Always handle AT END in SEARCH/SEARCH ALL 6. Always verify the table is loaded before first lookup 7. Use SSRANGE during development 8. Count and report lookup failures
18.11 Table Initialization and Cleanup Patterns
Before we move to advanced techniques, let us address a commonly overlooked topic: table initialization. How you initialize a table affects both correctness and debugging.
Initializing with INITIALIZE
The INITIALIZE statement recursively sets alphanumeric fields to spaces and numeric fields to zeros:
INITIALIZE ACCOUNT-TYPE-TABLE
*> All ACCT-TYPE-CODE fields become SPACES
*> All ACCT-MIN-BALANCE fields become ZERO
This is the cleanest way to prepare a table before loading. However, INITIALIZE can be slow on very large tables because it touches every byte. For performance-critical initialization of large tables, consider:
MOVE LOW-VALUES TO ACCOUNT-TYPE-TABLE
*> Sets all bytes to X'00' - fastest possible init
*> But makes alphanumeric fields contain LOW-VALUES,
*> not SPACES
Partial Reinitialization
When reusing a table across multiple iterations (e.g., processing accounts by branch in a loop), you need to reinitialize without affecting the load infrastructure:
2500-RESET-FOR-NEXT-BRANCH.
MOVE ZERO TO PROC-TABLE-COUNT
INITIALIZE PROC-TABLE-AREA
* The count is zero, so no valid data exists.
* The table storage still contains old values,
* but since we always check against
* PROC-TABLE-COUNT, the old data is unreachable.
.
⚠️ Debugging Tip: When you see stale data in a table during debugging, the most common causes are: (1) the table was not initialized before loading, (2) the count was not reset to zero between loads, or (3) the program is reading past the logical end of the table (using the OCCURS count instead of the loaded count).
Clearing Individual Entries
Sometimes you need to mark individual entries as deleted without reorganizing the table:
MOVE HIGH-VALUES TO TABLE-KEY(WS-DELETE-POS)
*> "Soft delete" - entry will sort to the end
*> and will not match any valid lookup key
*> Alternative: use a status flag
MOVE "D" TO TABLE-STATUS(WS-DELETE-POS)
*> Then in your search, add:
*> AND TABLE-STATUS(IDX) NOT = "D"
18.12 Real-World Table Design Considerations
Determining Maximum Table Size
How do you decide the maximum OCCURS count? Consider these factors:
- Current data volume: How many entries exist today?
- Growth projection: How fast is the data growing? Allow for 2-3 years of growth.
- Memory budget: How much WORKING-STORAGE can your program afford? On z/OS, the default region size limits available storage.
- Safety margin: Add 20-50% above projected maximum to avoid emergency recompilations.
At GlobalBank, the standard practice is:
Maximum OCCURS = Current count * 1.5, rounded up
to the next power of 10
So if the branch table currently has 340 entries: 340 * 1.5 = 510, rounded to 1,000. This provides ample growth room.
Table Design Patterns for Different Access Patterns
Pattern A: Lookup Only (Read-Only After Load)
*> Best: ASCENDING KEY + SEARCH ALL
*> Sort data in file before loading
*> No modification after load
Pattern B: Lookup and Update
*> Good: INDEXED BY for fast SET operations
*> Consider maintaining a "dirty" flag per entry
*> Write back changed entries at end of processing
Pattern C: Accumulator Table
*> Table used to accumulate totals by category
01 CATEGORY-TOTALS.
05 CAT-ENTRY OCCURS 50 TIMES
INDEXED BY CAT-IDX.
10 CAT-CODE PIC X(3).
10 CAT-COUNT PIC 9(7) COMP.
10 CAT-TOTAL-AMT PIC 9(11)V99 COMP-3.
10 CAT-MIN-AMT PIC 9(9)V99 COMP-3.
10 CAT-MAX-AMT PIC 9(9)V99 COMP-3.
*> For each transaction:
SET CAT-IDX TO 1
SEARCH CAT-ENTRY
AT END
PERFORM 3300-ADD-NEW-CATEGORY
WHEN CAT-CODE(CAT-IDX) = WS-TRANS-CATEGORY
ADD 1 TO CAT-COUNT(CAT-IDX)
ADD WS-TRANS-AMOUNT TO
CAT-TOTAL-AMT(CAT-IDX)
IF WS-TRANS-AMOUNT < CAT-MIN-AMT(CAT-IDX)
MOVE WS-TRANS-AMOUNT TO
CAT-MIN-AMT(CAT-IDX)
END-IF
IF WS-TRANS-AMOUNT > CAT-MAX-AMT(CAT-IDX)
MOVE WS-TRANS-AMOUNT TO
CAT-MAX-AMT(CAT-IDX)
END-IF
END-SEARCH
This accumulator pattern is extremely common in batch reporting programs. Derek Washington estimated that 60% of GlobalBank's batch programs use at least one accumulator table.
Pattern D: Configuration Table
*> Loaded from a parameter file at program start
*> Controls program behavior (thresholds, flags, limits)
01 CONFIG-TABLE.
05 CFG-ENTRY OCCURS 20 TIMES
ASCENDING KEY IS CFG-PARAM-NAME
INDEXED BY CFG-IDX.
10 CFG-PARAM-NAME PIC X(20).
10 CFG-PARAM-VALUE PIC X(50).
10 CFG-PARAM-TYPE PIC X(1).
88 CFG-IS-NUMERIC VALUE "N".
88 CFG-IS-TEXT VALUE "T".
88 CFG-IS-FLAG VALUE "F".
Configuration tables allow programs to be modified without recompilation — changing thresholds, enabling/disabling features, and adjusting limits by editing the parameter file instead of the source code.
💡 Production Wisdom: Maria Chen's rule: "If a value might change in the next two years, put it in a configuration table, not in the source code. Recompiling a production COBOL program requires change management approval, regression testing, and a deployment window. Editing a parameter file takes five minutes."
Handling Duplicate Keys
When a table may contain duplicate keys, SEARCH ALL becomes problematic because it might find any one of the duplicates. For duplicate-aware lookups:
*> Strategy 1: SEARCH (serial) to find ALL matches
SET TBL-IDX TO 1
MOVE ZERO TO WS-MATCH-COUNT
SEARCH TBL-ENTRY
AT END
CONTINUE
WHEN TBL-KEY(TBL-IDX) = WS-LOOKUP-KEY
ADD 1 TO WS-MATCH-COUNT
PERFORM 3200-PROCESS-MATCH
* Continue searching from next position
SET TBL-IDX UP BY 1
IF TBL-IDX <= TBL-COUNT
GO TO SEARCH-AGAIN
END-IF
END-SEARCH
*> Strategy 2: SEARCH ALL to find first, then scan
*> forward and backward for adjacent duplicates
SEARCH ALL TBL-ENTRY
AT END
SET WS-NOT-FOUND TO TRUE
WHEN TBL-KEY(TBL-IDX) = WS-LOOKUP-KEY
PERFORM 3300-SCAN-DUPLICATES
END-SEARCH
The second strategy is more efficient for large tables: the binary search quickly locates one matching entry, then a short linear scan in both directions finds all adjacent entries with the same key (since the table is sorted, duplicates are adjacent).
18.13 Advanced Table Techniques
Table Sorting in COBOL
If your table arrives unsorted and you need to use SEARCH ALL, you can sort it in place. While COBOL does not have a built-in array sort statement, a simple bubble sort works for tables under a few hundred entries:
5000-SORT-TABLE.
PERFORM VARYING WS-OUTER FROM 1 BY 1
UNTIL WS-OUTER >= WS-TABLE-COUNT
PERFORM VARYING WS-INNER FROM 1 BY 1
UNTIL WS-INNER > WS-TABLE-COUNT
- WS-OUTER
IF TABLE-KEY(WS-INNER) >
TABLE-KEY(WS-INNER + 1)
MOVE TABLE-ENTRY(WS-INNER)
TO WS-TEMP-ENTRY
MOVE TABLE-ENTRY(WS-INNER + 1)
TO TABLE-ENTRY(WS-INNER)
MOVE WS-TEMP-ENTRY
TO TABLE-ENTRY(WS-INNER + 1)
END-IF
END-PERFORM
END-PERFORM.
For large tables, consider using the SORT statement with an input/output procedure, or sort the file before loading.
Multiple Search Keys on the Same Table
Sometimes you need to search a table by different keys at different times. Since SEARCH ALL only works with the declared ASCENDING/DESCENDING KEY, you have two options:
- Use SEARCH (serial) for alternate keys
- Maintain a separate index table for each alternate key
*> Primary table sorted by procedure code
01 PROC-TABLE.
05 PROC-ENTRY OCCURS 5000 TIMES
ASCENDING KEY IS PROC-CODE
INDEXED BY PROC-IDX.
10 PROC-CODE PIC X(5).
10 PROC-DESC PIC X(50).
10 PROC-CAT PIC X(3).
*> Cross-reference index sorted by category
01 PROC-CAT-XREF.
05 CAT-XREF-COUNT PIC 9(5) COMP.
05 CAT-XREF-ENTRY OCCURS 5000 TIMES
ASCENDING KEY IS CX-CATEGORY
INDEXED BY CX-IDX.
10 CX-CATEGORY PIC X(3).
10 CX-PROC-POS PIC 9(5) COMP.
The cross-reference table stores the category code and the position in the primary table. Search the cross-reference by category, then use the position to access the full record from the primary table.
88-Level Conditions in Tables
You can define 88-level condition names on table entries for cleaner logic:
01 STATUS-TABLE.
05 STATUS-ENTRY OCCURS 10 TIMES
INDEXED BY STAT-IDX.
10 STATUS-CODE PIC X(2).
10 STATUS-DESC PIC X(20).
10 STATUS-TYPE PIC X(1).
88 STATUS-IS-ACTIVE VALUE "A".
88 STATUS-IS-PENDING VALUE "P".
88 STATUS-IS-CLOSED VALUE "C".
Then in your logic:
IF STATUS-IS-ACTIVE(STAT-IDX)
PERFORM 3100-PROCESS-ACTIVE
END-IF
18.12 The Student Mainframe Lab
🧪 Try It Yourself: Building a State Code Lookup
In this lab, you will create a program that demonstrates all three search strategies on the same data. Create a table of US state codes and names, then look up states using:
- Direct indexing (use numeric FIPS codes as subscripts)
- Serial search (SEARCH by state abbreviation)
- Binary search (SEARCH ALL by state name)
Steps: 1. Define the state table with at least 10 entries 2. Load it using REDEFINES on hardcoded values 3. Implement all three lookup methods 4. Time each method using FUNCTION CURRENT-DATE before and after 10,000 lookups 5. Display the results comparison
🧪 Try It Yourself: Multi-Dimensional Grade Table
Build a grade lookup table that varies by department (5 departments) and course level (4 levels: 100, 200, 300, 400). Each cell contains a letter grade cutoff percentage.
- Define the two-dimensional table
- Load it from hardcoded values
- Accept a department number, course level, and student percentage
- Look up the appropriate cutoff and determine the letter grade
- Handle invalid department and course level gracefully
18.13 Common Mistakes and Debugging
Mistake 1: Forgetting to SET Index Before SEARCH
*> WRONG - index may be at any position
SEARCH ACCT-ENTRY
AT END ...
WHEN ...
END-SEARCH
*> CORRECT
SET ACCT-IDX TO 1
SEARCH ACCT-ENTRY
AT END ...
WHEN ...
END-SEARCH
Mistake 2: Using SEARCH ALL on Unsorted Data
The program will not abend — it will simply produce incorrect results, finding items that exist or failing to find items that are there. This is especially insidious because the bug is intermittent.
Mistake 3: Subscript Off-by-One
COBOL tables are 1-based. Element(0) does not exist and will cause unpredictable results:
*> WRONG
PERFORM VARYING WS-IDX FROM 0 BY 1
UNTIL WS-IDX > TABLE-COUNT
DISPLAY TABLE-FIELD(WS-IDX)
END-PERFORM
*> CORRECT
PERFORM VARYING WS-IDX FROM 1 BY 1
UNTIL WS-IDX > TABLE-COUNT
DISPLAY TABLE-FIELD(WS-IDX)
END-PERFORM
Mistake 4: SEARCH ALL with Multiple Non-ANDed Conditions
*> WRONG - OR is not allowed in SEARCH ALL
SEARCH ALL TBL-ENTRY
WHEN TBL-KEY(IDX) = "A"
OR TBL-KEY(IDX) = "B"
...
END-SEARCH
*> CORRECT - Use two separate SEARCH ALL calls
SEARCH ALL TBL-ENTRY
WHEN TBL-KEY(IDX) = "A"
PERFORM 3100-PROCESS
END-SEARCH
IF NOT WS-FOUND
SEARCH ALL TBL-ENTRY
WHEN TBL-KEY(IDX) = "B"
PERFORM 3100-PROCESS
END-SEARCH
END-IF
Mistake 5: Modifying ODO Object During Table Access
*> DANGEROUS - changing BRANCH-COUNT while looping
PERFORM VARYING WS-IDX FROM 1 BY 1
UNTIL WS-IDX > BRANCH-COUNT
IF BRANCH-STATUS(WS-IDX) = "D"
SUBTRACT 1 FROM BRANCH-COUNT *> DON'T!
END-IF
END-PERFORM
18.14 Performance Considerations
Compile Options
- SSRANGE: Enable boundary checking in development; consider disabling in production for performance-critical batch (after thorough testing).
- OPTIMIZE: The compiler can optimize table access with OPT(2); it may convert sequential SEARCH loops into more efficient machine code.
- TRUNC(OPT): Can affect how subscripts are truncated; be cautious with large table sizes.
Memory Placement
For very large tables that are read-only during execution, consider placing them in LINKAGE SECTION and loading via subprogram, or using external data areas. This can improve memory utilization in CICS environments where multiple program instances share the same table.
When Tables Are Not Enough
If your table exceeds roughly 100,000 entries or needs to be updated dynamically during execution, consider: - VSAM KSDS files for key-sequenced access (Chapter 25) - DB2 tables with cached result sets (Chapter 28) - Shared memory (dataspace/hiperspaces) for very large read-only tables on z/OS
18.15 Complete Worked Example: MedClaim Diagnosis Code Lookup System
Let us bring together all the concepts from this chapter into a complete, production-quality program. This program loads a diagnosis code table from a file, verifies sort order, and performs lookups for a batch of claims. It demonstrates every defensive programming practice we have covered.
IDENTIFICATION DIVISION.
PROGRAM-ID. DIAGLKUP.
*============================================================
* MedClaim Diagnosis Code Lookup System
* Loads ICD-10 diagnosis codes, verifies sort order,
* performs binary search lookups, and reports statistics.
*============================================================
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT DIAG-FILE
ASSIGN TO "DIAGCODE.DAT"
ORGANIZATION IS LINE SEQUENTIAL
FILE STATUS IS WS-DIAG-FS.
SELECT CLAIM-FILE
ASSIGN TO "CLAIMS.DAT"
ORGANIZATION IS LINE SEQUENTIAL
FILE STATUS IS WS-CLAIM-FS.
DATA DIVISION.
FILE SECTION.
FD DIAG-FILE.
01 DIAG-INPUT-REC.
05 DI-CODE PIC X(7).
05 DI-DESCRIPTION PIC X(60).
05 DI-CATEGORY PIC X(3).
05 DI-SEVERITY PIC 9.
FD CLAIM-FILE.
01 CLAIM-INPUT-REC.
05 CI-CLAIM-ID PIC X(12).
05 CI-DIAG-CODE PIC X(7).
05 CI-AMOUNT PIC 9(7)V99.
WORKING-STORAGE SECTION.
01 WS-FILE-STATUS.
05 WS-DIAG-FS PIC X(2).
05 WS-CLAIM-FS PIC X(2).
01 WS-EOF-FLAGS.
05 WS-DIAG-EOF PIC X VALUE "N".
88 DIAG-EOF VALUE "Y".
05 WS-CLAIM-EOF PIC X VALUE "N".
88 CLAIM-EOF VALUE "Y".
01 WS-TABLE-LOADED PIC X VALUE "N".
88 TABLE-IS-LOADED VALUE "Y".
* Diagnosis code table
01 DIAG-TABLE-AREA.
05 DT-COUNT PIC 9(5) COMP VALUE 0.
05 DT-MAX PIC 9(5) COMP VALUE 20000.
05 DT-ENTRY OCCURS 1 TO 20000 TIMES
DEPENDING ON DT-COUNT
ASCENDING KEY IS DT-CODE
INDEXED BY DT-IDX.
10 DT-CODE PIC X(7).
10 DT-DESCRIPTION PIC X(60).
10 DT-CATEGORY PIC X(3).
10 DT-SEVERITY PIC 9.
* Statistics
01 WS-STATS.
05 WS-TOTAL-LOOKUPS PIC 9(7) VALUE 0.
05 WS-FOUND-COUNT PIC 9(7) VALUE 0.
05 WS-NOT-FOUND-COUNT PIC 9(7) VALUE 0.
05 WS-PREV-CODE PIC X(7) VALUE
LOW-VALUES.
05 WS-SORT-ERRORS PIC 9(5) VALUE 0.
* Lookup result
01 WS-LOOKUP-RESULT.
05 WS-LR-DESC PIC X(60).
05 WS-LR-CAT PIC X(3).
05 WS-LR-SEV PIC 9.
05 WS-LR-STATUS PIC X VALUE SPACE.
88 WS-LR-FOUND VALUE "F".
88 WS-LR-MISSING VALUE "M".
PROCEDURE DIVISION.
0000-MAIN.
PERFORM 1000-INITIALIZE
PERFORM 2000-PROCESS-CLAIMS
PERFORM 3000-REPORT-STATISTICS
PERFORM 9000-TERMINATE
STOP RUN
.
1000-INITIALIZE.
PERFORM 1100-LOAD-DIAG-TABLE
IF NOT TABLE-IS-LOADED
DISPLAY "FATAL: DIAGNOSIS TABLE NOT LOADED"
STOP RUN
END-IF
OPEN INPUT CLAIM-FILE
IF WS-CLAIM-FS NOT = "00"
DISPLAY "CLAIM FILE OPEN ERROR: "
WS-CLAIM-FS
STOP RUN
END-IF
READ CLAIM-FILE
AT END SET CLAIM-EOF TO TRUE
END-READ
.
1100-LOAD-DIAG-TABLE.
OPEN INPUT DIAG-FILE
IF WS-DIAG-FS NOT = "00"
DISPLAY "DIAG FILE OPEN ERROR: "
WS-DIAG-FS
EXIT PARAGRAPH
END-IF
MOVE 0 TO DT-COUNT
MOVE LOW-VALUES TO WS-PREV-CODE
MOVE 0 TO WS-SORT-ERRORS
READ DIAG-FILE
AT END SET DIAG-EOF TO TRUE
END-READ
PERFORM UNTIL DIAG-EOF
* Overflow check
IF DT-COUNT >= DT-MAX
DISPLAY "TABLE OVERFLOW AT " DT-COUNT
CLOSE DIAG-FILE
EXIT PARAGRAPH
END-IF
ADD 1 TO DT-COUNT
MOVE DI-CODE TO DT-CODE(DT-COUNT)
MOVE DI-DESCRIPTION TO
DT-DESCRIPTION(DT-COUNT)
MOVE DI-CATEGORY TO
DT-CATEGORY(DT-COUNT)
MOVE DI-SEVERITY TO
DT-SEVERITY(DT-COUNT)
* Sort order check
IF DT-CODE(DT-COUNT) <=
WS-PREV-CODE
ADD 1 TO WS-SORT-ERRORS
IF WS-SORT-ERRORS <= 5
DISPLAY "SORT ERROR #"
WS-SORT-ERRORS
" AT ENTRY " DT-COUNT
": " DT-CODE(DT-COUNT)
" <= " WS-PREV-CODE
END-IF
END-IF
MOVE DT-CODE(DT-COUNT) TO WS-PREV-CODE
READ DIAG-FILE
AT END SET DIAG-EOF TO TRUE
END-READ
END-PERFORM
CLOSE DIAG-FILE
* Validate results
IF DT-COUNT = 0
DISPLAY "WARNING: DIAG TABLE EMPTY"
EXIT PARAGRAPH
END-IF
IF WS-SORT-ERRORS > 0
DISPLAY WS-SORT-ERRORS
" SORT ERRORS - CANNOT USE "
"BINARY SEARCH"
EXIT PARAGRAPH
END-IF
MOVE "Y" TO WS-TABLE-LOADED
DISPLAY "LOADED " DT-COUNT
" DIAGNOSIS CODES"
.
2000-PROCESS-CLAIMS.
PERFORM UNTIL CLAIM-EOF
ADD 1 TO WS-TOTAL-LOOKUPS
PERFORM 2100-LOOKUP-DIAGNOSIS
READ CLAIM-FILE
AT END SET CLAIM-EOF TO TRUE
END-READ
END-PERFORM
.
2100-LOOKUP-DIAGNOSIS.
MOVE SPACES TO WS-LR-DESC
SEARCH ALL DT-ENTRY
AT END
SET WS-LR-MISSING TO TRUE
ADD 1 TO WS-NOT-FOUND-COUNT
WHEN DT-CODE(DT-IDX) = CI-DIAG-CODE
SET WS-LR-FOUND TO TRUE
ADD 1 TO WS-FOUND-COUNT
MOVE DT-DESCRIPTION(DT-IDX)
TO WS-LR-DESC
MOVE DT-CATEGORY(DT-IDX)
TO WS-LR-CAT
MOVE DT-SEVERITY(DT-IDX)
TO WS-LR-SEV
END-SEARCH
.
3000-REPORT-STATISTICS.
DISPLAY " "
DISPLAY "=== LOOKUP STATISTICS ==="
DISPLAY "TOTAL LOOKUPS: " WS-TOTAL-LOOKUPS
DISPLAY "FOUND: " WS-FOUND-COUNT
DISPLAY "NOT FOUND: " WS-NOT-FOUND-COUNT
IF WS-TOTAL-LOOKUPS > 0
DISPLAY "HIT RATE: "
FUNCTION INTEGER(
WS-FOUND-COUNT * 100
/ WS-TOTAL-LOOKUPS) "%"
END-IF
.
9000-TERMINATE.
CLOSE CLAIM-FILE
DISPLAY "PROGRAM DIAGLKUP COMPLETE"
.
This program demonstrates: 1. File status checking after every I/O operation 2. Table overflow protection during loading 3. Sort order verification with error counting 4. Table-loaded flag check before first use 5. SEARCH ALL for efficient binary search 6. AT END handling with lookup failure counting 7. Statistics reporting for operational monitoring 8. Clean program structure with clear paragraph responsibilities
✅ Production Checklist: Before deploying any table-driven program, verify: - [ ] Maximum table size is large enough for current data + growth - [ ] File status is checked after OPEN - [ ] Table overflow is detected and handled - [ ] Sort order is verified before SEARCH ALL - [ ] Empty table is detected and handled - [ ] AT END conditions are handled in all searches - [ ] Lookup statistics are reported for monitoring - [ ] Table-loaded flag prevents searches before data is loaded
Monitoring Table Lookup Performance in Production
In production batch programs, tracking table lookup statistics helps identify performance problems and data quality issues. Here is a monitoring pattern used at MedClaim:
01 WS-LOOKUP-STATS.
05 WS-LS-TOTAL PIC 9(9) VALUE 0.
05 WS-LS-FOUND PIC 9(9) VALUE 0.
05 WS-LS-MISSED PIC 9(9) VALUE 0.
05 WS-LS-PCT PIC ZZ9.99.
05 WS-LS-TOP-MISS.
10 WS-TM-ENTRY OCCURS 10 TIMES.
15 WS-TM-KEY PIC X(7).
15 WS-TM-COUNT PIC 9(5).
05 WS-LS-TM-COUNT PIC 99 VALUE 0.
After each lookup:
ADD 1 TO WS-LS-TOTAL
IF WS-LR-FOUND
ADD 1 TO WS-LS-FOUND
ELSE
ADD 1 TO WS-LS-MISSED
PERFORM TRACK-MISSED-KEY
END-IF
The TRACK-MISSED-KEY paragraph maintains a "top 10 missed keys" list, which is invaluable for identifying data quality problems. If a particular diagnosis code keeps failing lookup, it might indicate a new code that has not been added to the reference table, a data entry error pattern, or a mapping issue between systems.
At the end of processing, the statistics are written to both the batch log and a monitoring database:
REPORT-LOOKUP-STATS.
COMPUTE WS-LS-PCT =
(WS-LS-FOUND * 100) / WS-LS-TOTAL
DISPLAY "TABLE LOOKUP STATISTICS:"
DISPLAY " TOTAL LOOKUPS: " WS-LS-TOTAL
DISPLAY " FOUND: " WS-LS-FOUND
DISPLAY " NOT FOUND: " WS-LS-MISSED
DISPLAY " HIT RATE: " WS-LS-PCT "%"
IF WS-LS-TM-COUNT > 0
DISPLAY " TOP MISSED KEYS:"
PERFORM VARYING WS-IDX FROM 1 BY 1
UNTIL WS-IDX > WS-LS-TM-COUNT
DISPLAY " " WS-TM-KEY(WS-IDX)
" (" WS-TM-COUNT(WS-IDX)
" misses)"
END-PERFORM
END-IF
.
📊 Operational Intelligence: James Okafor reviews lookup statistics weekly. When the hit rate drops below 99.5%, it triggers an investigation. Common causes include: a new quarter's procedure codes not yet loaded, a data feed from a new provider using non-standard codes, or a corrupt reference file. Without monitoring, these issues would manifest as silent claim processing errors.
Table Handling Across CICS and Batch
A brief note on how table handling differs between CICS (online) and batch environments, relevant because many COBOL developers work in both:
In batch programs, tables are loaded at initialization (paragraph 1000-INITIALIZE) and remain in memory for the entire run. Memory is typically plentiful, and the program has exclusive access to its storage.
In CICS programs, tables present additional considerations: - Each transaction instance gets its own WORKING-STORAGE, so loading a large table per transaction is wasteful - Shared tables should be placed in the CWA (Common Work Area) or a shared data area - Table loading should happen during CICS startup (via PLTPI programs), not per transaction - The COMMAREA can pass small tables between programs, but size limits apply (32K in most CICS configurations)
We will cover CICS-specific table patterns in Chapter 32 (CICS Transaction Processing).
18.16 Table Handling in the Enterprise Context
Understanding tables in isolation is necessary but not sufficient. In the enterprise environment where GlobalBank and MedClaim operate, table handling intersects with several broader concerns that merit discussion.
Table Data Governance
Who owns the data in a table? At GlobalBank, the account type table is owned by the Product Management department. Changes to account types must go through a formal change control process because they affect downstream systems. The branch table is owned by Operations. The interest rate table is owned by Treasury. Each table owner is responsible for ensuring data quality and timely updates.
When a table changes, all programs that use it must be retested. This is why copybooks (covered in Section 18.6) are critical — they centralize the table definition so that a structure change only requires updating one copybook, not dozens of programs. The programs must still be recompiled, but the source code changes are minimal.
Table Refresh Strategies
In a 24/7 banking environment, tables must be refreshed without taking systems offline. Three strategies are used:
-
Batch reload: The simplest approach — reload the table from the file at the start of each batch cycle. This is what we demonstrated in Section 18.6. It works for batch programs but not for online (CICS) programs that run continuously.
-
Hot swap: Maintain two copies of the table in memory (A and B). Load the new data into the inactive copy while the active copy continues serving requests. Then atomically switch the active pointer. This is the standard CICS approach for shared tables.
-
Version-dated entries: Include effective dates on each table entry. Instead of replacing old entries, add new entries with future effective dates. The lookup logic checks the effective date to select the correct entry. This is how MedClaim's fee schedule works — old fees remain for historical claim processing while new fees apply to current claims.
Table Testing Strategies
Derek Washington learned a painful lesson early in his career: table-driven programs must be tested with edge-case table data, not just edge-case transaction data. His testing checklist includes:
- Empty table: What happens if the table file is empty?
- Single entry: What happens with exactly one entry?
- Full table: What happens when the table is at maximum capacity?
- Overflow: What happens when one more entry than the maximum is loaded?
- Duplicate keys: What happens when two entries have the same key?
- Out-of-order data: What happens when SEARCH ALL is used on unsorted data?
- Missing keys: What happens when a lookup key does not exist in the table?
- Boundary keys: What happens with the first key, last key, and keys just outside the valid range?
💡 Testing Wisdom: "If your test data does not include an empty table test and a full table test, you have not tested your table handling." — Maria Chen
18.17 Chapter Summary
Tables are fundamental to virtually every production COBOL program. In this chapter, you have learned:
- The OCCURS clause defines table storage, with rules governing its placement and restrictions
- Subscripts provide intuitive element access but require runtime multiplication; indexes store byte displacements and are faster
- Multi-dimensional tables (up to 7 levels) model matrix-structured business data
- SEARCH performs serial lookup, starting from the current index position
- SEARCH ALL performs binary lookup on sorted data with equality conditions
- OCCURS DEPENDING ON creates variable-length tables, requiring careful validation of the DEPENDING ON value
- Tables can be loaded from hardcoded values (REDEFINES), files (READ loops), or copybooks (COPY)
- Defensive programming practices — boundary checking, sort verification, overflow protection — are essential for production reliability
- Direct indexing (O(1)), serial search (O(n)), and binary search (O(log n)) serve different use cases based on table size, sort order, and search requirements
In the next chapter, we will explore reference modification and pointer-based processing, which give you the ability to manipulate data at the byte level — a capability that becomes essential when processing variable-format records and interfacing with modern data interchange systems.
"I have seen billion-dollar systems brought down by a single subscript that went one position past the end of a table. The SSRANGE option exists for a reason — use it." — Priya Kapoor, Architect, GlobalBank