Searching and Sorting Tables

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against ISO/IEC 1989:2023 (edition 3, January 2023; an upcoming 4th edition is in ballot), where SEARCH and SEARCH ALL are part of the table-handling facility inherited from COBOL 85 and the table form of SORT was added by COBOL 2002; against IBM Enterprise COBOL for z/OS 6.5 (PTFs through September 2026); and against GnuCOBOL 3.2 (28 July 2023, the latest release), with which every program on it was compiled and run on 2026-10-20 with cobc -x -Wall (and again with -std=ibm, which printed nothing different). The programs that are deliberately wrong say so, and the compiler messages shown are the ones cobc (GnuCOBOL) 3.2.0 printed.

COBOL has no built-in dictionary or list type: a lookup table is an OCCURS item in WORKING-STORAGE that the program fills (from a VALUE and REDEFINES, or from a file) and then walks with a statement made for the purpose. This page covers the three statements that do that work: the serial SEARCH, the binary SEARCH ALL, and the SORT statement in its table form, which puts a table into the order that SEARCH ALL needs. The table definitions themselves (OCCURS, INDEXED BY, SET, subscripts versus indexes, OCCURS DEPENDING ON) are explained on Tables and OCCURS; the file-based SORT and MERGE statements are on SORT and MERGE. The examples use the Circulation public-library scenario: the book master BOOKMAST and the member master MEMBMAST are loaded into tables, and the day’s checkouts from LOANTRAN are searched by member and book.

Where the book is outdated: Taulli’s Modern Mainframe Development (2022, chapter 6) introduces OCCURS, INDEXED BY, SEARCH and SEARCH ALL at survey level, and for sorting it shows only a DFSORT job in JCL. It does not show the COBOL SORT statement at all, so neither the file form nor the table form (format 2) that this page documents. Its examples were written for GnuCOBOL through OpenCobolIDE, an unmaintained IDE (see Getting Started).

The two search statements at a glance

SEARCH (format 1) SEARCH ALL (format 2)

Method

Serial: tests the table elements one after another from the current index value

Binary: halves the remaining range at every test

Order of the table

Any; the order is irrelevant

The data must really be in the order that the ASCENDING KEY or DESCENDING KEY clause declares

Starting point

Whatever the search index holds when the statement starts, so the program must SET it

None; the whole table is searched and the index need not be initialized

WHEN conditions

Any number of WHEN phrases, each any condition

Exactly one WHEN phrase; only equality tests on the key items (or a condition-name), joined by AND

Table definition

OCCURS; an INDEXED BY index is the normal choice

OCCURS with INDEXED BY and a KEY IS phrase

Syntax

SEARCH table-name
    [VARYING index-name-or-identifier]
    [AT END imperative-statement]
    WHEN condition imperative-statement-or-NEXT SENTENCE
    [WHEN condition imperative-statement-or-NEXT SENTENCE] ...
[END-SEARCH]

table-name is the item that carries the OCCURS clause; it is written without a subscript and without reference modification. The index it uses is the first (or only) index named in its INDEXED BY phrase, called the search index. The statement works like this:

  1. If the search index is already greater than the highest occurrence, the search ends at once and the AT END phrase runs.

  2. Otherwise the WHEN conditions are evaluated in the order in which they are written. The first one that is true ends the search, its statements run and control passes to the end of the SEARCH. The search index then addresses the element that satisfied the condition.

  3. If no condition is true, the search index moves to the next element and step 2 repeats.

  4. If the end of the table is reached without a hit, the search ends and the AT END phrase runs. Without an AT END phrase, control just passes to the next statement.

The search index is the only index that SEARCH changes (plus the VARYING object, below). That has three consequences that every program must respect:

  • Initialize the index first. The search starts at the index’s current value, so SET table-index TO 1 before each search from the top. Enterprise COBOL requires the index to hold a valid value even if the program never uses it, and in a multidimensional table every superordinate index has to be set as well.

  • The index is left where the search stopped. After a hit it addresses the matching element, which is exactly what the WHEN statements need. After a miss it is past the end of the table, so a following search without a SET finds nothing at all.

  • Only one dimension per statement. To search a two-dimensional table the program executes a SEARCH for the inner table once per outer element (shown later), writing the indexes of all dimensions in the WHEN condition.

For a table defined with OCCURS ... DEPENDING ON, the search stops at the current value of the object of the DEPENDING ON phrase, not at the maximum, so that object must contain the real number of elements before the search (this is checked by the first example). END-SEARCH closes the statement, which is what allows a SEARCH to sit inside an IF or another conditional statement. NEXT SENTENCE in a WHEN phrase transfers control to the statement after the next period, not after END-SEARCH, so in new code use CONTINUE instead. Index data items cannot be used as subscripts, and the table’s ASCENDING KEY or DESCENDING KEY is ignored by the serial search.

The program loads the book master into a table whose size follows BOOK-COUNT, then looks a book up by ISBN. It uses VARYING with a plain numeric item so the entry number can be shown, and ends with a search that has two WHEN phrases.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. SERIAL1.
      * Serial SEARCH over the book table loaded from BOOKMAST.
       ENVIRONMENT DIVISION.
       INPUT-OUTPUT SECTION.
       FILE-CONTROL.
           SELECT BOOKMAST ASSIGN TO "BOOKMAST"
               ORGANIZATION IS LINE SEQUENTIAL.
       DATA DIVISION.
       FILE SECTION.
       FD  BOOKMAST.
           COPY BOOKREC.
       WORKING-STORAGE SECTION.
       01  EOF-FLAG              PIC X VALUE "N".
           88  AT-EOF            VALUE "Y".
       01  BOOK-COUNT            PIC 9(3) VALUE ZERO.
       01  BOOK-TABLE.
           05  BOOK-ENTRY        OCCURS 1 TO 100 TIMES
                                 DEPENDING ON BOOK-COUNT
                                 INDEXED BY BK-IX.
               10  TB-ID         PIC 9(6).
               10  TB-ISBN       PIC X(13).
               10  TB-TITLE      PIC X(30).
               10  TB-COPIES     PIC 9(3).
               10  TB-STATUS     PIC X.
       01  WANTED-ISBN           PIC X(13).
       01  POSITION-NO           PIC 9(3).
       PROCEDURE DIVISION.
       MAIN-PARA.
           PERFORM LOAD-TABLE
           MOVE "9780000000028" TO WANTED-ISBN
           PERFORM FIND-BOOK
           MOVE "9780000000099" TO WANTED-ISBN
           PERFORM FIND-BOOK
           PERFORM FIND-FIRST-WITHDRAWN
           STOP RUN.
       LOAD-TABLE.
           OPEN INPUT BOOKMAST
           PERFORM UNTIL AT-EOF
               READ BOOKMAST
                   AT END SET AT-EOF TO TRUE
                   NOT AT END
                       ADD 1 TO BOOK-COUNT
                       MOVE BOOK-ID     TO TB-ID (BOOK-COUNT)
                       MOVE BOOK-ISBN   TO TB-ISBN (BOOK-COUNT)
                       MOVE BOOK-TITLE  TO TB-TITLE (BOOK-COUNT)
                       MOVE BOOK-COPIES TO TB-COPIES (BOOK-COUNT)
                       MOVE BOOK-STATUS TO TB-STATUS (BOOK-COUNT)
               END-READ
           END-PERFORM
           CLOSE BOOKMAST.
       FIND-BOOK.
           SET BK-IX TO 1
           MOVE 1 TO POSITION-NO
           SEARCH BOOK-ENTRY VARYING POSITION-NO
               AT END
                   DISPLAY WANTED-ISBN " not in the catalogue"
               WHEN TB-ISBN (BK-IX) = WANTED-ISBN
                   DISPLAY WANTED-ISBN " found at entry " POSITION-NO
                       ": " TB-TITLE (BK-IX)
           END-SEARCH.
       FIND-FIRST-WITHDRAWN.
           SET BK-IX TO 1
           SEARCH BOOK-ENTRY
               WHEN TB-STATUS (BK-IX) = "W"
                   DISPLAY "First withdrawn book: " TB-ID (BK-IX)
               WHEN TB-COPIES (BK-IX) = ZERO
                   DISPLAY "First book without copies: " TB-ID (BK-IX)
           END-SEARCH.

It compiled with no warnings and printed:

9780000000028 found at entry 002: COBOL BASICS
9780000000099 not in the catalogue
First withdrawn book: 000004
  • The first search found the ISBN in entry 2. POSITION-NO started at 1 and was increased by 1 every time the search index moved, so it holds the entry number when the search stops.

  • The second ISBN is not in the table, and the AT END phrase ran after the four loaded entries. The table can hold 100 entries, but the search never looked at entries 5 to 100 because BOOK-COUNT is 4.

  • The last search has two WHEN phrases. The first element for which either one is true ends the search, and the phrases are tried in order for each element: book 4 is withdrawn, and no book has zero copies, so the first WHEN fired on entry 4. A search for the first element that satisfies any of several conditions is a single SEARCH with several WHEN phrases, not several searches.

VARYING takes either an index-name or a data item. With an integer data item (as here) the item grows by 1 whenever the search index advances. With an index data item it grows by the same amount as the search index. With an index-name of another table, one SEARCH steps two tables in parallel, and the search is driven by the table’s own index. An integer VARYING item is therefore the simplest way to obtain a plain entry number without a SET.

The index keeps its value

This program shows the most frequent mistake, a second search that starts where the first one stopped, and then a legitimate use of the same behaviour: finding every match by resuming one entry after each hit. It displays the entry number after SET entry-no TO index, because an index-name is not an ordinary numeric item (GnuCOBOL, if asked to DISPLAY an index, prints it as +000000003).

       IDENTIFICATION DIVISION.
       PROGRAM-ID. SERIAL2.
      * The search index keeps its value between SEARCH statements.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  CAT-VALUES.
           05  FILLER PIC X(9) VALUE "000001A02".
           05  FILLER PIC X(9) VALUE "000002A00".
           05  FILLER PIC X(9) VALUE "000003A05".
           05  FILLER PIC X(9) VALUE "000004W01".
           05  FILLER PIC X(9) VALUE "000005A07".
       01  CAT-TABLE REDEFINES CAT-VALUES.
           05  CAT-ENTRY         OCCURS 5 TIMES INDEXED BY CAT-IX.
               10  CAT-ID        PIC 9(6).
               10  CAT-STATUS    PIC X.
               10  CAT-COPIES    PIC 99.
       01  WANTED-ID             PIC 9(6).
       01  ENTRY-NO              PIC 9.
       PROCEDURE DIVISION.
       MAIN-PARA.
           DISPLAY "-- wrong: the second SEARCH is not preceded by SET"
           SET CAT-IX TO 1
           MOVE 3 TO WANTED-ID
           PERFORM FIND-ID
           MOVE 1 TO WANTED-ID
           PERFORM FIND-ID
           DISPLAY "-- right: SET CAT-IX TO 1 before every search"
           MOVE 3 TO WANTED-ID
           SET CAT-IX TO 1
           PERFORM FIND-ID
           MOVE 1 TO WANTED-ID
           SET CAT-IX TO 1
           PERFORM FIND-ID
           DISPLAY "-- every active book with copies, resuming the"
           DISPLAY "   search one entry after each hit"
           SET CAT-IX TO 1
           PERFORM UNTIL CAT-IX > 5
               SEARCH CAT-ENTRY
                   AT END
                       SET CAT-IX TO 6
                   WHEN CAT-STATUS (CAT-IX) = "A"
                    AND CAT-COPIES (CAT-IX) > 0
                       DISPLAY "   lendable: " CAT-ID (CAT-IX)
                       SET CAT-IX UP BY 1
               END-SEARCH
           END-PERFORM
           STOP RUN.
       FIND-ID.
           SEARCH CAT-ENTRY
               AT END
                   DISPLAY WANTED-ID " not found"
               WHEN CAT-ID (CAT-IX) = WANTED-ID
                   SET ENTRY-NO TO CAT-IX
                   DISPLAY WANTED-ID " found, entry " ENTRY-NO
           END-SEARCH.
-- wrong: the second SEARCH is not preceded by SET
000003 found, entry 3
000001 not found
-- right: SET CAT-IX TO 1 before every search
000003 found, entry 3
000001 found, entry 1
-- every active book with copies, resuming the
   search one entry after each hit
   lendable: 000001
   lendable: 000003
   lendable: 000005

Book 1 is in the table, yet the first block reports it as missing: the preceding search for book 3 left the index on entry 3, and book 1 sits before it. Compiled with -Wall, nothing warns about this, because the program is valid; the repair is the SET before each search, as in the second block. The last block resumes after each hit with SET CAT-IX UP BY 1 (the loop ends when the index passes 5, and AT END forces it to 6). Resuming is how COBOL visits all the matches of a non-unique key.

Searching a table of tables

SEARCH works on one dimension at a time. For the stock of each book on each shelf of three branches, the outer index selects the branch and the inner SEARCH goes over the shelves of that branch.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. TWODIM.
      * SEARCH works on one dimension at a time: set the outer index
      * with SET and search the inner table.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  STOCK-VALUES.
           05  FILLER PIC X(15) VALUE "010022000330004".
           05  FILLER PIC X(15) VALUE "050011000215003".
           05  FILLER PIC X(15) VALUE "010012000740009".
       01  STOCK-TABLE REDEFINES STOCK-VALUES.
           05  BRANCH            OCCURS 3 TIMES INDEXED BY BR-IX.
               10  SHELF         OCCURS 3 TIMES
                                 ASCENDING KEY IS SHELF-BOOK
                                 INDEXED BY SH-IX.
                   15  SHELF-BOOK    PIC 99.
                   15  SHELF-COPIES  PIC 999.
       01  WANTED-BOOK           PIC 99.
       01  BRANCH-NO             PIC 9.
       PROCEDURE DIVISION.
       MAIN-PARA.
           MOVE 20 TO WANTED-BOOK
           PERFORM VARYING BR-IX FROM 1 BY 1 UNTIL BR-IX > 3
               SET SH-IX TO 1
               SEARCH SHELF
                   AT END
                       CONTINUE
                   WHEN SHELF-BOOK (BR-IX SH-IX) = WANTED-BOOK
                       SET BRANCH-NO TO BR-IX
                       DISPLAY "Book " WANTED-BOOK " at branch "
                           BRANCH-NO ": " SHELF-COPIES (BR-IX SH-IX)
                           " copies"
               END-SEARCH
           END-PERFORM
           STOP RUN.
Book 20 at branch 1: 003 copies
Book 20 at branch 3: 007 copies

The table named after SEARCH is the inner one (SHELF) with no subscript. Its outer index is set before the statement (here by PERFORM VARYING), the inner one is set to 1, and the WHEN condition names both indexes. Branch 2 holds no book 20, and the search ended there silently because the AT END phrase only has CONTINUE.

Binary search: SEARCH ALL

Syntax and eligibility

SEARCH ALL table-name
    [AT END imperative-statement]
    WHEN key-item = value-or-expression [AND key-item = value-or-expression] ...
         imperative-statement-or-NEXT SENTENCE
[END-SEARCH]

A SEARCH ALL splits the range in two at every step, keeping the half in which the wanted key can lie, so the number of tests grows with the logarithm of the table size: a table of 5,000 members needs at most 13 comparisons, and one of 32,768 entries about 15 (the GnuCOBOL Programmer’s Guide gives the same figure for 32,768), where a serial search tests 2,500 elements on average for 5,000. For a table the program searches many times, the saving is large.

To be eligible, the table’s OCCURS clause must contain both an INDEXED BY phrase and an ASCENDING KEY or DESCENDING KEY phrase. The index used is always the one of the first INDEXED BY name, and it needs no SET. The results are predictable only if these conditions hold, and the compiler checks only the first:

  1. The table has a KEY clause and an index. A violation is a compile-time error.

  2. The data really are in key order. The KEY IS clause declares an order; it does not create it. If the table is not in that order, no message appears at compile time or at run time, and the search simply gives wrong answers.

  3. The key values are unique (the combination of all the keys when there are several). With duplicates, the search can stop on any of the equal elements.

The WHEN condition has strict rules, because the binary search must be able to decide which half to discard:

  • Each test compares a key item (subscripted with the table’s first index) for equality (=, EQUAL TO) with an identifier, a literal or an arithmetic expression, or it is a condition-name with a single value that belongs to a key item. AND joins the tests; OR and the relational operators other than equality are not allowed.

  • The compared value must not itself be a key item or an item subscripted by the search index.

  • When a key is used in the condition, all the keys that precede it in the table’s KEY phrase must be used too.

  • Floating-point key items are not allowed in Enterprise COBOL.

  • Without a hit, control passes to the AT END phrase, or to the next statement when it is missing. The final value of the index is then unpredictable. With a hit, the index addresses the matching element.

A member lookup

The member master is read in key order (its records are in MEMBER-ID sequence on file, which is what makes the table order right), loaded into a table that declares the key, and searched with SEARCH ALL.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. BINARY1.
      * SEARCH ALL over the member table, ascending key MEMBER-ID.
       ENVIRONMENT DIVISION.
       INPUT-OUTPUT SECTION.
       FILE-CONTROL.
           SELECT MEMBMAST ASSIGN TO "MEMBMAST"
               ORGANIZATION IS LINE SEQUENTIAL.
       DATA DIVISION.
       FILE SECTION.
       FD  MEMBMAST.
           COPY MEMBREC.
       WORKING-STORAGE SECTION.
       01  EOF-FLAG              PIC X VALUE "N".
           88  AT-EOF            VALUE "Y".
       01  MEMBER-COUNT          PIC 9(4) VALUE ZERO.
       01  MEMBER-TABLE.
           05  MEMBER-ENTRY      OCCURS 1 TO 5000 TIMES
                                 DEPENDING ON MEMBER-COUNT
                                 ASCENDING KEY IS TM-ID
                                 INDEXED BY MB-IX.
               10  TM-ID         PIC 9(6).
               10  TM-NAME       PIC X(30).
               10  TM-FINES      PIC S9(5)V99.
               10  TM-STATUS     PIC X.
                   88  TM-SUSPENDED VALUE "S".
       01  WANTED-ID             PIC 9(6).
       01  FINE-EDIT             PIC ZZ,ZZ9.99.
       PROCEDURE DIVISION.
       MAIN-PARA.
           PERFORM LOAD-TABLE
           MOVE 101 TO WANTED-ID
           PERFORM FIND-MEMBER
           MOVE 102 TO WANTED-ID
           PERFORM FIND-MEMBER
           MOVE 103 TO WANTED-ID
           PERFORM FIND-MEMBER
           STOP RUN.
       LOAD-TABLE.
           OPEN INPUT MEMBMAST
           PERFORM UNTIL AT-EOF
               READ MEMBMAST
                   AT END SET AT-EOF TO TRUE
                   NOT AT END
                       ADD 1 TO MEMBER-COUNT
                       MOVE MEMBER-ID     TO TM-ID (MEMBER-COUNT)
                       MOVE MEMBER-NAME   TO TM-NAME (MEMBER-COUNT)
                       MOVE MEMBER-FINES  TO TM-FINES (MEMBER-COUNT)
                       MOVE MEMBER-STATUS TO TM-STATUS (MEMBER-COUNT)
               END-READ
           END-PERFORM
           CLOSE MEMBMAST.
       FIND-MEMBER.
           SEARCH ALL MEMBER-ENTRY
               AT END
                   DISPLAY "Member " WANTED-ID " is not registered"
               WHEN TM-ID (MB-IX) = WANTED-ID
                   MOVE TM-FINES (MB-IX) TO FINE-EDIT
                   DISPLAY "Member " WANTED-ID " " TM-NAME (MB-IX)
                       " fines " FINE-EDIT
                   IF TM-SUSPENDED (MB-IX)
                       DISPLAY "   suspended: no new loans"
                   END-IF
           END-SEARCH.
Member 000101 GRACE HOPPER                   fines     12.50
Member 000102 ALAN TURING                    fines     50.00
   suspended: no new loans
Member 000103 is not registered

There is no SET before the search. TM-SUSPENDED is a condition-name of the table’s element, so the IF after the hit tests the matching element through the index that SEARCH ALL left behind. Member 103 is absent, and the AT END phrase reports it.

The order is the programmer’s responsibility

The first block of the output of the sort example in the next section searches a table that declares ASCENDING KEY IS SH-ID but whose entries are in the order 4, 1, 5, 2, 6, 3. The compiler cannot see the actual order of the data, so it raises nothing, and three of the six books that are in the table are reported as missing. The table SORT puts the entries in key order, after which every search finds its entry.

Composite keys and the two orders

A table may have several keys, listed in order of significance, and the WHEN condition then names every one of them joined with AND. The next program loads the checkouts of the day, sorts them by member and then by book, and searches by both keys. It also searches a table declared with DESCENDING KEY, used for fine bands.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. LOANKEY.
      * Table SORT on two keys, SEARCH ALL on both, and a fine band
      * lookup that needs a serial SEARCH on a DESCENDING-ordered table.
       ENVIRONMENT DIVISION.
       INPUT-OUTPUT SECTION.
       FILE-CONTROL.
           SELECT LOANTRAN ASSIGN TO "LOANTRAN"
               ORGANIZATION IS LINE SEQUENTIAL.
       DATA DIVISION.
       FILE SECTION.
       FD  LOANTRAN.
           COPY LOANREC.
       WORKING-STORAGE SECTION.
       01  EOF-FLAG              PIC X VALUE "N".
           88  AT-EOF            VALUE "Y".
       01  OPEN-COUNT            PIC 9(3) VALUE ZERO.
       01  OPEN-LOANS.
           05  OPEN-LOAN         OCCURS 1 TO 200 TIMES
                                 DEPENDING ON OPEN-COUNT
                                 ASCENDING KEY IS OL-MEMBER OL-BOOK
                                 INDEXED BY OL-IX.
               10  OL-MEMBER     PIC 9(6).
               10  OL-BOOK       PIC 9(6).
               10  OL-DATE       PIC 9(8).
       01  WANTED-MEMBER         PIC 9(6).
       01  WANTED-BOOK           PIC 9(6).
       01  DAYS-LATE             PIC 9(3).
       01  FINE-VALUES.
           05  FILLER PIC X(7) VALUE "0300100".
           05  FILLER PIC X(7) VALUE "0140050".
           05  FILLER PIC X(7) VALUE "0070025".
           05  FILLER PIC X(7) VALUE "0010010".
       01  FINE-BANDS REDEFINES FINE-VALUES.
           05  FINE-BAND         OCCURS 4 TIMES
                                 DESCENDING KEY IS FB-FROM
                                 INDEXED BY FB-IX.
               10  FB-FROM       PIC 9(3).
               10  FB-RATE       PIC 99V99.
       01  RATE-EDIT             PIC Z9.99.
       PROCEDURE DIVISION.
       MAIN-PARA.
           PERFORM LOAD-OPEN-LOANS
           SORT OPEN-LOAN ON ASCENDING KEY OL-MEMBER OL-BOOK
           MOVE 101 TO WANTED-MEMBER
           MOVE 2 TO WANTED-BOOK
           PERFORM FIND-LOAN
           MOVE 101 TO WANTED-MEMBER
           MOVE 1 TO WANTED-BOOK
           PERFORM FIND-LOAN
           MOVE 5 TO DAYS-LATE
           PERFORM FIND-RATE
           MOVE 20 TO DAYS-LATE
           PERFORM FIND-RATE
           MOVE ZERO TO DAYS-LATE
           PERFORM FIND-RATE
           STOP RUN.
       LOAD-OPEN-LOANS.
           OPEN INPUT LOANTRAN
           PERFORM UNTIL AT-EOF
               READ LOANTRAN
                   AT END SET AT-EOF TO TRUE
                   NOT AT END
                       IF LOAN-CHECKOUT
                           ADD 1 TO OPEN-COUNT
                           MOVE LOAN-MEMBER-ID TO OL-MEMBER (OPEN-COUNT)
                           MOVE LOAN-BOOK-ID   TO OL-BOOK (OPEN-COUNT)
                           MOVE LOAN-DATE      TO OL-DATE (OPEN-COUNT)
                       END-IF
               END-READ
           END-PERFORM
           CLOSE LOANTRAN.
       FIND-LOAN.
           SEARCH ALL OPEN-LOAN
               AT END
                   DISPLAY "No open loan " WANTED-MEMBER "/" WANTED-BOOK
               WHEN OL-MEMBER (OL-IX) = WANTED-MEMBER
                AND OL-BOOK (OL-IX) = WANTED-BOOK
                   DISPLAY "Open loan " WANTED-MEMBER "/" WANTED-BOOK
                       " since " OL-DATE (OL-IX)
           END-SEARCH.
       FIND-RATE.
           SET FB-IX TO 1
           SEARCH FINE-BAND
               AT END
                   DISPLAY DAYS-LATE " days late: no fine"
               WHEN DAYS-LATE >= FB-FROM (FB-IX)
                   MOVE FB-RATE (FB-IX) TO RATE-EDIT
                   DISPLAY DAYS-LATE " days late: band from "
                       FB-FROM (FB-IX) " rate " RATE-EDIT
           END-SEARCH.
Open loan 000101/000002 since 20261020
No open loan 000101/000001
005 days late: band from 001 rate  0.10
020 days late: band from 014 rate  0.50
000 days late: no fine

The two-key SEARCH ALL finds member 101 with book 2 and reports that member 101 holds no book 1 (member 101 appears only with book 2 in the file). In file order the checkouts of LOANTRAN already happen to be sorted by member, but the table SORT makes the program correct for any input order.

The fine-band lookup shows what SEARCH ALL cannot do. A fine is charged by the band that the days late fall in, so the test is a range (>= the lower bound) and not an equality. A binary search cannot be used, and the table is declared with a DESCENDING KEY only to document the order; the lookup is a serial SEARCH on a table sorted from the highest threshold downwards, so the first band whose threshold is not above the days late is the right one. Five days late is below the 7-day threshold and pays the 1-day band, 20 days pays the 14-day band, and zero days matches no band, so AT END means no fine. A range lookup in a table sorted by threshold is therefore the second legitimate use of the serial search on a sorted table.

What the compiler rejects

The rules above are partly enforced by GnuCOBOL 3.2. The messages below come from one-statement programs, each compiled with cobc -x -Wall, over a table E with ASCENDING KEY IS E-ID and INDEXED BY IX, unless the text says otherwise.

An inequality in the WHEN condition (also OR, which gives the same message):

           SEARCH ALL E
               WHEN E-ID (IX) >= WANTED-ID
                   DISPLAY "hit"
           END-SEARCH
d1.cob: in paragraph 'MAIN-PARA':
d1.cob:13: error: invalid SEARCH ALL condition
   13 >                WHEN E-ID (IX) >= WANTED-ID

A table with no KEY phrase (the table has INDEXED BY only):

d3.cob: in paragraph 'MAIN-PARA':
d3.cob:11: error: SEARCH ALL requires KEY phrase
   11 >            SEARCH ALL E

A table with a KEY phrase but without INDEXED BY:

d4.cob: in paragraph 'MAIN-PARA':
d4.cob:11: error: 'E' not indexed
   11 >            SEARCH ALL E

A WHEN that tests an item that is not a key (E-COPIES (IX) = 3):

d5.cob: in paragraph 'MAIN-PARA':
d5.cob:13: error: SEARCH ALL requires comparision of KEY field
   13 >                WHEN E-COPIES (IX) = 3

The wording is GnuCOBOL’s (the misspelling of "comparision" is the compiler’s own). Enterprise COBOL reports such errors with its own messages, which this page does not reproduce because they could not be run here.

The table form of SORT

Syntax

SORT table-name
    [ON {ASCENDING | DESCENDING} KEY key-item ...] ...
    [WITH DUPLICATES IN ORDER]
    [COLLATING SEQUENCE IS alphabet-name]

This is format 2 of SORT, added by COBOL 2002. It sorts the elements of an OCCURS table in place, in storage, with no sort work files, no SD and no INPUT PROCEDURE/OUTPUT PROCEDURE; it can appear anywhere in the procedure division, even in a program compiled with the THREAD option, where the file form is not supported. It is meant for tables that fit in storage.

  • table-name has an OCCURS clause. It may be qualified, and it may be subscripted, but the rightmost subscript must be omitted or written as ALL; that is how one row of a two-dimensional table is sorted. The number of elements sorted follows the OCCURS rules, so a table with OCCURS DEPENDING ON sorts only its current elements.

  • Each key-item is the table element itself or an item inside it that is not repeated by another OCCURS and is not variably located. The keys are listed from the most to the least significant, and ASCENDING/DESCENDING apply to all the keys that follow until the other word appears (the program above sorts by DESCENDING KEY SH-COPIES and then ASCENDING KEY SH-TITLE as two phrases). Enterprise COBOL allows alphanumeric, numeric, floating-point, national and edited keys; DBCS, pointer and object keys, and dynamic-length items, are not allowed.

  • If the KEY phrases are omitted, the keys declared in the table’s own OCCURS clause are used; if the statement names keys, they override the table’s. The key phrase can be omitted only when the table declares keys.

  • WITH DUPLICATES IN ORDER makes the sort stable: elements with equal keys stay in their original relative order. Without it, their order is undefined in the standard and in Enterprise COBOL.

  • COLLATING SEQUENCE IS alphabet-name chooses the collating sequence for alphabetic, alphanumeric, alphanumeric-edited, external floating-point and numeric-edited keys, using an alphabet from SPECIAL-NAMES. Without it, Enterprise COBOL compares character keys in the EBCDIC sequence unless a PROGRAM COLLATING SEQUENCE is given. On GnuCOBOL the program collating sequence, which is native (ASCII) unless set otherwise, applies, so a table sorted on a PC may come out in a different order than on z/OS when keys mix letters, digits and spaces (see NATIONAL, UTF-8 and Code Pages for the EBCDIC/ASCII difference).

  • The SORT-RETURN special register and the other sort special registers do not apply to the table form.

Sorting a table, then searching it

The program below declares a six-entry shelf table, searches it before sorting (the incorrect case), sorts it with ON ASCENDING KEY SH-ID, searches again, and then sorts twice more on other keys to show the order of keys and the treatment of ties.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. TABSORT.
      * An unsorted table, SEARCH ALL on it, then the table SORT.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  SHELF-VALUES.
           05  FILLER PIC X(20) VALUE "000004OLD LEDGERS 01".
           05  FILLER PIC X(20) VALUE "000001DATA STRUCT 03".
           05  FILLER PIC X(20) VALUE "000005COBOL TABLES03".
           05  FILLER PIC X(20) VALUE "000002COBOL BASICS02".
           05  FILLER PIC X(20) VALUE "000006BATCH JOBS  03".
           05  FILLER PIC X(20) VALUE "000003BATCH PROC  02".
       01  SHELF-TABLE REDEFINES SHELF-VALUES.
           05  SHELF-ENTRY       OCCURS 6 TIMES
                                 ASCENDING KEY IS SH-ID
                                 INDEXED BY SH-IX.
               10  SH-ID         PIC 9(6).
               10  SH-TITLE      PIC X(12).
               10  SH-COPIES     PIC 99.
       01  WANTED-ID             PIC 9(6).
       01  I                     PIC 9.
       PROCEDURE DIVISION.
       MAIN-PARA.
           DISPLAY "-- unsorted table, SEARCH ALL"
           PERFORM VARYING WANTED-ID FROM 1 BY 1 UNTIL WANTED-ID > 6
               PERFORM FIND-ID
           END-PERFORM
           SORT SHELF-ENTRY ON ASCENDING KEY SH-ID
           DISPLAY "-- after SORT ON ASCENDING KEY SH-ID"
           PERFORM VARYING WANTED-ID FROM 1 BY 1 UNTIL WANTED-ID > 6
               PERFORM FIND-ID
           END-PERFORM
           DISPLAY "-- SORT ON DESCENDING KEY SH-COPIES, SH-TITLE"
           SORT SHELF-ENTRY ON DESCENDING KEY SH-COPIES
                            ON ASCENDING KEY SH-TITLE
           PERFORM SHOW-TABLE
           DISPLAY "-- SORT ON ASCENDING KEY SH-COPIES (ties)"
           SORT SHELF-ENTRY ON ASCENDING KEY SH-COPIES
                WITH DUPLICATES IN ORDER
           PERFORM SHOW-TABLE
           STOP RUN.
       FIND-ID.
           SEARCH ALL SHELF-ENTRY
               AT END
                   DISPLAY "   " WANTED-ID " not found"
               WHEN SH-ID (SH-IX) = WANTED-ID
                   DISPLAY "   " WANTED-ID " found: " SH-TITLE (SH-IX)
           END-SEARCH.
       SHOW-TABLE.
           PERFORM VARYING I FROM 1 BY 1 UNTIL I > 6
               DISPLAY "   " SH-ID (I) " " SH-COPIES (I) " "
                   SH-TITLE (I)
           END-PERFORM.
-- unsorted table, SEARCH ALL
   000001 not found
   000002 not found
   000003 not found
   000004 found: OLD LEDGERS
   000005 found: COBOL TABLES
   000006 found: BATCH JOBS
-- after SORT ON ASCENDING KEY SH-ID
   000001 found: DATA STRUCT
   000002 found: COBOL BASICS
   000003 found: BATCH PROC
   000004 found: OLD LEDGERS
   000005 found: COBOL TABLES
   000006 found: BATCH JOBS
-- SORT ON DESCENDING KEY SH-COPIES, SH-TITLE
   000006 03 BATCH JOBS
   000005 03 COBOL TABLES
   000001 03 DATA STRUCT
   000003 02 BATCH PROC
   000002 02 COBOL BASICS
   000004 01 OLD LEDGERS
-- SORT ON ASCENDING KEY SH-COPIES (ties)
   000004 01 OLD LEDGERS
   000003 02 BATCH PROC
   000002 02 COBOL BASICS
   000006 03 BATCH JOBS
   000005 03 COBOL TABLES
   000001 03 DATA STRUCT
  • In the unsorted table the order is 4, 1, 5, 2, 6, 3. The binary search found 4, 5 and 6 and reported 1, 2 and 3 as missing, and no diagnostic was issued, so the KEY IS clause should be backed by a SORT (or by a file that is already in order) whenever the data come from elsewhere.

  • After SORT ... ON ASCENDING KEY SH-ID all six searches succeed.

  • DESCENDING KEY SH-COPIES followed by ASCENDING KEY SH-TITLE puts the books with most copies first and orders equal counts alphabetically by title.

  • The last sort used WITH DUPLICATES IN ORDER on SH-COPIES alone. The books with 2 copies stayed in the order they had before (3 then 2), and so did those with 3 copies (6, 5, 1). The GnuCOBOL guide says the phrase is syntactically recognized but that GnuCOBOL always behaves as if it were given; this run kept ties in their original order, and the program asks for it explicitly so that the result does not depend on the compiler.

A table that declares its keys can be sorted without naming them again. The tiny program below sorts a three-entry table with SORT E, taking the key from the table definition, and then overrides it with ON DESCENDING KEY:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. D9.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  T-VALUES.
           05  FILLER PIC X(2) VALUE "3C".
           05  FILLER PIC X(2) VALUE "1A".
           05  FILLER PIC X(2) VALUE "2B".
       01  T REDEFINES T-VALUES.
           05  E OCCURS 3 TIMES ASCENDING KEY IS E-ID.
               10  E-ID          PIC 9.
               10  E-X           PIC X.
       01  I PIC 9.
       PROCEDURE DIVISION.
       MAIN-PARA.
           SORT E
           PERFORM VARYING I FROM 1 BY 1 UNTIL I > 3
               DISPLAY E-ID (I) E-X (I)
           END-PERFORM
           SORT E ON DESCENDING KEY E-ID
           PERFORM VARYING I FROM 1 BY 1 UNTIL I > 3
               DISPLAY E-ID (I) E-X (I)
           END-PERFORM
           STOP RUN.
1A
2B
3C
3C
2B
1A

The manual of GnuCOBOL says that a table sort without keys "is unsupported and rejected by the compiler". In GnuCOBOL 3.2.0 that is true only if the table also lacks a KEY phrase in its OCCURS clause; with one, SORT E compiles and sorts, as the standard and Enterprise COBOL allow. When neither exists, the message is:

d8.cob: in paragraph 'MAIN-PARA':
d8.cob:11: error: table SORT requires KEY phrase
   11 >            STOP RUN.

Choosing a search: serial, binary or keyed file

flowchart TD A["Need to find a record by key"] --> B{"Does the data fit comfortably in storage?"} B -- "No" --> F["Keyed file: indexed file or VSAM KSDS, or a Db2 table"] B -- "Yes" --> C{"Equality on a unique key, searched many times?"} C -- "Yes" --> D["Sort once with the table SORT, then SEARCH ALL"] C -- "No" --> E{"Range lookup, several conditions or non-unique key?"} E -- "Yes" --> G["Serial SEARCH (sorted table for ranges)"] E -- "No" --> H["Few entries or one search: serial SEARCH is enough"]
Situation Choose Why

A few dozen entries, a code table that is searched a few times

Serial SEARCH

The loop is simple, the table needs no order, and the cost is not measurable.

Thousands of entries, many lookups per run (for example every LOANTRAN record against the member table)

Table SORT once, then SEARCH ALL

At most 13 tests per lookup for 5,000 members. The sort cost is paid once, and it is the only way to guarantee the order that SEARCH ALL assumes.

Conditions that are not plain equality: a fine band, a date range, "first element that is withdrawn or has no copies"

Serial SEARCH with one or several WHEN phrases

A binary search only decides on equality of unique keys. For ranges, order the table so that the first match is the wanted one.

The key is not unique and every match is wanted

Serial SEARCH, resumed after each hit

SEARCH ALL stops on any matching element, and results with duplicates are unpredictable.

A very large master, shared among programs, updated while it is read, or needing a key that survives the run

A keyed file (indexed organization on GnuCOBOL, VSAM KSDS on z/OS) or a Db2 table

The table lives and dies with the program, and its capacity is fixed at compile time. See Indexed Files and VSAM; Db2 is on Embedded SQL and Db2.

The table is the result of a merge with a sorted input (a balance-line update)

Neither: read both files in key order

A sequential match of two sorted files reads each record once, the usual shape of the nightly Circulation update.

Three further rules of thumb follow from the statements above:

  • A table is a copy. A program that loads BOOKMAST into a table and updates the table must write the changes back, and it works with a snapshot if another job changes the file.

  • The capacity of the table is part of the program. An OCCURS ... DEPENDING ON table holds up to the maximum written in the clause; loading more entries than that overwrites storage after the table unless the program checks the count, or is compiled with the subscript checking of the compiler (SSRANGE on Enterprise COBOL, -debug on GnuCOBOL), as described on Tables and OCCURS. COBOL 2014 dynamic-capacity tables (OCCURS DYNAMIC) remove the fixed limit where the compiler implements them; this page’s examples do not rely on them.

  • Sorting a table costs about as much as sorting the same records in a file, so a table is sorted once at load time. Do not sort inside a loop that searches.

Standard, Enterprise COBOL and GnuCOBOL

Feature ISO/IEC 1989:2023 Enterprise COBOL 6.5 GnuCOBOL 3.2

SEARCH, SEARCH ALL, END-SEARCH

Yes (already in COBOL 85)

Yes

Yes

Several WHEN phrases in SEARCH ALL

One WHEN only

One WHEN only

One WHEN only (a second WHEN gives syntax error, unexpected WHEN)

Table SORT (format 2)

Yes (COBOL 2002)

Yes; the release that introduced it was not verified for this page

Yes

SORT with the keys of the table’s OCCURS

Yes

Yes

Yes (the manual’s statement that keys are required is outdated, shown above)

WITH DUPLICATES IN ORDER (table SORT)

Yes

Yes

Accepted; ties were kept in order in this test

COLLATING SEQUENCE on table SORT

Yes

Yes, EBCDIC by default

Yes, native sequence by default

SORT-RETURN after a table SORT

Not applicable

Not applicable

Not tested

Common mistakes

  • No SET before a serial SEARCH: it starts where the last one stopped, as the second example showed.

  • A KEY clause on unsorted data: no diagnostic, wrong answers; SORT the table or load it in order.

  • Duplicate keys under SEARCH ALL: any one of the equal elements may be found; use a serial search resumed after each hit.

  • A range or OR in a SEARCH ALL: rejected with "invalid SEARCH ALL condition"; use a serial SEARCH.

  • SEARCH on a table that was loaded only partly: with OCCURS ... DEPENDING ON, update the object (BOOK-COUNT) while loading, otherwise the search covers more or fewer elements than were loaded.

  • NEXT SENTENCE in a WHEN phrase: it jumps to the next period, not to END-SEARCH; write CONTINUE.

  • A table sorted on a PC and searched on the mainframe (or the reverse) with mixed-case or alphanumeric keys: the ASCII and EBCDIC collating sequences order letters, digits and spaces differently, so a SEARCH ALL can fail on data sorted under the other sequence. Sort on the machine that searches, or name the sequence with COLLATING SEQUENCE.