COBOL Table Handling: OCCURS, INDEXED BY, SEARCH, and SEARCH ALL

COBOL tables (the COBOL term for arrays) are defined with the OCCURS clause and searched with SEARCH or SEARCH ALL. Unlike languages where arrays are first-class types, COBOL tables are embedded within the record hierarchy, making them a natural fit for the fixed-length, structure-oriented data that mainframe programs process. This guide covers everything from basic one-dimensional tables through variable-length DEPENDING ON tables and binary search.
Defining Tables with OCCURS
One-Dimensional Table
WORKING-STORAGE SECTION.
01 WS-MONTH-TABLE.
05 WS-MONTH-ENTRY OCCURS 12 TIMES
INDEXED BY WS-MONTH-IDX.
10 WME-NAME PIC X(10).
10 WME-DAYS PIC 9(2) COMP.
10 WME-QUARTER PIC 9(1) COMP.Each of the 12 entries has three fields. Total storage: 12 × 13 bytes = 156 bytes.
Access elements using a subscript (numeric field) or an index (defined by INDEXED BY):
*> Subscript access (numeric field):
MOVE 'JANUARY ' TO WME-NAME(1).
MOVE 31 TO WME-DAYS(1).
*> Index access:
SET WS-MONTH-IDX TO 1.
MOVE 'JANUARY ' TO WME-NAME(WS-MONTH-IDX).Subscript vs Index
Both subscripts and indexes access table elements, but they work differently internally:
*> Subscript — any numeric field:
01 WS-SUB PIC 9(4) COMP.
MOVE 3 TO WS-SUB.
DISPLAY WME-NAME(WS-SUB). *> accesses element 3
*> Index — defined in OCCURS, manipulated with SET:
SET WS-MONTH-IDX TO 3.
DISPLAY WME-NAME(WS-MONTH-IDX). *> accesses element 3The compiler converts a subscript to a byte offset at runtime: offset = (subscript - 1) × element_size. An index is already stored as a byte offset, so no multiplication is needed. For large tables accessed frequently, indexes are faster.
Index rules:
- Use
SETto assign a value (not MOVE or ADD) - Use
SET index UP BY norSET index DOWN BY nto increment/decrement - Indexes cannot be used in arithmetic statements
- Indexes can be compared with IF:
IF WS-MONTH-IDX > 6
Initializing Tables
VALUE on a Group Level
01 WS-FLAGS-TABLE.
05 WS-FLAG OCCURS 100 TIMES PIC X VALUE 'N'.All 100 flags initialize to 'N'. This works for simple tables where every element starts with the same value.
INITIALIZE Statement
INITIALIZE WS-MONTH-TABLE. *> numeric fields → 0, alpha fields → spaces
INITIALIZE WS-FLAGS-TABLE
REPLACING ALPHANUMERIC BY 'N'.PERFORM VARYING
For complex initialization where each element needs a different value:
PERFORM VARYING WS-SUB FROM 1 BY 1
UNTIL WS-SUB > 12
MOVE WS-MONTH-NAMES(WS-SUB) TO WME-NAME(WS-SUB)
MOVE WS-MONTH-DAYS-TABLE(WS-SUB) TO WME-DAYS(WS-SUB)
END-PERFORM.SEARCH (Linear Search)
SEARCH scans a table sequentially from the current index position until it finds a match or exhausts the table:
01 WS-STATE-TABLE.
05 WS-STATE OCCURS 50 TIMES
INDEXED BY WS-STATE-IDX.
10 WST-CODE PIC X(2).
10 WST-NAME PIC X(20).
10 WST-REGION PIC X(10).
*> ... (table loaded from file at initialization)
LOOKUP-STATE.
SET WS-STATE-IDX TO 1
SEARCH WS-STATE
AT END
MOVE 'NOT FOUND' TO WS-RESULT-REGION
SET WS-NOT-FOUND TO TRUE
WHEN WST-CODE(WS-STATE-IDX) = WS-SEARCH-CODE
MOVE WST-REGION(WS-STATE-IDX) TO WS-RESULT-REGION
SET WS-FOUND TO TRUE
END-SEARCH.Key points about SEARCH:
- Always
SET index TO 1before SEARCH (unless resuming a partial scan) - SEARCH increments the index automatically on each non-matching element
- AT END executes if SEARCH reaches the end without finding a match
- Multiple WHEN clauses are allowed — first match wins
- SEARCH works on unsorted tables
SEARCH ALL (Binary Search)
SEARCH ALL requires the table to be declared with ASCENDING KEY or DESCENDING KEY, and the actual data must be sorted in that order:
01 WS-ZIP-CODE-TABLE.
05 WS-ZIP-ENTRY OCCURS 1000 TIMES
ASCENDING KEY IS WZE-ZIP-CODE
INDEXED BY WS-ZIP-IDX.
10 WZE-ZIP-CODE PIC X(5).
10 WZE-CITY PIC X(30).
10 WZE-STATE PIC X(2).
LOOKUP-ZIP-CODE.
SEARCH ALL WS-ZIP-ENTRY
AT END
MOVE 'UNKNOWN ' TO WS-CITY
MOVE ' ' TO WS-STATE
WHEN WZE-ZIP-CODE(WS-ZIP-IDX) = WS-SEARCH-ZIP
MOVE WZE-CITY(WS-ZIP-IDX) TO WS-CITY
MOVE WZE-STATE(WS-ZIP-IDX) TO WS-STATE
END-SEARCH.Key points about SEARCH ALL:
- No need to SET the index before SEARCH ALL — it sets the index internally
- Only one WHEN clause is allowed
- The WHEN condition must compare the KEY field to a value
- The table data must actually be sorted — SEARCH ALL does not sort it
- For 1,000 elements: SEARCH checks up to 1,000; SEARCH ALL checks at most 10
Multi-Dimensional Tables
Tables can be nested up to 7 levels deep using nested OCCURS:
01 WS-SALES-TABLE.
05 WS-REGION OCCURS 5 TIMES
INDEXED BY WS-REG-IDX.
10 WS-MONTH OCCURS 12 TIMES
INDEXED BY WS-MON-IDX.
15 WSM-SALES-AMOUNT PIC S9(11)V99 COMP-3.
15 WSM-UNIT-COUNT PIC 9(7) COMP.Access requires both indexes or subscripts:
*> Subscript access:
ADD WS-TRANSACTION-AMOUNT
TO WSM-SALES-AMOUNT(WS-CURRENT-REGION WS-CURRENT-MONTH).
*> Nested PERFORM with index:
PERFORM VARYING WS-REG-IDX FROM 1 BY 1
UNTIL WS-REG-IDX > 5
AFTER WS-MON-IDX FROM 1 BY 1
UNTIL WS-MON-IDX > 12
INITIALIZE WSM-SALES-AMOUNT(WS-REG-IDX WS-MON-IDX)
INITIALIZE WSM-UNIT-COUNT(WS-REG-IDX WS-MON-IDX)
END-PERFORM.Variable-Length Tables: OCCURS DEPENDING ON
OCCURS n TIMES DEPENDING ON data-name creates a table whose active size varies at runtime:
01 WS-ORDER-RECORD.
05 OR-ORDER-ID PIC X(10).
05 OR-ITEM-COUNT PIC 9(3) COMP.
05 OR-ITEM-TABLE.
10 OR-ITEM OCCURS 1 TIMES
UP TO 50 TIMES
DEPENDING ON OR-ITEM-COUNT.
15 ORI-PRODUCT-ID PIC X(10).
15 ORI-QUANTITY PIC 9(5) COMP.
15 ORI-UNIT-PRICE PIC S9(7)V99 COMP-3.When you set OR-ITEM-COUNT to 3, only elements 1 through 3 are considered active. The record length used for file I/O is calculated as the fixed portion plus OR-ITEM-COUNT × element_size.
MOVE 3 TO OR-ITEM-COUNT
MOVE 'PROD-001 ' TO ORI-PRODUCT-ID(1)
MOVE 5 TO ORI-QUANTITY(1)
MOVE 29.99 TO ORI-UNIT-PRICE(1)
*> ... fill elements 2 and 3 ...
WRITE ORDER-RECORD-AREA. *> writes variable-length recordRules for DEPENDING ON:
- The DEPENDING ON field must be in WORKING-STORAGE, not in the table itself
- The minimum is always 1 (not 0)
- The maximum is the value after OCCURS (or UP TO for clarity)
- Moving or comparing a group that contains DEPENDING ON uses the current active length
Practical Pattern: Country Code Lookup Table
WORKING-STORAGE SECTION.
01 WS-CURRENCY-TABLE.
05 WS-CCY-ENTRY OCCURS 30 TIMES
ASCENDING KEY IS WCCY-CODE
INDEXED BY WS-CCY-IDX.
10 WCCY-CODE PIC X(3).
10 WCCY-NAME PIC X(20).
10 WCCY-SYMBOL PIC X(3).
10 WCCY-DECIMALS PIC 9(1) COMP.
01 WS-CCY-COUNT PIC 9(2) COMP VALUE 5.
PROCEDURE DIVISION.
LOAD-CURRENCY-TABLE.
MOVE 'EUR' TO WCCY-CODE(1)
MOVE 'EURO ' TO WCCY-NAME(1)
MOVE 'EUR' TO WCCY-SYMBOL(1)
MOVE 2 TO WCCY-DECIMALS(1)
MOVE 'GBP' TO WCCY-CODE(2)
MOVE 'BRITISH POUND ' TO WCCY-NAME(2)
MOVE 'GBP' TO WCCY-SYMBOL(2)
MOVE 2 TO WCCY-DECIMALS(2)
MOVE 'JPY' TO WCCY-CODE(3)
MOVE 'JAPANESE YEN ' TO WCCY-NAME(3)
MOVE 'JPY' TO WCCY-SYMBOL(3)
MOVE 0 TO WCCY-DECIMALS(3)
MOVE 'USD' TO WCCY-CODE(4)
MOVE 'US DOLLAR ' TO WCCY-NAME(4)
MOVE 'USD' TO WCCY-SYMBOL(4)
MOVE 2 TO WCCY-DECIMALS(4)
MOVE 'ZAR' TO WCCY-CODE(5)
MOVE 'SOUTH AFRICAN RAND ' TO WCCY-NAME(5)
MOVE 'ZAR' TO WCCY-SYMBOL(5)
MOVE 2 TO WCCY-DECIMALS(5).
FIND-CURRENCY.
SEARCH ALL WS-CCY-ENTRY
AT END
MOVE 'UNKNOWN CURRENCY' TO WS-ERROR-MSG
SET WS-ERROR-FOUND TO TRUE
WHEN WCCY-CODE(WS-CCY-IDX) = WS-INPUT-CCY-CODE
MOVE WCCY-NAME(WS-CCY-IDX) TO WS-DISPLAY-NAME
MOVE WCCY-DECIMALS(WS-CCY-IDX) TO WS-DECIMAL-PLACES
END-SEARCH.Next Steps
Table handling enables in-memory lookups and multi-dimensional data structures. The next batch processing topic is SORT and MERGE — sorting input files and merging sorted datasets using COBOL's built-in sort facility. See COBOL Sort and Merge, or return to the COBOL Mastery course.
