27 min read

> "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...

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 OCCURS clause 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:

  1. Level restriction: OCCURS cannot be used on 01, 66, 77, or 88 level items.
  2. 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).
  3. REDEFINES interaction: An item with OCCURS cannot also have REDEFINES, but an item with REDEFINES can contain subordinate items with OCCURS.
  4. 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:

  1. Loads the value of WS-SUB from memory
  2. Subtracts 1 (because tables are 1-based but offsets are 0-based)
  3. Multiplies the result by the element size (in this case, 41 bytes per entry)
  4. Adds the base address of the table
  5. 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:

  1. Load the index value (which is already a byte displacement)
  2. Add the base address of the table
  3. 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:

  1. Debugging: Subscripts can be displayed directly (DISPLAY WS-SUB), while indexes require conversion via SET before display.
  2. Arithmetic: Subscripts participate in arithmetic naturally (ADD 1 TO WS-SUB), while indexes require SET idx UP BY 1.
  3. Pass to subprograms: Subscripts are ordinary data items that can be passed through CALL...USING. Indexes cannot be passed directly.
  4. 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.

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

  1. 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.
  2. The WHEN condition is tested. If true, the associated action is performed and the search ends.
  3. If the condition is false, the index is automatically incremented by 1.
  4. Steps 2-3 repeat until either a WHEN condition is satisfied or the index exceeds the table size.
  5. If the index exceeds the table size without a WHEN match, the AT END clause executes.

🔴 Critical Rule: You must SET the 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

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:

  1. The table must have an ASCENDING KEY or DESCENDING KEY clause.
  2. The table must actually be sorted by that key before the search.
  3. The WHEN clause must test for equality (=) on the key field.
  4. Only one WHEN clause is allowed.
  5. Compound conditions in the WHEN clause can only use AND (not OR).
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

  1. The DEPENDING ON data item (BRANCH-COUNT) must be defined before the OCCURS item in the record.
  2. The DEPENDING ON item must be a numeric integer.
  3. The value of the DEPENDING ON item determines the current logical size of the table.
  4. The compiler allocates storage for the maximum (500 in this case).
  5. 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)

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:

  1. Current data volume: How many entries exist today?
  2. Growth projection: How fast is the data growing? Allow for 2-3 years of growth.
  3. Memory budget: How much WORKING-STORAGE can your program afford? On z/OS, the default region size limits available storage.
  4. 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:

  1. Use SEARCH (serial) for alternate keys
  2. 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:

  1. Direct indexing (use numeric FIPS codes as subscripts)
  2. Serial search (SEARCH by state abbreviation)
  3. 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.

  1. Define the two-dimensional table
  2. Load it from hardcoded values
  3. Accept a department number, course level, and student percentage
  4. Look up the appropriate cutoff and determine the letter grade
  5. Handle invalid department and course level gracefully

18.13 Common Mistakes and Debugging

*> 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:

  1. 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.

  2. 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.

  3. 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