1. OCCURS — declarando tabelas

A cláusula OCCURS define quantas vezes um campo se repete. Em vez de declarar TAXA-1, TAXA-2, TAXA-3... você declara uma única entrada com OCCURS:

COBOL Declarando tabelas com OCCURS
       WORKING-STORAGE SECTION.

             * Tabela simples: 12 acumuladores mensais
       01  WS-ACUMULADORES.
           05  WS-TOTAL-MES  PIC 9(13)V99
                             OCCURS 12 TIMES.

             * Tabela de registros: 50 agências com campos
       01  WS-TAB-AGENCIA.
           05  WS-AGENCIA         OCCURS 50 TIMES.
               10  AG-CODIGO      PIC 9(04).
               10  AG-NOME        PIC X(30).
               10  AG-SALDO       PIC S9(13)V99.
               10  AG-ATIVA       PIC X(01).
                   88  AGENCIA-ATIVA  VALUE 'S'.

             * Tabela de 5 taxas mensais
       01  WS-TAB-TAXA.
           05  WS-TAXA       PIC V9(06)
                             OCCURS 5 TIMES.

💗 Para iniciantes — onde OCCURS pode e não pode aparecer

  • OCCURS vai no nível 02 a 49 — nunca no nível 01, 66, 77 ou 88
  • Todos os subordinados de um item com OCCURS se repetem junto com ele
  • O número de ocorrências é fixo em tempo de compilação (exceto com DEPENDING ON, que vemos adiante)

2. Subscripts — acessando elementos

Para acessar um elemento específico da tabela você usa um subscript — um número inteiro entre parênteses que indica qual posição quer acessar. O subscript começa em 1, não em 0:

COBOL Acessando elementos com subscript
       WORKING-STORAGE SECTION.
       01  WS-IDX           PIC 9(02) VALUE 1.

       PROCEDURE DIVISION.
             * Subscript literal: acessa o 3º elemento diretamente
           MOVE 0 TO WS-TOTAL-MES(3)

             * Subscript variável: acessa o elemento indicado por WS-IDX
           ADD WS-VALOR TO WS-TOTAL-MES(WS-IDX)

             * Referência a subcampo de uma tabela de registros
           MOVE 'São Paulo'  TO AG-NOME(1)
           MOVE 0001        TO AG-CODIGO(1)
           MOVE 'S'         TO AG-ATIVA(1)

             * Percorrendo com PERFORM VARYING
           PERFORM VARYING WS-IDX FROM 1 BY 1
                           UNTIL WS-IDX > 12
               MOVE 0 TO WS-TOTAL-MES(WS-IDX)
           END-PERFORM

⚠️ Subscript fora dos limites — S0C5 (protection exception)

Se WS-IDX for 0, negativo ou maior que o número de ocorrências, o programa vai abonar com S0C5 ou S0C7. O COBOL não verifica automaticamente os limites da tabela em tempo de execução (exceto com a opção de compilação SSRANGE). Sempre valide o subscript antes de usá-lo.

3. INDEXED BY — índices nativos

Além dos subscripts (campos numéricos comuns), o COBOL tem o conceito de index — um registrador interno otimizado para percorrer tabelas. O index é declarado com INDEXED BY e manipulado com SET:

COBOL INDEXED BY e SET
       WORKING-STORAGE SECTION.
       01  WS-TAB-AGENCIA.
           05  WS-AGENCIA     OCCURS 50 TIMES
                              INDEXED BY IDX-AG.
               10  AG-CODIGO  PIC 9(04).
               10  AG-NOME    PIC X(30).

       PROCEDURE DIVISION.
             * SET inicializa ou move um valor para o index
           SET IDX-AG TO 1

             * SET UP / DOWN incrementa ou decrementa
           SET IDX-AG UP BY 1
           SET IDX-AG DOWN BY 1

             * Acessando com o index (parênteses igual ao subscript)
           MOVE 'Centro' TO AG-NOME(IDX-AG)

             * Comparação com index usa SET também
           IF IDX-AG > 50
               CONTINUE
           END-IF

             * Para copiar valor do index para campo numérico
           SET WS-IDX TO IDX-AG   *WS-IDX é PIC 9(02)

🦕 Subscript vs Index — qual usar?

Subscript (campo PIC 9) é mais simples de entender e manipular — você pode usar ADD, SUBTRACT, MOVE normalmente. Index (INDEXED BY) é internamente um deslocamento em bytes, não um contador, então é mais eficiente em CPU mas exige SET para mover valores. Use index quando precisar de SEARCH ou SEARCH ALL — esses verbos exigem que a tabela tenha INDEXED BY. Para iterações simples com PERFORM VARYING, subscript é suficiente.

4. Populando tabelas

Existem três formas principais de carregar dados em uma tabela:

4.1 Valores iniciais com VALUE e REDEFINES

COBOL Tabela com valores fixos via REDEFINES
       WORKING-STORAGE SECTION.

             * 1) Declara os dados como uma string com VALUE
       01  WS-MESES-DADO.
           05  VALUE 'JAN'.
           05  VALUE 'FEV'.
           05  VALUE 'MAR'.
           05  VALUE 'ABR'.
           05  VALUE 'MAI'.
           05  VALUE 'JUN'.
           05  VALUE 'JUL'.
           05  VALUE 'AGO'.
           05  VALUE 'SET'.
           05  VALUE 'OUT'.
           05  VALUE 'NOV'.
           05  VALUE 'DEZ'.

             * 2) REDEFINES enxerga o mesmo espaço de memória como tabela
       01  WS-TAB-MESES REDEFINES WS-MESES-DADO.
           05  WS-MES    PIC X(03) OCCURS 12 TIMES
                          INDEXED BY IDX-MES.

✅ Por que REDEFINES?

Em COBOL você não pode ter OCCURS e VALUE no mesmo item (exceto nível 88). O padrão é declarar os valores em uma área com VALUE e sobrepô-la com REDEFINES já com OCCURS. Assim você tem uma tabela pré-carregada sem precisar de código na PROCEDURE DIVISION.

4.2 Carregando de um arquivo

COBOL Populando tabela a partir de arquivo
       WORKING-STORAGE SECTION.
       01  WS-QTD-AGENCIAS   PIC 9(02) VALUE ZEROS.

       PROCEDURE DIVISION.
       1200-CARGA-TABELA.
           MOVE 0   TO   WS-QTD-AGENCIAS
           OPEN INPUT ARQ-AGENCIAS
           PERFORM UNTIL FIM-ARQUIVO-AG
               READ ARQ-AGENCIAS
                   AT END SET FIM-ARQUIVO-AG TO TRUE
                   NOT AT END
                       ADD 1 TO WS-QTD-AGENCIAS
                       MOVE REG-AGENCIA
                         TO WS-AGENCIA(WS-QTD-AGENCIAS)
               END-READ
           END-PERFORM
           CLOSE ARQ-AGENCIAS.

5. Tabelas bidimensionais

COBOL suporta até três dimensões com OCCURS aninhado. O exemplo clássico é uma matriz de valores por mês e por filial:

COBOL Tabela bidimensional — filial x mês
       WORKING-STORAGE SECTION.

             * 10 filiais, 12 meses cada
       01  WS-MATRIZ-SALDO.
           05  WS-FILIAL      OCCURS 10 TIMES
                              INDEXED BY IDX-FIL.
               10  WS-SALDO-MES  PIC S9(13)V99
                              OCCURS 12 TIMES
                              INDEXED BY IDX-MES.

       PROCEDURE DIVISION.
             * Acessa filial 3, mês 7
           ADD WS-VALOR TO WS-SALDO-MES(3, 7)

             * Percorre toda a matriz
           PERFORM VARYING WS-FIL-IDX FROM 1 BY 1
                           UNTIL WS-FIL-IDX > 10
               PERFORM VARYING WS-MES-IDX FROM 1 BY 1
                               UNTIL WS-MES-IDX > 12
                   MOVE 0 TO
                       WS-SALDO-MES(WS-FIL-IDX, WS-MES-IDX)
               END-PERFORM
           END-PERFORM

6. OCCURS DEPENDING ON

Quando o número de ocorrências só é conhecido em tempo de execução, use OCCURS … DEPENDING ON. O campo referenciado em DEPENDING ON controla quantos elementos são considerados válidos:

COBOL Tabela de tamanho variável
       WORKING-STORAGE SECTION.
       01  WS-QTD-ITENS    PIC 9(03) VALUE ZEROS.

       01  WS-TAB-ITENS.
           05  WS-ITEM        OCCURS 1 TO 100 TIMES
                              DEPENDING ON WS-QTD-ITENS
                              INDEXED BY IDX-ITEM.
               10  IT-CODIGO  PIC X(05).
               10  IT-VALOR   PIC 9(09)V99.

       PROCEDURE DIVISION.
             * Lendo da entrada e populando a tabela
           MOVE 0 TO WS-QTD-ITENS
           PERFORM UNTIL FIM-ENTRADA
               READ ARQ-ITENS
                   AT END SET FIM-ENTRADA TO TRUE
                   NOT AT END
                       ADD 1           TO WS-QTD-ITENS
                       MOVE IT-CODIGO-ENT TO
                           IT-CODIGO(WS-QTD-ITENS)
                       MOVE IT-VALOR-ENT  TO
                           IT-VALOR(WS-QTD-ITENS)
               END-READ
           END-PERFORM

🟣 Para quem já programa — ODO e MOVE de grupo

Com OCCURS DEPENDING ON (ODO), cuidado ao fazer MOVE de grupo: o tamanho movido considera o valor atual de WS-QTD-ITENS no momento do MOVE. Se WS-QTD-ITENS = 5, apenas 5 elementos são copiados. Isso pode truncar dados se o destino espera 100 elementos e a origem tem apenas 5. Em operações de MOVE entre estruturas ODO, sempre alinhe os tamanhos primeiro.

O verbo SEARCH percorre uma tabela da posição atual do index até encontrar uma condição (WHEN) ou chegar ao fim (AT END). A tabela precisa ter INDEXED BY:

COBOL SEARCH — busca linear
       WORKING-STORAGE SECTION.
       01  WS-TAB-AGENCIA.
           05  WS-AGENCIA     OCCURS 50 TIMES
                              INDEXED BY IDX-AG.
               10  AG-CODIGO  PIC 9(04).
               10  AG-NOME    PIC X(30).

       01  WS-CHAVE-BUSCA    PIC 9(04).
       01  WS-ACHOU          PIC X(01).
           88  ACHOU          VALUE 'S'.

       PROCEDURE DIVISION.
       2200-BUSCA-AGENCIA.
             * Obrigatório: inicializa o index antes do SEARCH
           SET IDX-AG TO 1
           MOVE 'N' TO WS-ACHOU

           SEARCH WS-AGENCIA
               AT END
                   MOVE 'AGENCIA NAO ENCONTRADA'
                     TO WS-MSG
                   PERFORM 9100-TRATA-NAO-ENCONTRADO
               WHEN AG-CODIGO(IDX-AG) = WS-CHAVE-BUSCA
                   SET ACHOU TO TRUE
                   MOVE AG-NOME(IDX-AG) TO WS-NOME-SAIDA
           END-SEARCH

             * SEARCH com múltiplas condições WHEN
           SET IDX-AG TO 1
           SEARCH WS-AGENCIA
               AT END
                   CONTINUE
               WHEN AG-CODIGO(IDX-AG) = WS-CHAVE-BUSCA
                    AND AGENCIA-ATIVA(IDX-AG)
                   PERFORM 2300-PROCESSA-AGENCIA
               WHEN AG-CODIGO(IDX-AG) > WS-CHAVE-BUSCA
                   CONTINUE  *interrompe a busca cedo se passou da chave
           END-SEARCH

💗 Como o SEARCH funciona por dentro

O SEARCH começa no elemento apontado pelo index (por isso o SET IDX TO 1 é obrigatório antes) e avança automaticamente até que: (1) uma cláusula WHEN seja verdadeira — executa o bloco e para; ou (2) o index ultrapasse o limite da tabela — executa o AT END e para. O index fica apontando para o elemento que satisfez a condição, então você ainda pode acessá-lo depois do END-SEARCH.

8. SEARCH ALL — busca binária

O SEARCH ALL faz uma busca binária — muito mais rápida que o SEARCH linear para tabelas grandes. Para funcionar, a tabela precisa estar ordenada e ter ASCENDING KEY ou DESCENDING KEY declarada:

COBOL SEARCH ALL — busca binária
       WORKING-STORAGE SECTION.

             * A tabela precisa de ASCENDING KEY ou DESCENDING KEY
       01  WS-TAB-PRODUTO.
           05  WS-PRODUTO       OCCURS 200 TIMES
                                ASCENDING KEY IS PROD-CODIGO
                                INDEXED BY IDX-PROD.
               10  PROD-CODIGO  PIC X(08).
               10  PROD-DESCR   PIC X(40).
               10  PROD-PRECO   PIC 9(07)V99.

       01  WS-CHAVE-PROD     PIC X(08).

       PROCEDURE DIVISION.
       2400-BUSCA-PRODUTO.
             * SEARCH ALL não precisa de SET antes — posiciona sozinho
           SEARCH ALL WS-PRODUTO
               AT END
                   MOVE 'PRODUTO INEXISTENTE' TO WS-MSG
               WHEN PROD-CODIGO(IDX-PROD) = WS-CHAVE-PROD
                   MOVE PROD-PRECO(IDX-PROD)
                     TO WS-PRECO-SAIDA
                   MOVE PROD-DESCR(IDX-PROD)
                     TO WS-DESCR-SAIDA
           END-SEARCH
SEARCHSEARCH ALL
AlgoritmoLinear (sequencial)Binário (divide e conquista)
Requer ordenaçãoNãoSim (ASCENDING/DESCENDING KEY)
SET antesSim (SET IDX TO 1)Não (posiciona automaticamente)
Múltiplos WHENSimNão (apenas um WHEN, só com =)
PerformanceO(n) — lento para tabelas grandesO(log n) — muito rápido
Uso típicoTabelas pequenas ou sem ordemTabelas grandes e ordenadas

⚠️ SEARCH ALL só aceita igualdade no WHEN

A cláusula WHEN do SEARCH ALL só pode testar igualdade (=) com a chave declarada em ASCENDING KEY. Você não pode usar >, <, AND com múltiplas condições ou comparar outro campo que não seja a chave. Se precisar de uma condição mais complexa, use SEARCH linear.

9. Exemplo completo

Um programa que carrega uma tabela de taxas de juros por prazo, depois aplica a taxa correta a cada operação de crédito lida de um arquivo:

COBOL Programa completo — aplica taxa por prazo
       IDENTIFICATION DIVISION.
       PROGRAM-ID. APLITAXA.

       ENVIRONMENT DIVISION.
       INPUT-OUTPUT SECTION.
       FILE-CONTROL.
           SELECT ARQ-TAXAS    ASSIGN TO DDTAXAS
               ORGANIZATION IS SEQUENTIAL
               FILE STATUS IS WS-FS-TAX.
           SELECT ARQ-CREDITO  ASSIGN TO DDCRED
               ORGANIZATION IS SEQUENTIAL
               FILE STATUS IS WS-FS-CRED.
           SELECT ARQ-SAIDA    ASSIGN TO DDSAIDA
               ORGANIZATION IS SEQUENTIAL
               FILE STATUS IS WS-FS-SAI.

       DATA DIVISION.
       FILE SECTION.
       FD ARQ-TAXAS.
       01 REG-TAXA.
           05 TX-PRAZO   PIC 9(03).   *prazo em meses
           05 TX-TAXA    PIC V9(06).  *taxa mensal decimal

       FD ARQ-CREDITO.
       01 REG-CREDITO.
           05 CR-CPF     PIC X(11).
           05 CR-VALOR   PIC 9(11)V99.
           05 CR-PRAZO   PIC 9(03).

       FD ARQ-SAIDA.
       01 REG-SAIDA.
           05 SA-CPF     PIC X(11).
           05 SA-VALOR   PIC 9(11)V99.
           05 SA-PRAZO   PIC 9(03).
           05 SA-TAXA    PIC V9(06).
           05 SA-STATUS  PIC X(01).

       WORKING-STORAGE SECTION.
       01 WS-FS-TAX    PIC X(02).
       01 WS-FS-CRED   PIC X(02).
       01 WS-FS-SAI    PIC X(02).
       01 WS-FIM-TAXA  PIC X(01).
           88 FIM-TAXAS  VALUE 'S'.
       01 WS-FIM-CRED  PIC X(01).
           88 FIM-CREDITOS VALUE 'S'.

       01 WS-QTD-TAXAS PIC 9(03) VALUE ZEROS.

             * Tabela de taxas ordenada por prazo — permite SEARCH ALL
       01  WS-TAB-TAXA.
           05  WS-TAXA         OCCURS 1 TO 100 TIMES
                               DEPENDING ON WS-QTD-TAXAS
                               ASCENDING KEY IS TAB-PRAZO
                               INDEXED BY IDX-TX.
               10  TAB-PRAZO   PIC 9(03).
               10  TAB-TAXA    PIC V9(06).

       PROCEDURE DIVISION.
       0000-INICIO.
           PERFORM 1000-INICIALIZA
           PERFORM 1100-CARGA-TAXAS
           PERFORM 2000-PROCESSA
           PERFORM 3000-FINALIZA
           STOP RUN.

       1000-INICIALIZA.
           MOVE 'N' TO WS-FIM-TAXA
           MOVE 'N' TO WS-FIM-CRED
           OPEN INPUT  ARQ-TAXAS
                       ARQ-CREDITO
                OUTPUT ARQ-SAIDA.

       1100-CARGA-TAXAS.
           PERFORM UNTIL FIM-TAXAS
               READ ARQ-TAXAS
                   AT END SET FIM-TAXAS TO TRUE
                   NOT AT END
                       ADD 1 TO WS-QTD-TAXAS
                       MOVE TX-PRAZO
                         TO TAB-PRAZO(WS-QTD-TAXAS)
                       MOVE TX-TAXA
                         TO TAB-TAXA(WS-QTD-TAXAS)
               END-READ
           END-PERFORM.

       2000-PROCESSA.
           READ ARQ-CREDITO
               AT END SET FIM-CREDITOS TO TRUE
           END-READ
           PERFORM UNTIL FIM-CREDITOS
               PERFORM 2100-APLICA-TAXA
               READ ARQ-CREDITO
                   AT END SET FIM-CREDITOS TO TRUE
               END-READ
           END-PERFORM.

       2100-APLICA-TAXA.
           MOVE CR-CPF   TO SA-CPF
           MOVE CR-VALOR TO SA-VALOR
           MOVE CR-PRAZO TO SA-PRAZO

           SEARCH ALL WS-TAXA
               AT END
                   MOVE 'E' TO SA-STATUS  *prazo sem taxa cadastrada
                   MOVE 0   TO SA-TAXA
               WHEN TAB-PRAZO(IDX-TX) = CR-PRAZO
                   MOVE TAB-TAXA(IDX-TX) TO SA-TAXA
                   MOVE 'S'               TO SA-STATUS
           END-SEARCH

           WRITE REG-SAIDA FROM SA-CPF.

       3000-FINALIZA.
           CLOSE ARQ-TAXAS
                 ARQ-CREDITO
                 ARQ-SAIDA.

10. Erros comuns

1. Esquecer o SET antes do SEARCH

COBOL SET antes do SEARCH
             * ERRADO: index ficou na posição da última busca
       SEARCH WS-AGENCIA
           AT END CONTINUE
           WHEN AG-CODIGO(IDX-AG) = WS-CHAVE
               PERFORM 2300-PROCESSA
       END-SEARCH

             * CORRETO: sempre reposiciona antes
       SET IDX-AG TO 1
       SEARCH WS-AGENCIA
           AT END CONTINUE
           WHEN AG-CODIGO(IDX-AG) = WS-CHAVE
               PERFORM 2300-PROCESSA
       END-SEARCH

2. Subscript com valor zero ou negativo — S0C5

COBOL Validando subscript antes de usar
             * ERRADO: WS-MES pode ser 0 se veio do arquivo com espaços
           ADD WS-VALOR TO WS-TOTAL-MES(WS-MES)

             * CORRETO: valida antes de acessar
           IF WS-MES >= 1 AND WS-MES <= 12
               ADD WS-VALOR TO WS-TOTAL-MES(WS-MES)
           ELSE
               PERFORM 9000-MES-INVALIDO
           END-IF

3. SEARCH ALL com tabela não ordenada

COBOL Tabela precisa estar ordenada para SEARCH ALL
             * Se a tabela carregada do arquivo não veio ordenada por prazo,
             * o SEARCH ALL pode retornar "não encontrado" para registros
             * que existem — a busca binária pressupõe ordenação

             * Solução 1: garantir que o arquivo de entrada já vem ordenado
             *   (SORT no JCL antes de chamar o programa)

             * Solução 2: usar SEARCH linear para tabelas não ordenadas
       SET IDX-TX TO 1
       SEARCH WS-TAXA
           AT END CONTINUE
           WHEN TAB-PRAZO(IDX-TX) = CR-PRAZO
               MOVE TAB-TAXA(IDX-TX) TO SA-TAXA
       END-SEARCH

4. INITIALIZE zera subscript/index

COBOL INITIALIZE em grupo zera tudo, incluindo contadores
             * CUIDADO: INITIALIZE em WS-CONTROLES zera WS-IDX-AGENCIA
       01  WS-CONTROLES.
           05  WS-IDX-AGENCIA  PIC 9(02).
           05  WS-TOTAL-GERAL  PIC 9(10).

             * Depois do INITIALIZE, WS-IDX-AGENCIA = 0
             * Tentar acessar AGENCIA(WS-IDX-AGENCIA) → abend S0C5

             * Correto: reatribua o subscript após INITIALIZE
           INITIALIZE WS-CONTROLES
           MOVE 1 TO WS-IDX-AGENCIA

🟣 Para quem já programa — SORT interno de tabelas

O COBOL não tem um verbo nativo para ordenar tabelas em memória. As abordagens comuns em produção são: (1) garantir que o arquivo de entrada já chegue ordenado via SORT STEP no JCL; (2) usar o verbo SORT do COBOL com arquivo de trabalho temporário; (3) em alguns compiladores como Enterprise COBOL, chamar uma sub-rotina de sort via CALL 'SORT'. O SEARCH ALL pressupõe que você garantiu a ordenação — não ordena sozinho.