1. Como funciona o SQL embutido
Um programa COBOL com SQL passa por duas etapas de compilação antes de virar load module executável:
Código fonte (.cbl)
│
▼ DSNHPC (Pré-compilador DB2)
│ ├─ Substitui EXEC SQL...END-EXEC por CALLs internas
│ ├─ Gera DBRM (Database Request Module) → vai para o BIND
│ └─ Emite código COBOL modificado (.cbl limpo)
│
▼ IGYCRCTL (Compilador COBOL)
│ Compila o .cbl limpo → gera objeto .obj
│
▼ IEWL (Linkage Editor)
│ Liga objeto + DB2 stubs → Load Module no PDS de execução
│
▼ DSNTBIND (BIND)
Gera Package ou Plan a partir do DBRM
Otimiza o plano de acesso no catálogo DB2
🦕 Analogia — o tradutor antes do compilador
Imagine que o pré-compilador é um assistente que lê seu código, pega cada bloco EXEC SQL e troca por um CALL para uma rotina DB2. O COBOL resultante não tem mais SQL — só COBOL puro com chamadas. Por isso o compilador COBOL nem sabe que existia SQL: ele compila o código já "traduzido".
Sintaxe de um bloco SQL
EXEC SQL SELECT NOME, SALDO INTO :WS-NOME, :WS-SALDO FROM CONTA WHERE NR_CONTA = :WS-NR-CONTA END-EXEC * Regras: * - EXEC SQL e END-EXEC nas colunas 7–72 * - Variáveis COBOL precedidas de : (dois-pontos) * - Qualquer SQL válido entre os delimitadores * - Comentários COBOL (*) fora do bloco, -- dentro
2. SQLCA — a área de comunicação
O DB2 preenche a estrutura SQLCA (SQL Communication Area) após cada comando SQL. O campo mais importante é o SQLCODE:
| SQLCODE | Significado | Ação típica |
|---|---|---|
0 |
Sucesso — nenhuma ocorrência especial | Continua normalmente |
+100 |
Nenhuma linha encontrada (NOT FOUND) | Tratar como "não existe" |
+1 a +99 |
Warning (aviso) — operação concluída com ressalva | Avaliar SQLERRD/SQLWARN |
negativo |
Erro — operação NÃO concluída | ROLLBACK + abend ou log |
WORKING-STORAGE SECTION. EXEC SQL INCLUDE SQLCA END-EXEC * O pré-compilador expande isso para a estrutura completa: * 01 SQLCA. * 05 SQLCAID PIC X(8). *'SQLCA ' * 05 SQLCABC PIC S9(9) COMP. *tamanho da SQLCA (136) * 05 SQLCODE PIC S9(9) COMP. *código de retorno ← o mais usado * 05 SQLERRM. *mensagem de erro * 49 SQLERRML PIC S9(4) COMP. * 49 SQLERRMC PIC X(70). * 05 SQLERRP PIC X(8). *módulo que detectou o erro * 05 SQLERRD OCCURS 6 TIMES * PIC S9(9) COMP. *SQLERRD(3) = linhas afetadas * 05 SQLWARN. * 49 SQLWARN0 PIC X. *'W' se algum warning ocorreu * 05 SQLSTATE PIC X(5). *código ANSI SQL (ex: '02000')
✅ SQLCODE vs SQLSTATE
SQLCODE é IBM-específico e é o que você verá em 99% do código legado. SQLSTATE é o padrão ANSI SQL e portável entre bancos. Em z/OS COBOL, use SQLCODE — é mais descritivo para o DB2 e é o que os DBAs esperam ver nas mensagens de log.
3. DCLGEN — declarando variáveis host
O DCLGEN (Declaration Generator) é um utilitário DB2 que lê a definição de uma tabela no catálogo e gera automaticamente a estrutura COBOL correspondente. Você não declara variáveis host na mão — usa o DCLGEN e faz COPY.
//GENDCL JOB ...
//STEP01 EXEC PGM=IKJEFT01
//SYSTSPRT DD SYSOUT=*
//SYSTSIN DD *
DSN SYSTEM(DBS1)
DCLGEN TABLE(SCHEMA1.CONTA) -
LIBRARY('HLQ.COPYLIB(DCCONTA)') -
ACTION(REPLACE) -
LANGUAGE(COBOL) -
APOST
END
/*
******************************************************************
* DCLGEN TABLE(SCHEMA1.CONTA)
******************************************************************
EXEC SQL DECLARE SCHEMA1.CONTA TABLE
( NR_CONTA CHAR(10) NOT NULL
, NM_TITULAR VARCHAR(60) NOT NULL
, VL_SALDO DECIMAL(13,2) NOT NULL
, DT_ABERTURA DATE NOT NULL
, CD_STATUS CHAR(1) NOT NULL
, VL_LIMITE DECIMAL(13,2) -- nullable
) END-EXEC.
* COBOL DECLARATION FOR TABLE SCHEMA1.CONTA
01 DCONTA.
10 NR-CONTA PIC X(10).
10 NM-TITULAR.
49 NM-TITULAR-LEN PIC S9(4) COMP.
49 NM-TITULAR-TEXT PIC X(60).
10 VL-SALDO PIC S9(13)V99 COMP-3.
10 DT-ABERTURA PIC X(10).
10 CD-STATUS PIC X(1).
10 VL-LIMITE PIC S9(13)V99 COMP-3.
WORKING-STORAGE SECTION. EXEC SQL INCLUDE SQLCA END-EXEC COPY DCCONTA. *expande a estrutura DCONTA inteira
⚠️ Nunca declare variáveis host na mão
Digitar à mão os tipos de dados de uma tabela DB2 é pedir para ter divergência entre o que o programa espera e o que está no catálogo. Se a coluna VL_SALDO mudar de DECIMAL(13,2) para DECIMAL(15,2), o DCLGEN regenerado atualiza o copybook e todos os programas que usam COPY DCCONTA capturam a mudança na próxima compilação.
4. SELECT INTO, INSERT, UPDATE, DELETE
Para operações que retornam exatamente uma linha (ou nenhuma), usa-se SELECT INTO diretamente. Para múltiplas linhas, usa-se cursor (seção 6).
PROCEDURE DIVISION. 2000-BUSCA-CONTA. EXEC SQL SELECT NR_CONTA , NM_TITULAR , VL_SALDO , CD_STATUS INTO :NR-CONTA , :NM-TITULAR , :VL-SALDO , :CD-STATUS FROM SCHEMA1.CONTA WHERE NR_CONTA = :WS-CONTA-PESQ END-EXEC EVALUATE TRUE WHEN SQLCODE = 0 PERFORM 3000-PROCESSA-CONTA WHEN SQLCODE = +100 PERFORM 9100-CONTA-NAO-ENCONTRADA WHEN OTHER PERFORM 9900-ERRO-DB2 END-EVALUATE.
4000-INSERE-CONTA. EXEC SQL INSERT INTO SCHEMA1.CONTA (NR_CONTA, NM_TITULAR, VL_SALDO, DT_ABERTURA, CD_STATUS) VALUES (:NR-CONTA, :NM-TITULAR, :VL-SALDO, CURRENT DATE, 'A') END-EXEC PERFORM 9000-TRATA-SQLCODE. 5000-ATUALIZA-SALDO. EXEC SQL UPDATE SCHEMA1.CONTA SET VL_SALDO = :VL-SALDO , CD_STATUS = :CD-STATUS WHERE NR_CONTA = :NR-CONTA END-EXEC IF SQLCODE = +100 PERFORM 9100-CONTA-NAO-ENCONTRADA ELSE PERFORM 9000-TRATA-SQLCODE END-IF. 6000-INATIVA-CONTA. EXEC SQL DELETE FROM SCHEMA1.CONTA WHERE NR_CONTA = :NR-CONTA AND CD_STATUS = 'I' END-EXEC * SQLERRD(3) = número de linhas deletadas MOVE SQLERRD(3) TO WS-LINHAS-AFETADAS PERFORM 9000-TRATA-SQLCODE.
5. Tratando o SQLCODE
Todo comando SQL deve ser seguido de verificação do SQLCODE. Um único SELECT que retorna SQL negativo e não é tratado pode corromper dados ou gerar abend U4038.
WORKING-STORAGE SECTION. 01 WS-SQLCODE-DISP S9(09) SIGN LEADING SEPARATE. 01 WS-MSG-ERRO X(80). … 9000-TRATA-SQLCODE. IF SQLCODE = 0 OR SQLCODE = +100 CONTINUE ELSE MOVE SQLCODE TO WS-SQLCODE-DISP STRING 'ERRO DB2 SQLCODE=' DELIMITED SIZE WS-SQLCODE-DISP DELIMITED SIZE ' MSG=' DELIMITED SIZE SQLERRMC DELIMITED SIZE INTO WS-MSG-ERRO END-STRING DISPLAY WS-MSG-ERRO UPON SYSOUT EXEC SQL ROLLBACK END-EXEC MOVE 16 TO RETURN-CODE STOP RUN END-IF.
💗 Códigos DB2 mais frequentes no dia a dia
- -803 — chave duplicada em INSERT (violação de unique index)
- -805 — Package não encontrado no catálogo (precisa de BIND)
- -811 — SELECT INTO retornou mais de uma linha (use cursor!)
- -818 — timestamp mismatch — DBRM e load module de compilações diferentes
- -904 — recurso indisponível (tabela em status de manutenção)
- -911 — deadlock ou timeout — transação sofreu ROLLBACK automático
- -922 — autorização negada (usuário sem GRANT na tabela)
6. Cursores — lendo múltiplas linhas
Quando um SELECT pode retornar mais de uma linha, você precisa de cursor. O ciclo sempre é: DECLARE → OPEN → FETCH em loop → CLOSE.
WORKING-STORAGE SECTION. EXEC SQL INCLUDE SQLCA END-EXEC COPY DCCONTA. 01 WS-FIM-CURSOR PIC X VALUE 'N'. * DECLARE fica na WORKING-STORAGE (fora da PROCEDURE DIVISION) EXEC SQL DECLARE CUR-CONTAS CURSOR FOR SELECT NR_CONTA, NM_TITULAR, VL_SALDO, CD_STATUS FROM SCHEMA1.CONTA WHERE CD_STATUS = 'A' ORDER BY NR_CONTA END-EXEC PROCEDURE DIVISION. 1000-PROCESSA-CONTAS. EXEC SQL OPEN CUR-CONTAS END-EXEC PERFORM 9000-TRATA-SQLCODE PERFORM 1100-FETCH-CONTA PERFORM 1200-LOOP-CONTAS UNTIL WS-FIM-CURSOR = 'S' EXEC SQL CLOSE CUR-CONTAS END-EXEC. 1100-FETCH-CONTA. EXEC SQL FETCH CUR-CONTAS INTO :NR-CONTA, :NM-TITULAR, :VL-SALDO, :CD-STATUS END-EXEC EVALUATE TRUE WHEN SQLCODE = 0 CONTINUE WHEN SQLCODE = +100 MOVE 'S' TO WS-FIM-CURSOR WHEN OTHER PERFORM 9900-ERRO-DB2 END-EVALUATE. 1200-LOOP-CONTAS. PERFORM 2000-PROCESSA-LINHA PERFORM 1100-FETCH-CONTA.
Cursor FOR UPDATE — atualizar enquanto lê
EXEC SQL DECLARE CUR-ATUALIZA CURSOR FOR SELECT NR_CONTA, VL_SALDO FROM SCHEMA1.CONTA WHERE CD_STATUS = 'A' FOR UPDATE OF VL_SALDO -- bloqueia para atualização END-EXEC …depois do FETCH… EXEC SQL UPDATE SCHEMA1.CONTA SET VL_SALDO = :VL-SALDO-NOVO WHERE CURRENT OF CUR-ATUALIZA -- atualiza a linha do cursor END-EXEC PERFORM 9000-TRATA-SQLCODE.
WITH HOLD — cursor que sobrevive ao COMMIT
EXEC SQL DECLARE CUR-LOTE CURSOR WITH HOLD FOR SELECT NR_CONTA FROM SCHEMA1.CONTA WHERE DT_VENCIMENTO < CURRENT DATE END-EXEC * Sem WITH HOLD: COMMIT fecha todos os cursores abertos. * Com WITH HOLD: o cursor permanece aberto após COMMIT, * posicionado na próxima linha a ser lida. * Útil em jobs de lote que fazem COMMIT a cada N registros * para liberar lock sem ter que reabrir o cursor. PERFORM 1100-FETCH-LOTE PERFORM VARYING WS-CTR FROM 1 BY 1 UNTIL WS-FIM-CURSOR = 'S' PERFORM 2000-PROCESSA IF FUNCTION MOD(WS-CTR, 1000) = 0 EXEC SQL COMMIT END-EXEC *commit a cada 1000 END-IF PERFORM 1100-FETCH-LOTE END-PERFORM EXEC SQL COMMIT END-EXEC.
7. Indicadores de nulo
Uma coluna DB2 declarada sem NOT NULL pode conter NULL. Se você faz FETCH de um valor NULL sem indicador, o DB2 retorna SQLCODE -305. O indicador é uma variável S9(4) COMP acompanhada do campo host, prefixada com dois-pontos e separada por espaço:
WORKING-STORAGE SECTION. 01 IND-VL-LIMITE S9(4) COMP. * -1 = valor é NULL * 0 = valor presente, sem truncamento * >0 = valor presente, mas string foi truncada (VARCHAR) PROCEDURE DIVISION. EXEC SQL SELECT VL_LIMITE INTO :VL-LIMITE :IND-VL-LIMITE -- indicador logo depois FROM SCHEMA1.CONTA WHERE NR_CONTA = :NR-CONTA END-EXEC IF SQLCODE = 0 IF IND-VL-LIMITE = -1 MOVE 0 TO VL-LIMITE *trata NULL como zero ELSE CONTINUE *VL-LIMITE tem valor real END-IF END-IF * INSERT com NULL explícito via indicador: MOVE -1 TO IND-VL-LIMITE EXEC SQL INSERT INTO SCHEMA1.CONTA (NR_CONTA, VL_LIMITE) VALUES (:NR-CONTA, :VL-LIMITE :IND-VL-LIMITE) END-EXEC * Como IND-VL-LIMITE = -1, o DB2 grava NULL em VL_LIMITE
8. WHENEVER — tratamento automático
WHENEVER instrui o pré-compilador a inserir automaticamente um GO TO após cada comando SQL para tratar condições específicas. É uma alternativa ao IF SQLCODE … manual depois de cada comando:
WORKING-STORAGE SECTION. EXEC SQL INCLUDE SQLCA END-EXEC PROCEDURE DIVISION. 0000-INICIO. * A partir daqui, qualquer SQLCODE negativo desvia para 9900 EXEC SQL WHENEVER SQLERROR GO TO :9900-ERRO-DB2 END-EXEC * A partir daqui, SQLCODE = +100 desvia para 8000 EXEC SQL WHENEVER NOT FOUND GO TO :8000-FIM-CURSOR END-EXEC EXEC SQL OPEN CUR-CONTAS END-EXEC PERFORM 1100-FETCH PERFORM 1200-LOOP UNTIL WS-FIM = 'S' EXEC SQL CLOSE CUR-CONTAS END-EXEC STOP RUN. 8000-FIM-CURSOR. MOVE 'S' TO WS-FIM. 9900-ERRO-DB2. DISPLAY 'SQLCODE=' SQLCODE EXEC SQL ROLLBACK END-EXEC STOP RUN.
⚠️ WHENEVER usa GO TO — cuidado com escopo
WHENEVER é textual: ele injeta GO TO depois de cada comando SQL que aparecer após a declaração WHENEVER. Isso inclui OPEN, CLOSE e o próprio FETCH dentro do loop. O GO TO gerado pode pular fora de um PERFORM, causando comportamento inesperado. A maioria dos shops modernos prefere o padrão explícito de checar SQLCODE com EVALUATE após cada comando.
9. Exemplo completo
O programa EXTRATO lê todas as movimentações de uma conta num período, acumula o saldo e grava um arquivo de extrato. Demonstra cursor WITH HOLD, indicador de nulo, commit parcial e tratamento de SQLCODE:
*----------------------------------------------------------------
* EXTRATO — gera extrato de movimentações por conta/período
*----------------------------------------------------------------
IDENTIFICATION DIVISION.
PROGRAM-ID. EXTRATO.
DATA DIVISION.
WORKING-STORAGE SECTION.
EXEC SQL INCLUDE SQLCA END-EXEC
COPY DCMOVTO. *DCLGEN da tabela MOVIMENTACAO
01 WS-PARMS.
05 WS-CONTA-INI PIC X(10).
05 WS-DT-INI PIC X(10). *YYYY-MM-DD
05 WS-DT-FIM PIC X(10).
01 WS-TOTAL-DEB S9(13)V99 COMP-3.
01 WS-TOTAL-CRE S9(13)V99 COMP-3.
01 WS-CTR S9(09) COMP.
01 WS-FIM PIC X VALUE 'N'.
01 IND-DESCRICAO S9(4) COMP. *indicador de nulo p/ descrição
EXEC SQL
DECLARE CUR-MOVTO CURSOR WITH HOLD FOR
SELECT NR_CONTA, DT_MOVTO, TP_MOVTO,
VL_MOVTO, DS_HISTORICO
FROM SCHEMA1.MOVIMENTACAO
WHERE NR_CONTA = :WS-CONTA-INI
AND DT_MOVTO BETWEEN :WS-DT-INI AND :WS-DT-FIM
ORDER BY DT_MOVTO, HR_MOVTO
END-EXEC
PROCEDURE DIVISION.
0000-INICIO.
INITIALIZE WS-TOTAL-DEB WS-TOTAL-CRE WS-CTR
MOVE '1234567890' TO WS-CONTA-INI
MOVE '2026-01-01' TO WS-DT-INI
MOVE '2026-06-30' TO WS-DT-FIM
EXEC SQL OPEN CUR-MOVTO END-EXEC
PERFORM 9000-TRATA-SQLCODE
PERFORM 1000-FETCH-MOVTO
PERFORM 1100-LOOP-MOVTO
UNTIL WS-FIM = 'S'
EXEC SQL CLOSE CUR-MOVTO END-EXEC
EXEC SQL COMMIT END-EXEC
DISPLAY 'TOTAL DEBITOS : ' WS-TOTAL-DEB
DISPLAY 'TOTAL CREDITOS: ' WS-TOTAL-CRE
DISPLAY 'REGISTROS : ' WS-CTR
STOP RUN.
1000-FETCH-MOVTO.
EXEC SQL
FETCH CUR-MOVTO
INTO :NR-CONTA, :DT-MOVTO, :TP-MOVTO,
:VL-MOVTO, :DS-HISTORICO :IND-DESCRICAO
END-EXEC
EVALUATE TRUE
WHEN SQLCODE = 0 CONTINUE
WHEN SQLCODE = +100 MOVE 'S' TO WS-FIM
WHEN OTHER PERFORM 9900-ERRO-DB2
END-EVALUATE.
1100-LOOP-MOVTO.
ADD 1 TO WS-CTR
IF TP-MOVTO = 'D'
ADD VL-MOVTO TO WS-TOTAL-DEB
ELSE
ADD VL-MOVTO TO WS-TOTAL-CRE
END-IF
IF IND-DESCRICAO = -1
MOVE 'SEM HISTORICO' TO DS-HISTORICO-TEXT
END-IF
PERFORM 2000-GRAVA-EXTRATO
IF FUNCTION MOD(WS-CTR, 500) = 0
EXEC SQL COMMIT END-EXEC
PERFORM 9000-TRATA-SQLCODE
END-IF
PERFORM 1000-FETCH-MOVTO.
9000-TRATA-SQLCODE.
IF SQLCODE NOT = 0 AND NOT = +100
DISPLAY 'SQLCODE=' SQLCODE ' ' SQLERRMC
EXEC SQL ROLLBACK END-EXEC
STOP RUN
END-IF.
9900-ERRO-DB2.
DISPLAY 'ERRO FATAL SQLCODE=' SQLCODE
EXEC SQL ROLLBACK END-EXEC
STOP RUN.
10. Erros comuns
1. SELECT INTO sem checar SQLCODE +100
EXEC SQL SELECT VL_SALDO INTO :VL-SALDO FROM SCHEMA1.CONTA WHERE NR_CONTA = :NR-CONTA END-EXEC * ❌ Se SQLCODE = +100, VL-SALDO ainda tem o valor anterior! COMPUTE WS-NOVO-SALDO = VL-SALDO + WS-CREDITO * ✅ Cheque SQLCODE ANTES de usar os campos host.
2. SELECT INTO com resultado de múltiplas linhas — SQLCODE -811
EXEC SQL SELECT VL_SALDO INTO :VL-SALDO FROM SCHEMA1.CONTA WHERE CD_STATUS = 'A' -- pode retornar milhares! END-EXEC * SQLCODE -811: "resultado do SELECT INTO é mais de uma linha" * ✅ Se WHERE pode retornar múltiplas linhas → use cursor.
3. Esquecer o indicador de nulo — SQLCODE -305
EXEC SQL SELECT VL_LIMITE INTO :VL-LIMITE -- VL_LIMITE é nullable FROM SCHEMA1.CONTA WHERE NR_CONTA = :NR-CONTA END-EXEC * SQLCODE -305 se VL_LIMITE contiver NULL no banco * ✅ Sempre use indicador: :VL-LIMITE :IND-VL-LIMITE
4. COMMIT dentro de cursor sem WITH HOLD
EXEC SQL OPEN CUR-PROC END-EXEC PERFORM UNTIL WS-FIM = 'S' EXEC SQL FETCH CUR-PROC INTO :NR-CONTA END-EXEC PERFORM PROCESSA EXEC SQL COMMIT END-EXEC *❌ fecha CUR-PROC! *próximo FETCH → SQLCODE -501 (cursor fechado) END-PERFORM * ✅ Declare o cursor WITH HOLD para manter aberto após COMMIT.
5. SQLCODE -818 — timestamp mismatch
SQLCODE -818: o timestamp do DBRM não corresponde ao load module.
Causa: o programa foi pré-compilado e o DBRM resultante foi vinculado
(BIND), mas depois o fonte foi recompilado sem novo BIND —
ou vice-versa.
Solução: refazer o build completo:
1. Pré-compilação (gera novo DBRM)
2. Compilação COBOL
3. Link-edit
4. BIND do novo DBRM
Todos os quatro passos devem usar o mesmo código fonte.