Working with Tables (Arrays) & the OCCURS Clause

COBOL Fundamentals

Chapter 6 · Working with Tables (Arrays) & the OCCURS Clause

Every field so far has held exactly one value. Real COBOL programs constantly need many values of the same shape — twelve months of sales figures, a hundred customer records held in memory at once — and OCCURS is the real clause that makes a single data item repeat into a table.

OCCURS: Defining a Table

Reusing Chapter 5's own real counted-loop variable, WS-I, here's a real one-dimensional table of twelve monthly sales figures:

01 WS-MONTHLY-SALES. 05 WS-SALES-AMT PIC 9(6)V99 OCCURS 12 TIMES.

OCCURS 12 TIMES tells COBOL to reserve twelve real copies of WS-SALES-AMT, one after another in storage. A single element is referenced by writing a subscript directly in parentheses after the item's name:

PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 12 ADD WS-SALES-AMT(WS-I) TO WS-YEAR-TOTAL END-PERFORM.

Subscripts Start at 1, Not 0

A Genuine, Easy Trap Coming from Most Other Languages COBOL tables are real 1-based, not 0-based. WS-SALES-AMT(1) is January, the genuine first element — not January minus one. Chapter 5's own PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 12 example above already assumes this directly: it starts WS-I at 1 and stops once it exceeds 12, the real count of elements — no adjustment needed, because COBOL never had a zeroth element to begin with.

INDEXED BY: A Real, More Efficient Alternative to a Subscript

A plain numeric field like WS-I works as a subscript, but COBOL also offers a genuinely different mechanism: an index, declared with INDEXED BY directly on the table itself. Extending Chapter 3's own real WS-CUSTOMER-RECORD into a table of a hundred customers:

01 WS-CUSTOMER-TABLE. 05 WS-CUSTOMER-ENTRY OCCURS 100 TIMES INDEXED BY WS-CUST-IDX. 10 WS-CUST-ID PIC 9(6). 10 WS-CUST-NAME PIC X(30).
Why an Index Is Genuinely Faster, Not Just a Style Choice A plain subscript like WS-I holds an ordinary occurrence number (1, 2, 3, ...) that the runtime has to multiply by the element's own size every single time it's used to find the real storage address. A real index, by contrast, is stored directly as a displacement — it moves by the element's own size each step, not by a plain count of 1 — so the runtime can use it straight away with no multiplication needed. On a table accessed millions of times in a real batch job, that's a genuine, measurable difference, not a cosmetic one.

An indexed table is set and stepped with real SET statements rather than ordinary arithmetic:

SET WS-CUST-IDX TO 1. SET WS-CUST-IDX UP BY 1.

SEARCH: Real Sequential Table Lookup

SEARCH walks an indexed table one element at a time, starting from wherever the index currently points — which is exactly why a real SET ... TO 1 normally comes first, to guarantee the search starts at the beginning rather than partway through:

SET WS-CUST-IDX TO 1 SEARCH WS-CUSTOMER-ENTRY AT END DISPLAY "CUSTOMER NOT FOUND" WHEN WS-CUST-ID(WS-CUST-IDX) = 402118 DISPLAY "FOUND: " WS-CUST-NAME(WS-CUST-IDX) END-SEARCH.

AT END is the real clause that fires only if the whole table is walked with no match found. SEARCH genuinely requires the table to have been declared with INDEXED BY in the first place — it's the index itself that SEARCH advances automatically on every failed comparison.

SEARCH ALL: Real Binary Search

SEARCH ALL is a genuinely different algorithm — a real binary search, jumping to the midpoint of the remaining range each time rather than checking one element after another. That real speed comes with a real requirement: the table's own definition must declare which field it's sorted by, using ASCENDING KEY or DESCENDING KEY:

01 WS-CUSTOMER-TABLE. 05 WS-CUSTOMER-ENTRY OCCURS 100 TIMES ASCENDING KEY IS WS-CUST-ID INDEXED BY WS-CUST-IDX. 10 WS-CUST-ID PIC 9(6). 10 WS-CUST-NAME PIC X(30).
SEARCH ALL WS-CUSTOMER-ENTRY AT END DISPLAY "CUSTOMER NOT FOUND" WHEN WS-CUST-ID(WS-CUST-IDX) = 402118 DISPLAY "FOUND: " WS-CUST-NAME(WS-CUST-IDX) END-SEARCH.
A Real, Silent Failure Mode Declaring ASCENDING KEY IS WS-CUST-ID is a genuine promise to the compiler, not an instruction that sorts anything. If the real data actually loaded into the table isn't genuinely in ascending order by WS-CUST-ID, SEARCH ALL doesn't detect the mismatch — it just follows the binary-search algorithm anyway, and a real entry that's genuinely present in the table can come back as "not found," or the wrong entry can be returned, with no error raised anywhere.

Multi-Dimensional Tables

An OCCURS item can itself contain another OCCURS item, producing a genuine multi-dimensional table — here, quarterly sales figures across four real regions:

01 WS-REGIONAL-SALES. 05 WS-REGION OCCURS 4 TIMES. 10 WS-QUARTER-SALES PIC 9(7)V99 OCCURS 4 TIMES.

Referencing one specific element needs both real subscripts at once, separated by a space inside one set of parentheses — the region subscript first, then the quarter:

ADD WS-QUARTER-SALES(WS-REGION-IDX WS-QTR-IDX) TO WS-GRAND-TOTAL.

OCCURS DEPENDING ON: Real Variable-Length Tables

Every table so far has had a fixed, unchanging size. OCCURS DEPENDING ON lets a table's real size vary at runtime instead, between a stated minimum and maximum, driven by the current value of another field:

01 WS-ORDER-LINES. 05 WS-LINE-COUNT PIC 9(3). 05 WS-ORDER-LINE OCCURS 1 TO 50 TIMES DEPENDING ON WS-LINE-COUNT PIC X(20).

Here, an order can genuinely hold anywhere from 1 to 50 line items, and the true, current number in use is tracked in WS-LINE-COUNT — a real space-saving mechanism for records where the count of repeating items is itself unknown until the program actually runs.

Hands-On Exercises

Exercise 1

Using this chapter's own real WS-MONTHLY-SALES example, explain in your own words why WS-SALES-AMT(1), not WS-SALES-AMT(0), is the correct reference for January's real figure, and why this genuinely matters for someone coming from a language where arrays start at 0.

📄 View solution
Exercise 2

Using this chapter's own real material, explain in your own words the specific, genuine reason an INDEXED BY index is faster than an ordinary numeric subscript like WS-I, and why SEARCH specifically requires a table to be declared with INDEXED BY in the first place.

📄 View solution
Exercise 3

A colleague declares WS-CUSTOMER-ENTRY OCCURS 100 TIMES ASCENDING KEY IS WS-CUST-ID, then loads real customer records into the table in the exact order they arrive from an unsorted file, without ever sorting them first. Using this chapter's own real material, explain in your own words what could genuinely go wrong the next time the program runs SEARCH ALL against that table.

📄 View solution

Chapter 6 Quick Reference

  • OCCURS n TIMES — defines a real table with n repeating elements; subscripts are 1-based, never 0
  • Subscript — a plain numeric field (e.g. WS-I) in parentheses after the item name
  • INDEXED BY — a real, genuinely faster displacement-based alternative to a subscript; set/advanced with SET, not ordinary arithmetic
  • SEARCH — real sequential search from the index's current position; requires INDEXED BY; AT END for no match
  • SEARCH ALL — real binary search; requires ASCENDING KEY/DESCENDING KEY declared and data genuinely sorted that way, or results are silently wrong
  • Multi-dimensional tablesOCCURS nested inside OCCURS; multiple subscripts are space-separated in one set of parentheses
  • OCCURS m TO n TIMES DEPENDING ON — a real variable-length table, sized at runtime by another field's current value