SQL Embebido en RPG

SQLRPGLE: como combinar la potencia de SQL con la logica de RPG. Host variables, cursores, SQL dinamico, manejo de errores con SQLCODE/SQLSTATE, commitment control y patrones de alto rendimiento.

Que es SQLRPGLE

SQLRPGLE es RPG con sentencias SQL embebidas directamente en el codigo fuente. El precompilador SQL convierte las sentencias SQL en llamadas a las APIs de Db2 antes de compilar el RPG. El resultado es un programa que combina la logica procedural de RPG con la potencia de consultas SQL, similar a como JDBC o ODBC integran SQL en Java o C.

RPG nativo (sin SQL)

  • Acceso a datos con CHAIN, READ, WRITE, UPDATE
  • Un registro a la vez (record-level access)
  • Requiere archivos logicos para filtrar/ordenar
  • Muy eficiente para acceso por clave individual
  • Archivos declarados con DCL-F
  • Sin capacidad de JOIN nativa en RPG

SQLRPGLE (SQL embebido)

  • Acceso con SELECT, INSERT, UPDATE, DELETE
  • Conjuntos de datos (set-based operations)
  • Filtros y ordenamiento con WHERE/ORDER BY
  • Eficiente para operaciones masivas y JOINs
  • No requiere DCL-F para tablas SQL
  • JOIN, GROUP BY, subqueries directamente
SQLRPGLE — Estructura basica
**free
ctl-opt dftactgrp(*no) actgrp(*caller);
ctl-opt option(*srcstmt : *nodebugio);

// No necesitas DCL-F para tablas accedidas via SQL

dcl-s nombre   varchar(100);
dcl-s saldo    packed(11:2);
dcl-s clienteId packed(7:0);

// Sentencia SQL embebida: prefijo EXEC SQL
exec sql
  SELECT CLINOM, CLISALDO
  INTO :nombre, :saldo
  FROM MILIB.CLIENTES
  WHERE CLIID = :clienteId;

if sqlcode = 0;
  // Registro encontrado, procesar
endif;

*inlr = *on;
return;
CL — Compilacion de SQLRPGLE con CRTSQLRPGI
/* Compilar SQLRPGLE como programa *PGM */
CRTSQLRPGI OBJ(MILIB/MIPGM)
           SRCFILE(MILIB/QRPGLESRC)
           SRCMBR(MIPGM)
           COMMIT(*NONE)
           DBGVIEW(*SOURCE)
           OPTION(*SRCSTMT *NODEBUGIO)
           CLOSQLCSR(*ENDMOD)
           DATFMT(*ISO)
           DFTACTGRP(*NO)
           ACTGRP(*CALLER)
           OBJTYPE(*PGM)

/* Compilar como modulo *MODULE para ILE */
CRTSQLRPGI OBJ(MILIB/MIPGM)
           SRCFILE(MILIB/QRPGLESRC)
           OBJTYPE(*MODULE)
           DBGVIEW(*SOURCE)
           COMMIT(*NONE)

/* Compilar desde IFS (stream file) */
CRTSQLRPGI OBJ(MILIB/MIPGM)
           SRCSTMF('/home/dev/src/mipgm.sqlrpgle')
           OBJTYPE(*PGM)
           DBGVIEW(*SOURCE)
           COMMIT(*NONE)
Flujo de compilacion: El comando CRTSQLRPGI primero ejecuta el precompilador SQL, que convierte las sentencias EXEC SQL en llamadas a APIs internas de Db2. Luego compila el RPG resultante. Si hay errores SQL, apareceran en el spool CRTSQLRPGI con el prefijo SQL-.

Host variables

Las host variables son variables RPG usadas dentro de sentencias SQL. Se identifican con el prefijo : (dos puntos) que indica al precompilador que es una variable del programa host (RPG), no una columna SQL. Es el mismo concepto que los parametros ? en prepared statements de JDBC.

SQLRPGLE — Host variables y notacion :colon
**free

// Variables RPG que se usan como host variables en SQL
dcl-s wkClienteId packed(7:0);
dcl-s wkNombre    varchar(100);
dcl-s wkEmail     varchar(200);
dcl-s wkSaldo     packed(11:2);
dcl-s wkFecha     date(*iso);

// Data structures como host variables
dcl-ds clienteDS qualified;
  id       packed(7:0);
  nombre   varchar(100);
  email    varchar(200);
  saldo    packed(11:2);
  fechaAlta date(*iso);
end-ds;

// Uso en SELECT INTO (una fila)
wkClienteId = 1001;
exec sql
  SELECT CLINOM, CLIEMAIL, CLISALDO
  INTO :wkNombre, :wkEmail, :wkSaldo
  FROM MILIB.CLIENTES
  WHERE CLIID = :wkClienteId;

// Uso con data structure (una fila completa)
exec sql
  SELECT CLIID, CLINOM, CLIEMAIL, CLISALDO, CLIFECALTA
  INTO :clienteDS
  FROM MILIB.CLIENTES
  WHERE CLIID = :wkClienteId;

// Ahora: clienteDS.nombre tiene el valor
dsply clienteDS.nombre;

// Host variables en INSERT
exec sql
  INSERT INTO MILIB.CLIENTES
    (CLIID, CLINOM, CLIEMAIL, CLISALDO)
  VALUES
    (:wkClienteId, :wkNombre, :wkEmail, :wkSaldo);

// Host variables en UPDATE
exec sql
  UPDATE MILIB.CLIENTES
  SET CLISALDO = :wkSaldo,
      CLIEMAIL = :wkEmail
  WHERE CLIID = :wkClienteId;

// Host variables en DELETE
exec sql
  DELETE FROM MILIB.CLIENTES
  WHERE CLIID = :wkClienteId;
Regla de oro: Siempre usa host variables para pasar valores a SQL. Nunca concatenes valores directamente en el string SQL. Ademas de prevenir SQL injection, las host variables permiten que Db2 reutilice el plan de acceso (query plan caching), mejorando dramaticamente el rendimiento.

SELECT INTO

SELECT INTO se usa cuando esperas exactamente una fila como resultado. Si la consulta devuelve cero filas, SQLCODE sera 100 (not found). Si devuelve mas de una fila, SQLCODE sera -811 (result set has more than one row).

SQLRPGLE — SELECT INTO: una fila
dcl-s wkNombre   varchar(100);
dcl-s wkSaldo    packed(11:2);
dcl-s wkTotal    packed(13:2);
dcl-s wkConteo   int(10);
dcl-s wkClienteId packed(7:0) inz(1001);

// Lectura simple por clave
exec sql
  SELECT CLINOM, CLISALDO
  INTO :wkNombre, :wkSaldo
  FROM MILIB.CLIENTES
  WHERE CLIID = :wkClienteId;

// Verificar resultado
select;
  when sqlcode = 0;
    // OK: encontrado
    dsply ('Cliente: ' + %trim(wkNombre));
  when sqlcode = 100;
    // No encontrado
    dsply 'Cliente no existe';
  other;
    // Error SQL
    dsply ('Error SQL: ' + %char(sqlcode));
endsl;

// Funciones agregadas (siempre retornan una fila)
exec sql
  SELECT COUNT(*), SUM(CLISALDO)
  INTO :wkConteo, :wkTotal
  FROM MILIB.CLIENTES
  WHERE CLIACTIVO = 'A';

dsply ('Clientes activos: ' + %char(wkConteo) +
       ' Total: $' + %char(wkTotal));

// SELECT INTO con JOIN
dcl-s wkFactura  packed(10:0) inz(50001);
dcl-s wkCliNom   varchar(100);
dcl-s wkMonto    packed(11:2);
dcl-s wkFecha    date(*iso);

exec sql
  SELECT c.CLINOM, f.FACMONTO, f.FACFECHA
  INTO :wkCliNom, :wkMonto, :wkFecha
  FROM MILIB.FACTURAS f
  JOIN MILIB.CLIENTES c ON c.CLIID = f.FACCLIID
  WHERE f.FACID = :wkFactura;

Cursores

Los cursores se usan cuando una consulta puede devolver multiples filas. Funcionan como un iterador: se declara, se abre, se leen filas una por una con FETCH, y se cierra. Es el patron equivalente a un ResultSet en JDBC o un cursor de base de datos en cualquier lenguaje.

SQLRPGLE — Ciclo DECLARE / OPEN / FETCH / CLOSE
dcl-s wkId      packed(7:0);
dcl-s wkNombre  varchar(100);
dcl-s wkSaldo   packed(11:2);
dcl-s wkContador int(10) inz(0);

// Paso 1: DECLARE cursor (solo define, no ejecuta)
exec sql
  DECLARE curClientes CURSOR FOR
    SELECT CLIID, CLINOM, CLISALDO
    FROM MILIB.CLIENTES
    WHERE CLIACTIVO = 'A'
    ORDER BY CLISALDO DESC;

// Paso 2: OPEN cursor (ejecuta la query)
exec sql OPEN curClientes;

if sqlcode <> 0;
  dsply 'Error abriendo cursor';
  return;
endif;

// Paso 3: FETCH en loop
dow sqlcode = 0;
  exec sql
    FETCH curClientes
    INTO :wkId, :wkNombre, :wkSaldo;

  if sqlcode = 0;
    wkContador += 1;
    // Procesar cada fila
    dsply (%char(wkId) + ' ' + %trim(wkNombre) +
           ' $' + %char(wkSaldo));
  endif;
enddo;

// Paso 4: CLOSE cursor
exec sql CLOSE curClientes;

dsply ('Total procesados: ' + %char(wkContador));
SQLRPGLE — Cursor con data structure
dcl-ds clienteDS qualified;
  id       packed(7:0);
  nombre   varchar(100);
  email    varchar(200);
  saldo    packed(11:2);
  fechaAlta date(*iso);
end-ds;

dcl-s wkMinSaldo packed(11:2) inz(1000);

exec sql
  DECLARE curTopClientes CURSOR FOR
    SELECT CLIID, CLINOM, CLIEMAIL, CLISALDO, CLIFECALTA
    FROM MILIB.CLIENTES
    WHERE CLISALDO >= :wkMinSaldo
    ORDER BY CLISALDO DESC
    FETCH FIRST 100 ROWS ONLY;

exec sql OPEN curTopClientes;

dow sqlcode = 0;
  exec sql FETCH curTopClientes INTO :clienteDS;

  if sqlcode = 0;
    // Acceso con notacion punto
    if clienteDS.saldo > 50000;
      exec sql
        UPDATE MILIB.CLIENTES
        SET CLICATEG = 'PREMIUM'
        WHERE CLIID = :clienteDS.id;
    endif;
  endif;
enddo;

exec sql CLOSE curTopClientes;
SQLRPGLE — Cursor FOR UPDATE (actualizar fila actual)
// Cursor FOR UPDATE: permite UPDATE WHERE CURRENT OF
exec sql
  DECLARE curActualizar SCROLL CURSOR FOR
    SELECT CLIID, CLINOM, CLISALDO
    FROM MILIB.CLIENTES
    WHERE CLIACTIVO = 'A' AND CLISALDO < 0
    FOR UPDATE OF CLISALDO, CLICATEG;

exec sql OPEN curActualizar;

dow sqlcode = 0;
  exec sql
    FETCH NEXT FROM curActualizar
    INTO :wkId, :wkNombre, :wkSaldo;

  if sqlcode = 0;
    // Actualizar la fila actual del cursor directamente
    exec sql
      UPDATE MILIB.CLIENTES
      SET CLICATEG = 'MOROSO',
          CLISALDO = 0
      WHERE CURRENT OF curActualizar;
  endif;
enddo;

exec sql CLOSE curActualizar;
INSENSITIVE

Snapshot: no ve cambios posteriores a OPEN

SENSITIVE

Dinamico: ve cambios en la tabla base

SCROLL

Permite navegar hacia adelante y atras (FETCH PRIOR)

FOR UPDATE

Permite UPDATE/DELETE WHERE CURRENT OF cursor

WITH HOLD

Cursor sobrevive un COMMIT (no se cierra)

FOR READ ONLY

Solo lectura, permite mejor optimizacion

SQL dinamico

SQL dinamico permite construir sentencias SQL en tiempo de ejecucion. Se usa cuando la estructura de la consulta (columnas, tablas, condiciones) no se conoce en tiempo de compilacion. Es equivalente a usar prepared statements en JDBC con PreparedStatement.execute().

SQLRPGLE — EXECUTE IMMEDIATE (sin resultados)
dcl-s sqlStmt varchar(1000);
dcl-s wkTabla varchar(128);
dcl-s wkBiblio varchar(10);

wkBiblio = 'MILIB';
wkTabla = 'TEMP_PROCESO';

// Construir y ejecutar SQL dinamicamente
sqlStmt = 'DELETE FROM ' + %trim(wkBiblio) + '.' +
          %trim(wkTabla) +
          ' WHERE FECHA < CURRENT_DATE - 30 DAYS';

exec sql EXECUTE IMMEDIATE :sqlStmt;

if sqlcode = 0;
  dsply 'Purga completada';
elseif sqlcode = 100;
  dsply 'Sin registros para borrar';
else;
  dsply ('Error: ' + %char(sqlcode));
endif;
SQLRPGLE — PREPARE y EXECUTE (con parametros)
dcl-s sqlStmt   varchar(1000);
dcl-s wkNombre  varchar(100);
dcl-s wkEmail   varchar(200);
dcl-s wkId      packed(7:0);

// PREPARE: compila el SQL una vez (con ? como placeholders)
sqlStmt = 'UPDATE MILIB.CLIENTES ' +
          'SET CLINOM = ?, CLIEMAIL = ? ' +
          'WHERE CLIID = ?';

exec sql PREPARE updCliente FROM :sqlStmt;

// EXECUTE: ejecuta multiples veces con distintos valores
wkId = 1001;
wkNombre = 'Fernando Secchi';
wkEmail = 'fernando@ejemplo.com';
exec sql EXECUTE updCliente USING :wkNombre, :wkEmail, :wkId;

wkId = 1002;
wkNombre = 'Maria Lopez';
wkEmail = 'maria@ejemplo.com';
exec sql EXECUTE updCliente USING :wkNombre, :wkEmail, :wkId;

// PREPARE + cursor para SELECT dinamico
dcl-s wkResultNom  varchar(100);
dcl-s wkResultSaldo packed(11:2);

sqlStmt = 'SELECT CLINOM, CLISALDO FROM MILIB.CLIENTES ' +
          'WHERE CLICATEG = ? ORDER BY CLISALDO DESC';

exec sql PREPARE selCateg FROM :sqlStmt;
exec sql DECLARE curDinamico CURSOR FOR selCateg;

dcl-s wkCategoria char(10) inz('PREMIUM');
exec sql OPEN curDinamico USING :wkCategoria;

dow sqlcode = 0;
  exec sql FETCH curDinamico INTO :wkResultNom, :wkResultSaldo;
  if sqlcode = 0;
    dsply (%trim(wkResultNom) + ': $' + %char(wkResultSaldo));
  endif;
enddo;

exec sql CLOSE curDinamico;
PREPARE vs EXECUTE IMMEDIATE: Usa PREPARE + EXECUTE cuando ejecutas la misma sentencia multiples veces con distintos parametros (Db2 reutiliza el plan de acceso). Usa EXECUTE IMMEDIATE para sentencias unicas que no se repiten. Es exactamente la misma logica que PreparedStatement vs Statement en JDBC.

Indicator variables

Las indicator variables (indicadores null) son variables INT(5) que acompanan a una host variable para detectar valores NULL. Si el indicador es -1, el valor SQL es NULL. Si es 0, el valor es valido. Son esenciales cuando las columnas permiten NULL.

SQLRPGLE — Indicator variables para NULLs
dcl-s wkNombre    varchar(100);
dcl-s wkEmail     varchar(200);
dcl-s wkTelefono  varchar(20);
dcl-s wkClienteId packed(7:0) inz(1001);

// Indicadores: INT(5), uno por cada columna nullable
dcl-s indEmail    int(5);
dcl-s indTelefono int(5);

exec sql
  SELECT CLINOM, CLIEMAIL, CLITELEFONO
  INTO :wkNombre, :wkEmail :indEmail,
       :wkTelefono :indTelefono
  FROM MILIB.CLIENTES
  WHERE CLIID = :wkClienteId;

if sqlcode = 0;
  dsply ('Nombre: ' + %trim(wkNombre));

  if indEmail = 0;
    dsply ('Email: ' + %trim(wkEmail));
  else;
    dsply 'Email: (sin dato)';   // NULL
  endif;

  if indTelefono = 0;
    dsply ('Tel: ' + %trim(wkTelefono));
  else;
    dsply 'Tel: (sin dato)';     // NULL
  endif;
endif;

// Insertar con NULL usando indicador
wkNombre = 'Nuevo Cliente';
wkEmail = '';
indEmail = -1;  // Indicar que es NULL
wkTelefono = '11-4567-8900';
indTelefono = 0;  // Indicar que tiene valor

exec sql
  INSERT INTO MILIB.CLIENTES
    (CLINOM, CLIEMAIL, CLITELEFONO)
  VALUES
    (:wkNombre, :wkEmail :indEmail,
     :wkTelefono :indTelefono);
SQLRPGLE — Array de indicadores con DS
dcl-ds clienteDS qualified;
  id        packed(7:0);
  nombre    varchar(100);
  email     varchar(200);
  telefono  varchar(20);
  saldo     packed(11:2);
end-ds;

// Array de indicadores (uno por campo de la DS)
dcl-s indicadores int(5) dim(5);

exec sql
  SELECT CLIID, CLINOM, CLIEMAIL, CLITELEFONO, CLISALDO
  INTO :clienteDS :indicadores
  FROM MILIB.CLIENTES
  WHERE CLIID = 1001;

// indicadores(1) -> CLIID null?
// indicadores(2) -> CLINOM null?
// indicadores(3) -> CLIEMAIL null?
// indicadores(4) -> CLITELEFONO null?
// indicadores(5) -> CLISALDO null?

if indicadores(3) = -1;
  dsply 'Email es NULL';
endif;

Commitment control

El commitment control en Db2 for i define como se manejan las transacciones. Determina cuando los cambios se hacen permanentes y que nivel de aislamiento tienen las lecturas. Es equivalente a los niveles de aislamiento de transacciones en cualquier base de datos relacional (READ UNCOMMITTED, READ COMMITTED, etc.).

Nivel IBM iEquivalente SQL estandarComportamiento
*NONESin transaccionCada sentencia es auto-commit (sin rollback)
*CHGREAD UNCOMMITTEDLee datos no confirmados (dirty reads posibles)
*CSREAD COMMITTEDSolo lee datos confirmados (cursor stability)
*ALLREPEATABLE READBloquea todos los registros leidos
*RRSERIALIZABLEAislamiento total, maximo bloqueo
SQLRPGLE — COMMIT y ROLLBACK
// El nivel de commit se define en compilacion:
// CRTSQLRPGI ... COMMIT(*CHG)
// O se puede cambiar en runtime con SET TRANSACTION

dcl-s wkError ind inz(*off);

// Iniciar logica transaccional
exec sql
  INSERT INTO MILIB.FACTURAS
    (FACID, FACCLIID, FACMONTO, FACFECHA)
  VALUES
    (50001, 1001, 15000.00, CURRENT_DATE);

if sqlcode <> 0;
  wkError = *on;
endif;

if not wkError;
  exec sql
    UPDATE MILIB.CLIENTES
    SET CLISALDO = CLISALDO + 15000.00
    WHERE CLIID = 1001;

  if sqlcode <> 0;
    wkError = *on;
  endif;
endif;

// Confirmar o revertir la transaccion completa
if not wkError;
  exec sql COMMIT;
  dsply 'Transaccion confirmada';
else;
  exec sql ROLLBACK;
  dsply 'Transaccion revertida por error';
endif;
Recomendacion: Para la mayoria de aplicaciones batch, usa COMMIT(*NONE) por simplicidad y rendimiento. Para aplicaciones interactivas o procesos que modifican multiples tablas relacionadas, usa COMMIT(*CHG) o *CS con COMMIT/ROLLBACK explicito.

Manejo de errores SQL

Despues de cada sentencia SQL embebida, Db2 actualiza las variables de estado SQLCODE y SQLSTATE. Ademas, GET DIAGNOSTICS permite obtener informacion detallada del ultimo error o condicion.

SQLCODE = 0

Exito: sentencia ejecutada correctamente

SQLCODE = 100

No encontrado: SELECT sin resultados o fin de cursor

SQLCODE > 0

Advertencia: sentencia ejecutada con condicion

SQLCODE < 0

Error: sentencia fallo (verificar SQLSTATE)

SQLSTATE '00000'

OK: ejecucion exitosa sin condiciones

SQLSTATE '02000'

Not found: no hay mas filas

SQLRPGLE — SQLCODE, SQLSTATE y GET DIAGNOSTICS
dcl-s wkNombre varchar(100);

exec sql
  SELECT CLINOM INTO :wkNombre
  FROM MILIB.CLIENTES
  WHERE CLIID = 9999;

// Verificacion basica con SQLCODE
select;
  when sqlcode = 0;
    dsply ('Encontrado: ' + %trim(wkNombre));

  when sqlcode = 100;
    dsply 'Cliente no encontrado';

  when sqlcode < 0;
    // Error: obtener detalles con GET DIAGNOSTICS
    dcl-s wkMsgText  varchar(500);
    dcl-s wkSQLState char(5);

    exec sql
      GET DIAGNOSTICS CONDITION 1
        :wkMsgText = MESSAGE_TEXT,
        :wkSQLState = RETURNED_SQLSTATE;

    dsply ('Error SQL ' + %char(sqlcode) +
           ' Estado: ' + wkSQLState);
    dsply ('Mensaje: ' + %trim(wkMsgText));
endsl;

// Obtener numero de filas afectadas
exec sql
  DELETE FROM MILIB.TEMPORAL
  WHERE FECHA < CURRENT_DATE - 90 DAYS;

dcl-s wkRowCount int(10);
exec sql
  GET DIAGNOSTICS :wkRowCount = ROW_COUNT;

dsply ('Registros eliminados: ' + %char(wkRowCount));
SQLRPGLE — Procedimiento reutilizable para log de errores
dcl-proc logErrorSQL;
  dcl-pi *n;
    operacion varchar(50) const;
  end-pi;

  dcl-s msgText   varchar(500);
  dcl-s sqlSt     char(5);
  dcl-s sqlCd     int(10);

  sqlCd = sqlcode;
  exec sql
    GET DIAGNOSTICS CONDITION 1
      :msgText = MESSAGE_TEXT,
      :sqlSt = RETURNED_SQLSTATE;

  // Registrar en tabla de errores
  exec sql
    INSERT INTO MILIB.ERROR_LOG
      (ERRTIMESTAMP, ERROPERACION, ERRSQLCODE,
       ERRSQLSTATE, ERRMENSAJE)
    VALUES
      (CURRENT_TIMESTAMP, :operacion, :sqlCd,
       :sqlSt, :msgText);

  // Commit del log independiente de la transaccion
  exec sql COMMIT;
end-proc;

// Uso del procedimiento:
exec sql
  UPDATE MILIB.CLIENTES SET CLISALDO = :nuevoSaldo
  WHERE CLIID = :clienteId;

if sqlcode < 0;
  logErrorSQL('UPDATE CLIENTES id=' + %char(clienteId));
  exec sql ROLLBACK;
endif;

Performance tips

SQL embebido en RPG puede ser extremadamente eficiente si se siguen buenas practicas. El optimizador de Db2 for i es muy sofisticado, pero necesita ayuda del programador para trabajar al maximo.

SQLRPGLE — Buenas practicas de rendimiento
// 1. SIEMPRE usar host variables (no concatenar valores)
// MAL: Db2 no puede reutilizar el plan de acceso
sqlStmt = 'SELECT * FROM CLIENTES WHERE CLIID = ' +
          %char(wkId);
exec sql EXECUTE IMMEDIATE :sqlStmt;

// BIEN: Db2 reutiliza el plan para cualquier valor de :wkId
exec sql
  SELECT CLINOM, CLISALDO INTO :wkNom, :wkSaldo
  FROM MILIB.CLIENTES
  WHERE CLIID = :wkId;

// 2. Usar FETCH FIRST n ROWS ONLY (como LIMIT en MySQL)
exec sql
  DECLARE curTop CURSOR FOR
    SELECT CLINOM, CLISALDO
    FROM MILIB.CLIENTES
    ORDER BY CLISALDO DESC
    FETCH FIRST 10 ROWS ONLY;

// 3. Seleccionar solo columnas necesarias (nunca SELECT *)
// MAL: trae todas las columnas, I/O innecesario
exec sql SELECT * INTO :clienteDS FROM MILIB.CLIENTES ...;
// BIEN: solo trae lo que necesitas
exec sql SELECT CLINOM, CLISALDO INTO :wkNom, :wkSaldo ...;

// 4. Blocked FETCH: leer multiples filas de una vez
dcl-ds arrayClientes qualified dim(100);
  id      packed(7:0);
  nombre  varchar(100);
  saldo   packed(11:2);
end-ds;
dcl-s wkFetched int(10);

exec sql
  DECLARE curBloque CURSOR FOR
    SELECT CLIID, CLINOM, CLISALDO
    FROM MILIB.CLIENTES WHERE CLIACTIVO = 'A';

exec sql OPEN curBloque;

dow sqlcode = 0;
  exec sql
    FETCH curBloque FOR 100 ROWS
    INTO :arrayClientes;

  exec sql GET DIAGNOSTICS :wkFetched = ROW_COUNT;

  for i = 1 to wkFetched;
    // Procesar arrayClientes(i) - 100 filas por viaje
  endfor;
enddo;

exec sql CLOSE curBloque;

// 5. OPTIMIZE FOR n ROWS (hint al optimizador)
exec sql
  DECLARE curOpt CURSOR FOR
    SELECT CLIID, CLINOM FROM MILIB.CLIENTES
    WHERE CLIACTIVO = 'A'
    OPTIMIZE FOR 50 ROWS;

Practicas que degradan rendimiento

  • SELECT * cuando solo necesitas 2 columnas
  • Concatenar valores literales en SQL dinamico
  • Abrir y cerrar cursor en cada iteracion
  • No usar indices en columnas del WHERE
  • FETCH de a una fila con millones de registros
  • Commitment control innecesario en batch read-only

Practicas que mejoran rendimiento

  • Seleccionar solo columnas necesarias
  • Host variables y PREPARE reutilizable
  • Abrir cursor una vez, iterar, cerrar
  • Crear indices que cubran los WHERE comunes
  • Blocked FETCH (FOR n ROWS) para volumen
  • COMMIT(*NONE) para procesos batch de lectura

Ejemplo completo

Un programa SQLRPGLE completo que genera un reporte de clientes con deuda, incluyendo cursores, manejo de errores, data structures y actualizacion de categorias basada en reglas de negocio.

RPTDEUDA.SQLRPGLE — Reporte de deudores
**free
// ---------------------------------------------------------------
// RPTDEUDA: Reporte de clientes con saldo deudor
//           Genera reporte y actualiza categorias
// ---------------------------------------------------------------
ctl-opt dftactgrp(*no) actgrp(*caller);
ctl-opt option(*srcstmt : *nodebugio);

// ---------------------------------------------------------------
// Interfaz del programa
// ---------------------------------------------------------------
dcl-pi *n;
  prmMinDeuda  packed(11:2);     // Deuda minima a reportar
  prmConteo    packed(7:0);      // Retorno: registros procesados
end-pi;

// ---------------------------------------------------------------
// Variables de trabajo
// ---------------------------------------------------------------
dcl-ds deudorDS qualified;
  id          packed(7:0);
  nombre      varchar(100);
  email       varchar(200);
  saldo       packed(11:2);
  diasDeuda   int(10);
end-ds;

dcl-s indEmail    int(5);
dcl-s wkContador  packed(7:0) inz(0);
dcl-s wkTotalDeuda packed(13:2) inz(0);
dcl-s wkCategoria char(10);
dcl-s wkMsgText   varchar(500);

// ---------------------------------------------------------------
// Cursor: clientes con deuda, con JOIN a pagos
// ---------------------------------------------------------------
exec sql
  DECLARE curDeudores CURSOR FOR
    SELECT c.CLIID,
           c.CLINOM,
           c.CLIEMAIL,
           c.CLISALDO,
           DAYS(CURRENT_DATE) - DAYS(
             COALESCE(p.ULTIMO_PAGO, c.CLIFECALTA)
           ) AS DIAS_DEUDA
    FROM MILIB.CLIENTES c
    LEFT JOIN (
      SELECT PAGCLIID,
             MAX(PAGFECHA) AS ULTIMO_PAGO
      FROM MILIB.PAGOS
      GROUP BY PAGCLIID
    ) p ON p.PAGCLIID = c.CLIID
    WHERE c.CLISALDO < :prmMinDeuda * -1
      AND c.CLIACTIVO = 'A'
    ORDER BY c.CLISALDO ASC;

// ---------------------------------------------------------------
// Logica principal
// ---------------------------------------------------------------
exec sql OPEN curDeudores;

if sqlcode <> 0;
  exec sql GET DIAGNOSTICS CONDITION 1
    :wkMsgText = MESSAGE_TEXT;
  dsply ('Error cursor: ' + %trim(wkMsgText));
  prmConteo = 0;
  *inlr = *on;
  return;
endif;

dow sqlcode = 0;
  exec sql
    FETCH curDeudores
    INTO :deudorDS.id, :deudorDS.nombre,
         :deudorDS.email :indEmail,
         :deudorDS.saldo, :deudorDS.diasDeuda;

  if sqlcode <> 0;
    leave;
  endif;

  wkContador += 1;
  wkTotalDeuda += %abs(deudorDS.saldo);

  // Categorizar segun dias de deuda
  select;
    when deudorDS.diasDeuda > 180;
      wkCategoria = 'INCOBRABLE';
    when deudorDS.diasDeuda > 90;
      wkCategoria = 'CRITICO';
    when deudorDS.diasDeuda > 30;
      wkCategoria = 'MOROSO';
    other;
      wkCategoria = 'PENDIENTE';
  endsl;

  // Actualizar categoria
  exec sql
    UPDATE MILIB.CLIENTES
    SET CLICATEG = :wkCategoria
    WHERE CLIID = :deudorDS.id;

  // Insertar en tabla de reporte temporal
  exec sql
    INSERT INTO QTEMP.RPT_DEUDA
      (RPTCLIID, RPTNOMBRE, RPTSALDO,
       RPTDIASDEUDA, RPTCATEGORIA)
    VALUES
      (:deudorDS.id, :deudorDS.nombre,
       :deudorDS.saldo, :deudorDS.diasDeuda,
       :wkCategoria);
enddo;

exec sql CLOSE curDeudores;

prmConteo = wkContador;
*inlr = *on;
return;
Patron clave: Observa como el programa combina SELECT con JOIN y subquery en el cursor, pero luego usa la logica procedural de RPG para la clasificacion y las actualizaciones. Esta combinacion de SQL para lectura y RPG para logica de negocio es el patron mas comun y eficiente en aplicaciones IBM i modernas.