miércoles, 29 de diciembre de 2010

CURSOR II: ROWSET

Vamos a mostrar otra manera de recuperar registros de un cursor.
Partimos de una rutina que realiza un CURSOR a una tabla para recuperar todos los registros que cumplan una determinada condición.
Nuestra rutina devolverá la información recuperada al programa llamante a través de una tabla de 10 ocurrencias definida en la linkage.

Vamos con el ejemplo:
Imaginemos que la tabla a la que queremos acceder tiene estos datos:


Siendo CAMPO1 la clave.

En esta versión del uso de cursores mostraremos el uso de la sentencia ROWSET para recuperar los datos.
Trabajaremos con un cursor ROWSET POSITIONING. Este tipo de cursor nos permite recuperar uno o más registros en un solo FETCH.
Definimos un cursor del siguiente modo:

WORKING-STORAGE SECTION.

EXEC SQL
   DECLARE CURSOR CUR_TABLA ROWSET POSITIONING FOR
    SELECT CAMPO1,
           CAMPO2,
           CAMPO3,
           CAMPO4,
           CAMPO5
      FROM TABLA1
     WHERE CAMPO1 = :CLAVE-CAMPO1
     ORDER BY CAMPO1
END-EXEC

01 TBL-FETCH.
   05 TBL-CAMPO1 PIC XX OCCURS 100 TIMES.
   05 TBL-CAMPO2 PIC XX OCCURS 100 TIMES.
   05 TBL-CAMPO3 PIC XX OCCURS 100 TIMES.
   05 TBL-CAMPO4 PIC XX OCCURS 100 TIMES.
   05 TBL-CAMPO5 PIC XX OCCURS 100 TIMES.

01 HNUM-REGS PIC S9(9) COMP.


IMPORTANTE: La variable HNUM-REGS sera la variable HOST que indicará al FETCH cuantos registros queremos recuperar.
Esta variable tiene que ser binaria (tipo COMP) y además debe estar definida en el nivel de working 01.

En la linkage recibiremos la información de los campos clave.

LINKAGE SECTION.
01 ENTRADA.
   05 NUM-REGISTROS PIC 9(4).
   05 ENT-CAMPO1 PIC XX.
01 SALIDA.
   05 SAL-DATOS OCCURS 10 TIMES.
      10 SAL-CAMPO1 PIC XX.
      10 SAL-CAMPO2 PIC XX.
      10 SAL-CAMPO3 PIC XX.
      10 SAL-CAMPO4 PIC XX.
      10 SAL-CAMPO5 PIC XX.


En el inicio del programa informamos las variables de acceso al WHERE y realizaremos el OPEN del cursor:

INICIO

IF ENT-CAMPO1 EQUAL SPACES OR LOW-VALUES
   DISPLAY 'LA CLAVE ESTA VACIA'
   PERFORM 90000-FINAL
END-IF

MOVE ENT-CAMPO1 TO CLAVE-CAMPO1


Si no informa la variable número de registros la informamos con el número máximo de ocurrencias, en este caso 10.

IF NUM-REGISTROS EQUAL ZEROS OR LOW-VALUES
   MOVE 10 TO HNUM-REGS
ELSE
   MOVE NUM-REGISTROS TO HNUM-REGS
END-IF

EXEC SQL
   OPEN CUR_TABLA
END-EXEC


Controlaremos el SQLCODE. Si no fuese cero, tendríamos que dar un error.

IF SQLCODE NOT EQUAL ZEROES
   DISPLAY 'ERROR EN EL OPEN CURSOR. EL SQLCODE ES: 'SQLCODE
   PERFORM 90000-FINAL
END-IF


En el proceso realizamos un FECTH para recuperar todos los registros marcados por la variable de entrada NUM-REGISTROS(HNUM-REGS) en una tabla interna(TBL-FETCH).
A continuación volcamos la información de nuestra tabla interna en la tabla de salida:

PROCESO

EXEC SQL
   FETCH NEXT ROWSET CUR_TABLA FOR :HNUM-REGS ROWS
    INTO :TBL-CAMPO1,
         :TBL-CAMPO2,
         :TBL-CAMPO3,
         :TBL-CAMPO4,
         :TBL-CAMPO5
END-EXEC


Controlamos el SQLCODE devuelto:

EVALUATE TRUE
   WHEN SQLCODE EQUAL ZEROES
      DISPLAY 'TODO EN ORDEN'
   WHEN SQLCODE EQUAL +100 AND SQLERRD(3) > 0
      DISPLAY 'TODO EN ORDEN'
   WHEN SQLCODE EQUAL +100 AND SQLERRD(3) = 0
      DISPLAY 'NO ENCUENTRO NADA'
      PERFORM 90000-FINAL
   WHEN OTHER
      DISPLAY 'ALGO VA MAL. EL SQLCODE ES: 'SQLCODE
      PERFORM 90000-FINAL
END-EVALUATE


La variable de SQL SQLERRD(3) nos informa de cuantos registros se han recuperado en el FETCH (pudiese darse el caso de que nosotros quisiésemos recuperar 30 registros pero en la tabla sólo hubiese 10, el resultado del FETCH sería un SQLCODE=100 pero SQLERRD(3) > 0).
En nuestro caso el número de registros recuperados sería 6.

PERFORM UNTIL IND-REGISTROS GREATER SQLERRD(3)
   PERFORM GUARDAR-EN-SAL-DATOS
END-PERFORM


Al terminar, cerraremos el cursor:

EXEC SQL
   CLOSE CUR_TABLA
END-EXEC


Controlamos el SQLCODE. Si es distinto de cero daremos un error.

IF SQLCODE NOT EQUAL ZEROES
   DISPLAY 'ERROR CERRANDO CURSOR. EL SQLCODE ES: 'SQLCODE
END-IF


Según la tabla de nuestro ejemplo, habríamos guardado en SAL-DATOS todos los registros.

Para finalizar devolvemos el control al programa llamante.

FINAL

GOBACK.


La sentencia ROWSET de DB2 se puede usar con otras instrucciones SQL. Se puede usar con el INSERT / UPDATE / DELETE.
Si alguien está interesado en conocer más sobre el tema, que pregunte sin compromiso : )

Os dejo el programa de ejemplo para descargar.

lunes, 27 de diciembre de 2010

Programas con DB2 II: CURSOR.

Listado: CURSOR.

Una rutina de listado realiza un CURSOR a una tabla para recuperar todos los registros que cumplan una determinada condición.
Nuestra rutina devolverá la información recuperada al programa llamante a través de una tabla interna definida en la linkage. Esta tabla tendrá un número de ocurrencias OCCURS que puede ser menor que el número de registros recuperados por el cursor.
Tendremos que tener en cuenta todo esto a la hora de codificar el programa.

Vamos con el ejemplo:
Imaginemos que la tabla a la que queremos acceder tiene estos datos:


Siendo CAMPO1 + CAMPO2 la clave.

Definiremos un cursor del siguiente modo:

WORKING-STORAGE SECTION.

EXEC SQL
   DECLARE CURSOR CUR_TABLA FOR
    SELECT CAMPO1,
           CAMPO2,
           CAMPO3,
           CAMPO4,
           CAMPO5
      FROM TABLA1
     WHERE CAMPO1 = :CLAVE-CAMPO1
       AND CAMPO2 > :CLAVE-CAMPO2
     ORDER BY CAMPO1, CAMPO2
END-EXEC

En la linkage recibiremos la información de los campos clave.

LINKAGE SECTION.
01 ENTRADA.
   05 ENT-CAMPO1 PIC XX.
   05 ENT-CAMPO2 PIC XX.
01 SALIDA.
   05 SAL-ULTIMA-CLAVE PIC XXXX.
   05 SAL-RECONSULTAR PIC X.
   05 SAL-DATOS OCCURS 5 TIMES.
      10 SAL-CAMPO1 PIC XX.
      10 SAL-CAMPO2 PIC XX.
      10 SAL-CAMPO3 PIC XX.
      10 SAL-CAMPO4 PIC XX.
      10 SAL-CAMPO5 PIC XX.

En el inicio del programa informaremos las variables de acceso al WHERE, realizaremos el OPEN del cursor y un primer FETCH. Podemos poner una validación del campo clave, para comprobar que trae datos, y en caso de no ser así finalizar la ejecución.

INICIO

IF ENT-CAMPO1 EQUAL SPACES OR LOW-VALUES
   DISPLAY 'LA CLAVE ESTA VACIA'
   PERFORM 90000-FINAL
END-IF

MOVE ENT-CAMPO1 TO CLAVE-CAMPO1
MOVE ENT-CAMPO2 TO CLAVE-CAMPO2

EXEC SQL
   OPEN CUR_TABLA
END-EXEC

Controlaremos el SQLCODE. Si no fuese cero, tendríamos que dar un error.

IF SQLCODE NOT EQUAL ZEROES
   DISPLAY 'ERROR EN EL OPEN CURSOR. EL SQLCODE ES: 'SQLCODE
   PERFORM 90000-FINAL
END-IF

EXEC SQL
   FETCH CUR_TABLA
    INTO :TABLA1-CAMPO1,
         :TABLA1-CAMPO2,
         :TABLA1-CAMPO3,
         :TABLA1-CAMPO4,
         :TABLA1-CAMPO5
END-EXEC

Controlamos el SQLCODE devuelto:

EVALUATE TRUE
   WHEN SQLCODE EQUAL ZEROES
      DISPLAY 'TODO EN ORDEN'
   WHEN SQLCODE EQUAL +100
      DISPLAY 'NO ENCUENTRO NADA'
      PERFORM 90000-FINAL
   WHEN OTHER
      DISPLAY 'ALGO VA MAL. EL SQLCODE ES: 'SQLCODE
      PERFORM 90000-FINAL
END-EVALUATE

SÓLO si el primer FETCH que realizamos nos devuelve un SQLCODE +100 quiere decir que no hay ningún registro con la clave que hemos introducido.
En posteriores FETCH, un SQLCODE +100 significa que YA NO HAY MÁS registros para esa clave.

En el proceso realizaremos un bucle para ir guardando la información devuelta por el cursor en nuestra tabla SAL-TABLA, e ir haciendo sucesivos FETCH hasta que recupere todos o el máximo de OCCURS de nuestra tabla interna:

PROCESO

PERFORM UNTIL SQLCODE EQUAL +100 OR IND-REGISTROS GREATER 5
   PERFORM GUARDAR-EN-SAL-TABLA

   PERFORM HAGO-OTRO-FETCH
END-PERFORM

Si salimos del bucle por la condición de SQLCODE = +100 informaremos el campo SAL-RECONSULTAR con una 'N' y el campo SAL-ULTIMA-CLAVE con espacios, pues ya hemos recuperado todos los registros que había en la tabla.

Si salimos del bucle por la condición de IND-REGISTROS > 5 informaremos el campo SAL-RECONSULTAR con una 'S' y el campo SAL-ULTIMA-CLAVE con los campos clave CAMPO1 y CAMPO2.

En cualquiera de los dos casos, cerraremos el cursor:

EXEC SQL
   CLOSE CUR_TABLA
END-EXEC

Controlamos el SQLCODE. Si es distinto de cero daremos un error.

IF SQLCODE NOT EQUAL ZEROES
   DISPLAY 'ERROR CERRANDO CURSOR. EL SQLCODE ES: 'SQLCODE
END-IF

Según la tabla de nuestro ejemplo, habríamos guardado en SAL-TABLA los registros:

Por lo que nos faltaría por recuperar un último registro. El campo SAL-ULTIMA-CLAVE valdría 'AAFF'.

Para finalizar devolvemos el control al programa llamante.
FINAL

GOBACK.

La siguiente vez que entrásemos al programa accederíamos al cursor con CAMPO1 = 'AA' y CAMPO2 = 'FF' y recuperaríamos el registro que falta:


Os dejo el código completo de un programa de ejemplo para descargar.

Nota: En respuesta a la sugerencia de Jose Antonio, hemos creado un nuevo artículo explicando el uso del ROWSET en los cursores.

lunes, 20 de diciembre de 2010

SQL en JCL.

En este artículo vamos a ver una manera de hacer querys a través de un JCL:

//PASO001  EXEC IKJEFT01
//SYSTSIN  DD *
DSN SYSTEM(DB2D)
RUN PROGRAM(DSNTEP2) PLAN(DSNTEP2) -
    LIB('LIBRERIA.DE.TU.INSTALACION')
END
//SYSIN    DD *
Aquí escribiríamos la query
/*


Por ejemplo:
//SYSIN    DD *
  UPDATE TBPRU01
   SET CAMPO1 = 'SI'
   WHERE CLAVE = '01'
  ;
/*

Ó

//SYSIN    DD *
INSERT INTO TBPRU01
      (CAMPO1, CAMPO2, CAMPO3)
      VALUES
      ('01', '02', '03')
      ;
/*

Otra utilidad de las querys en JCLs podría ser hacer un inventario de todas las tablas de las aplicaciones, con sus campos, índices, etc. Lo que viene siendo la típica documentación que nadie quiere hacer, pero que con este JCL te llevará 1 minuto.

Para ello utilizaríamos las ya conocidas tablas del SYSIBM: SYSTABLES, SYSCOLUMS, SYSINDEXES, SYSKEYS.

Os dejo dos ejemplos: uno para obtener la información de los campos de cada tabla (con su formato) y otro para obtener la información de los índices de cada tabla (con los campos que los componen).

Inventario tablas:

//UNLOAD EXEC PGM=IKJEFT01,DYNAMNBR=20
//SYSTSPRT DD SYSOUT=*
//SYSTSIN DD *
DSN SYSTEM(DB2D)
RUN PROGRAM(DSNTIAUL) PLAN(DSNTIAUL) -
LIB('LIBRERIA.DE.TU.INSTALACION')
//SYSPRINT DD SYSOUT=*
//SYSUDUMP DD SYSOUT=*
//SYSPUNCH DD SYSOUT=*
//SYSREC00 DD DSN=XXXX.UNLOAD.TABLAS,
//            DISP=(NEW,CATLG,DELETE),UNIT=SYSDA,
//            SPACE=(CYL,(100,50),RLSE)
//SYSIN DD *
  SELECT B.TBNAME || ';' ||
        A.REMARKS || ';' ||
        B.NAME    || ';' ||
        CASE
         WHEN B.COLTYPE = 'INTEGER' THEN
           STRIP(B.COLTYPE,T,' ') || '(' ||
           STRIP(DIGITS(B.LENGTH),L,'0') || ')'
         WHEN B.COLTYPE = 'CHAR' THEN
           STRIP(B.COLTYPE,T,' ') || '(' ||
           STRIP(DIGITS(B.LENGTH),L,'0') || ')'
         WHEN B.COLTYPE = 'DECIMAL' THEN
           STRIP(B.COLTYPE,T,' ') || '(' ||
           STRIP(DIGITS(B.LENGTH),L,'0') || ',' ||
           STRIP(DIGITS(B.SCALE),L,'0') || ')'
        ELSE
           STRIP(B.COLTYPE,T,' ')
        END || ';' ||
        B.NULLS || ';' ||
        B.REMARKS
   FROM SYSIBM.SYSTABLES A
       ,SYSIBM.SYSCOLUMNS B
  WHERE A.NAME = B.TBNAME
    AND B.TBNAME LIKE 'TBPRU%'
    AND A.CREATOR = 'DB2O'
    AND B.CREATOR = 'DB2O'
 ORDER BY B.TBNAME,
          B.COLNO;
/*

Incluimos entre cada campo un separador ";" para poder guardar la información en un archivo .csv y poder abrirlo con el EXCEL.

El resultado una vez exportado a .csv sería:
Columna 1: Nombre físico
Columna 2: Nombre descriptivo
Columna 3: Campo
Columna 4: Tipo de dato
Columna 5: Acepta nulos
Columna 6: Descripción del campo

TBPRU01;TABLA NUMERO 1;CAMPO1;DECIMAL(9,2);N;ES EL CAMPO NUMERO 1


Inventario índices:

//UNLOAD EXEC PGM=IKJEFT01,DYNAMNBR=20
//SYSTSPRT DD SYSOUT=*
//SYSTSIN DD *
DSN SYSTEM(DB2D)
RUN PROGRAM(DSNTIAUL) PLAN(DSNTIAUL) -
LIB('LIBRERIA.DE.TU.INSTALACION')
//SYSPRINT DD SYSOUT=*
//SYSUDUMP DD SYSOUT=*
//SYSPUNCH DD SYSOUT=*
//SYSREC00 DD DSN=XXXX.UNLOAD.INDICES,
//            DISP=(NEW,CATLG,DELETE),UNIT=SYSDA,
//            SPACE=(CYL,(100,50),RLSE)
//SYSIN DD *
SELECT A.TBNAME || ';' ||
     A.NAME || ';' ||
     B.COLNAME
FROM SYSIBM.SYSINDEXES A
    ,SYSIBM.SYSKEYS B
WHERE A.NAME = B.IXNAME
 AND A.TBNAME LIKE 'TBPRU%'
 AND A.CREATOR = 'DB2O'
 AND B.CREATOR = 'DB2O'
ORDER BY A.TBNAME
      ,A.NAME
      ,B.COLNO;
/*

El resultado una vez exportado a .csv sería:
Columna 1: Nombre físico tabla
Columna 2: Nombre índice
Columna 3: Campo

TBPRU01;INPRU011;CAMPO1


Y por supuesto podéis hacer las combinaciones de campos que os parezcan.

miércoles, 15 de diciembre de 2010

Utilidades REXX I: Querys dinámicas.

REXX (REstructured eXtended eXecutor) es un lenguaje de programación desarrollado en IBM y, en principio, está disponible en todos los entornos mainframe. No necesita compilación y puede ejecutarse tanto desde un JCL como con un TSO EXEC.

No vamos a entrar a ver las sentencias básicas del REXX ni como construír "mi primer programa", pero si alguien tiene alguna duda, puede consultárnoslo ^^.

Lo que vamos a ver es un ejemplo de para qué podemos usar este lenguaje. En nuestro caso será para construir una query dinámica, es decir, una sentencia SELECT en la que los campos del FROM y del WHERE serán variables que tomaremos de un fichero.

¿Para qué puedo necesitar esto? Veamos el caso práctico.

Imaginemos que queremos localizar el apellido "Ercilurrutigastañaza" porque hemos cambiado los campos que contienen apellidos de nuestra base de datos y ahora ya nos cabe el apellido entero "Ercilurrutigastañazagogeascoa". Y queremos actualizarlo para que el pobre hombre tenga bien puesto su apellido en los recibos del banco.
Vamos a buscar todas las tablas donde esté almacenado ese apellido.

El problema es que los campos donde se guarda el apellido no tienen un nombre intuitivo, asi que no podemos buscar por todas las tablas el campo "APEL", por ejemplo.

Lo que buscaremos será el contenido del campo, teniendo en cuenta que el campo mide 20 posiciones y que es un carácter, buscaremos en todas las tablas los campos que midan 20 o más y que sean caracteres.

Veamos la cadena con la sentencia que nos recuperará la lista de tablas+campos en un fichero:

//*-----------------------------------------------------------
//* DESCARGA | UNLOAD | DESCARGAMOS EN UN FICHERO LA INFORMACION
//*------------------------------------------------------------
//UNLOAD EXEC PGM=IKJEFT01,DYNAMNBR=20
//SYSTSPRT DD SYSOUT=*
//SYSTSIN DD *
 DSN SYSTEM(DSNT)
 RUN PROGRAM(DSNTIAUL) PLAN(DSNTIAUL) -
 LIB('LIBRERIA.DE.TU.INSTALACION')
//SYSPRINT DD SYSOUT=*
//SYSUDUMP DD SYSOUT=*
//SYSPUNCH DD SYSOUT=*
//SYSREC00 DD DSN=XXXX.FICHERO.CAMPOS.TABLAS,
// DISP=(,CATLG,),UNIT=SYSDA,
// SPACE=(CYL,(100,50),RLSE)
//SYSIN DD *
  SELECT CHAR(TBNAME, 20)
        ,CHAR(NAME, 20)
    FROM SYSIBM.SYSCOLUMNS
   WHERE TBNAME LIKE 'TB%'
     AND COLTYPE LIKE '%CHAR%'
     AND TBCREATOR = 'DBO'
     AND LENGTH >= 20
     ;
/*


Donde:
CHAR(TBNAME, 20):Lo que mida el nombre de las tablas como máximo
CHAR(NAME, 20) :Lo que mida el nombre de los campos como máximo
SYSIBM.SYSCOLUMNS:Contiene información de cada columna de cada tabla y vista de la BD
'TB%':Parte común a todas nuestras tablas
'%CHAR%':Es el tipo de dato
'DBO':Owner DB2 que corresponda
LENGTH:Longitud del campo que buscamos

La información del fichero tendría esta pinta:
TBPRU01 CAMPO1
TBPRU01 CAMPO2
TBPRU01 CAMPO3
TBPRU02 CAMPO4
TBPRU02 CAMPO5
TBPRU02 CAMPO6
TBPRU03 CAMPO7
TBPRU03 CAMPO8
TBPRU03 CAMPO9
TBPRU04 CAMPO1
(...)

Veamos el código del programa REXX:

/* REXX */
STAT = MSG('ON')
USER=SYSVAR(SYSUID)
RXSQL_DB2SUBSYS = DSNT
"EXECIO * DISKR DESCARGA (STEM LINE. FINIS"
DO I = 1 TO LINE.0
   TABLA= STRIP(SUBSTR(LINE.I,1,20))
   CAMPO= STRIP(SUBSTR(LINE.I,21,20))
   SENTENCIA = "SELECT COUNT(*)",
               "FROM "STRIP(TABLA),
               "WHERE "STRIP(CAMPO) "= 'Ercilurrutigastañaza'"
   CALL RXSQL SENTENCIA
   IF RXSQL_SQLCODE < 0 THEN
      DO
        SAY "ERROR EN TABLA:"STRIP(TABLA) "SQLCODE:"RXSQL_SQLCODE
        SAY SENTENCIA
      END
   IF STRIP(TEMP.1.1) > 0 THEN
      DO
        SAY SENTENCIA
        SAY STRIP(TEMP.1.1)
      END
END
CALL RXSQLEND
EXIT 0

Donde:
"EXECIO * DISKR DESCARGA (STEM LINE. FINIS":Lee el fichero DESCARGA hasta el final
TABLA= STRIP(SUBSTR(LINE.I,1,20)):Los nombres de tablas del fichero
CAMPO= STRIP(SUBSTR(LINE.I,21,20)):Los nombres de campos del fichero
SAY SENTENCIA:Nos displayará las sentencias en las que encontremos el apellido que buscamos

Lo ejecutaremos en una cadena:

//*=========================================================
//* EJECUCION DEL PROGRAMA REXX
//*=========================================================
//REXTBLS EXEC PGM=IKJEFT01
//*-------
//SYSPROC DD DISP=SHR,DSN=LIBRERIA.DONDE.ESTA.EL.PROGRAMA
//DESCARGA DD DISP=SHR,DSN=XXXX.FICHERO.CAMPOS.TABLAS
//SYSTSPRT DD DSN=XXXX.LISTA.CAMPOS.APELLIDO
//         DISP=(,CATLG),
//         SPACE=(CYL,(60,10),RLSE),UNIT=SYSDA
//SYSLIST DD SYSOUT=*
//SYSOUT DD SYSOUT=*
//SYSTSIN DD *
 REXTBLS
/*

Donde:
XXXX.LISTA.CAMPOS.APELLIDO:Fichero donde escribiremos los SAY
REXTBLS:Nombre del programa REXX

Podéis descargaros el JCL completo y el código del programa.

martes, 14 de diciembre de 2010

SYSTABLES, SYSCOLUMNS y SYSINDEXES

Muchas veces queremos conocer la información relativa a la base de datos de una aplicación o instalación, sobre todo cuando nos enfrentamos con ella por primera vez.

Uno de los métodos más fiables y rápidos es utilizar las tablas donde DB2 guarda dicha información. Estas tablas forman parte del "table space" SYSDBASE.

A continuación mencionamos las más comunes y de mayor utilidad. Los ejemplos los haremos sobre el propio SYSDBASE por ser común en todas las instalaciones, pero dejamos como sugerencia para familiarizarnos con las mismas modificar dichas sentencias a gusto de consumidor.

SYSTABLES

Contiene información de cada tabla, vista o alias de la BD.

Algunos campos relevantes de la tabla son los siguientes:

NAME : nombre de la tabla, vista o alias
CREATOR : esquema de la tabla, vista o alias
TYPE : tipo de objeto (A=alias, T=tabla, V=vista...)
DBNAME : base de datos que contiene el "table space" de la tabla o vista
TSNAME : "table space" que contiene la tabla o vista
COLCOUNT : número de columnas de la tabla o vista
REMARKS : información adicional sobre el objeto

Gracias a esta tabla podríamos realizar una consulta a la BD para obtener información de las tablas que forman parte de una aplicación, las vistas que existen en la instalación... En este caso consultaremos las tablas que forman parte del "table space" SYSDBASE:

SELECT SUBSTR(NAME, 1, 15) AS NAME ,      
       SUBSTR(CREATOR, 1, 10) AS CREATOR ,
       SUBSTR(DBNAME, 1, 10) AS DBNAME ,  
       COLCOUNT ,                         
       REMARKS                            
  FROM SYSIBM.SYSTABLES                   
WHERE  CREATOR = 'SYSIBM'                 
AND    TYPE    = 'T'                      
AND    TSNAME  = 'SYSDBASE';              

El resultado de la consulta son el nombre de las tablas y el número de columnas que las forman:

---------+---------+---------+---------+---------+---------+-----
NAME             CREATOR     DBNAME      COLCOUNT  REMARKS       
---------+---------+---------+---------+---------+---------+-----
SYSFIELDS        SYSIBM      DSNDB06           13                
SYSTABLESPACE    SYSIBM      DSNDB06           41                
SYSTABLES        SYSIBM      DSNDB06           52                
SYSTABLEPART     SYSIBM      DSNDB06           41                
SYSTABAUTH       SYSIBM      DSNDB06           29                
SYSSYNONYMS      SYSIBM      DSNDB06            7                
SYSRELS          SYSIBM      DSNDB06           15                
SYSKEYS          SYSIBM      DSNDB06            7                
SYSINDEXPART     SYSIBM      DSNDB06           35                
SYSINDEXES       SYSIBM      DSNDB06           46                
SYSFOREIGNKEYS   SYSIBM      DSNDB06            7                
SYSCOLUMNS       SYSIBM      DSNDB06           35                
SYSCOLAUTH       SYSIBM      DSNDB06           15                

SYSCOLUMNS

Contiene información de cada columna de cada tabla y vista de la BD.

Algunos campos relevantes de la tabla son los siguientes:

NAME : nombre de columna
TBNAME : nombre de la tabla o vista que contiene la columna
TBCREATOR : esquema de la tabla o vista que contiene la columna
COLTYPE : tipo de la columna (INTEGER, FLOAT, CHAR...)
LENGTH : longitud del atributo de la columna, en caso de un decimal sería la precisión
SCALE : escala de un decimal
NULLS : indica si la columna puede contener valores nulos (Y=si, N=no)
REMARKS : información adicional sobre la columna

En este ejemplo mostraremos la forma de obtener información relativa a todos los campos de la tabla SYSCOLUMNS:

SELECT SUBSTR(TBNAME, 1, 10) AS TBNAME ,
       SUBSTR(NAME, 1, 10) AS NAME ,
       COLTYPE ,
       LENGTH ,
       SCALE ,
       NULLS ,
       REMARKS
  FROM SYSIBM.SYSCOLUMNS
 WHERE TBNAME = 'SYSTABLES';

Como resultado obtenemos todos los campos con sus datos de longitud, comentarios, etc...:

---------+---------+---------+---------+---------+---------+----
TBNAME      NAME        COLTYPE   LENGTH   SCALE  NULLS  REMARKS
---------+---------+---------+---------+---------+---------+----
SYSCOLUMNS  COLCARDF    FLOAT          8       0  N   
SYSCOLUMNS  HIGH2KEY    VARCHAR     2000       0  N   
SYSCOLUMNS  LOW2KEY     VARCHAR     2000       0  N   
SYSCOLUMNS  NAME        VARCHAR      128       0  N   
SYSCOLUMNS  STATSTIME   TIMESTMP      10       0  N   
SYSCOLUMNS  TBCREATOR   VARCHAR      128       0  N   
SYSCOLUMNS  TBNAME      VARCHAR      128       0  N   
SYSCOLUMNS  VERSION     SMALLINT       2       0  N  
SYSCOLUMNS  COLNO       SMALLINT       2       0  N 
SYSCOLUMNS  COLTYPE     CHAR           8       0  N 
SYSCOLUMNS  DEFAULT     CHAR           1       0  N 
SYSCOLUMNS  DEFAULTVAL  VARCHAR     1536       0  N 
SYSCOLUMNS  FOREIGNKEY  CHAR           1       0  N 
SYSCOLUMNS  KEYSEQ      SMALLINT       2       0  N 
SYSCOLUMNS  LENGTH      SMALLINT       2       0  N 
SYSCOLUMNS  NAME        VARCHAR      128       0  N 
SYSCOLUMNS  NULLS       CHAR           1       0  N 
...

SYSINDEXES

Contiene información de cada índice de cada tabla y vista de la BD.

Algunos campos relevantes de la tabla son los siguientes:

NAME : nombre del índice
CREATOR : esquema de la tabla o vista del índice
TBNAME : nombre de la tabla o vista del índice
TBCREATOR : esquema de la tabla o vista del índice
UNIQUERULE : indica si el índice es único (D=se permiten duplicados, U=Es único, P=Único e índice primario...)
CLUSTERING : si ha sido especificado "cluster" para el índice (Y=si, N=no)

En este ejemplo uniremos las tablas SYSINDEXES y SYSKEYS para obtener toda la información que nos interesa, ya que SYSKEYS tiene información de los campos que contienen los índices definidos en SYSINDEXES.

SELECT SUBSTR(TBNAME, 1, 10) AS TBNAME,
       SUBSTR(NAME, 1, 15) AS NAME,
       SUBSTR(COLNAME, 1, 10) AS COLNAME,
       UNIQUERULE ,
       CLUSTERING
  FROM SYSIBM.SYSINDEXES A , SYSIBM.SYSKEYS B
 WHERE A.NAME = B.IXNAME
   AND A.TBNAME = 'SYSTABLES';


El resultado son todos los índices definidos sobre la tabla SYSTABLES y los campos que los forman:

---------+---------+---------+---------+---------+---------+---
TBNAME      NAME             COLNAME     UNIQUERULE  CLUSTERING
---------+---------+---------+---------+---------+---------+---
SYSTABLES   SYSTABLES_IX     CREATOR     D           Y
SYSTABLES   SYSTABLES_IX     NAME        D           Y
SYSTABLES   SYSTABLES_IX     STATSTIME   D           Y
SYSTABLES   SYSTABLES_IX2    DBNAME      D           N
SYSTABLES   SYSTABLES_IX2    STATSTIME   D           N
SYSTABLES   SYSTABLES_IX2    TSNAME      D           N
SYSTABLES   DSNDTX01         CREATOR     P           N
SYSTABLES   DSNDTX01         NAME        P           N
SYSTABLES   DSNDTX02         CREATOR     U           N
SYSTABLES   DSNDTX02         DBID        U           N
SYSTABLES   DSNDTX02         NAME        U           N
SYSTABLES   DSNDTX02         OBID        U           N
SYSTABLES   DSNDTX03         TBCREATOR   D           N
SYSTABLES   DSNDTX03         TBNAME      D           N
...

Una posible aplicación interesante de dichas consultas sería extraer la información de la BD a ficheros para ser importada (tras formateo previo) a herramientas CASE. De esta forma, tendríamos modelos entidad-relación completamente actualizados de forma automática. Lo dejamos como sugerencia de un futuro post.

lunes, 13 de diciembre de 2010

Programas con DB2 I: SELECT, INSERT, UPDATE y DELETE.

En la mayoría de aplicaciones existirán, para cada tabla, una serie de rutinas con los diferentes tipos de accesos: consulta, lista, alta, modificación y baja.
Esto se traduce en SQL como: SELECT, CURSOR, INSERT, UPDATE y DELETE.
En este artículo vamos a "construir" cuatro de ellos; dejaremos la rutina de lista para explicar en un artículo aparte.

Este tipo de rutinas serán llamadas desde otros programas, ya sean BATCH u ONLINE. Tendrán definidas en la LINKAGE SECTION los campos a través de los cuales intercambiarán información con el programa llamante. Estos campos pueden venir definidos dentro de COPYs.

Consulta: SELECT.

Se trata del caso más sencillo en el que sólo queremos recuperar 1 registro de la tabla.

Vamos a ver un ejemplo:
LINKAGE SECTION
01 ENTRADA.
   05 CLAVE-SELECT PIC X(10).
01 SALIDA.
   05 SAL-CAMPO1 PIC X(10).
   05 SAL-CAMPO2 PIC X.
   05 SAL-PRI-CAMPO3 PIC XX.
   05 SAL-SEC-CAMPO3 PIC XX.

(...)
Los campos de entrada de la linkage contendrán la información enviada por el programa llamante. En este caso la clave por la que queremos acceder a nuestra SELECT.

En el inicio del programa podemos poner una validación del campo clave, para comprobar que trae datos, y en caso de no ser así finalizar la ejecución.
INICIO

IF CLAVE-SELECT EQUAL SPACES OR LOW-VALUES
   DISPLAY 'LA CLAVE VIENE VACIA'
   PERFORM 90000-FINAL
END-IF


Realizaremos la SELECT:

EXEC SQL
   SELECT CAMPO1,
          CAMPO2,
          PRI_CAMPO3,
          SEC_CAMPO3
     INTO :TABLA1-CAMPO1,
          :TABLA1-CAMPO2,
          :TABLA1-PRI-CAMPO3,
          :TABLA1-SEC-CAMPO3
     FROM TABLA1
    WHERE CAMPO1 = :CLAVE-SELECT
    ORDER BY CAMPO1
END-EXEC


Los campos que van en el apartado de SELECT son los campos de la tabla. Se escribirán del mismo modo en el que estén en el CREATE de la tabla. Se utilizará siempre el guión bajo.
Ocurre lo mismo en el apartado de ORDER BY.

Los campos que van en el apartado de INTO son variables HOST definidas en la WORKING del programa. Podemos nombrarlos como queramos. Siempre irán precedidos por los dos puntos ":". Se utilizará siempre el guión alto.
Ocurre lo mismo para los campos utilizados en el apartado de WHERE.
En caso de que tengamos campos numéricos, siempre irán definidos como signados y comprimidos (S9(X) COMP-3).

La variable HOST utilizada en el WHERE no deberá tener niveles inferiores. En caso de que así sea, deberemos utilizar un REDEFINES o moverla a otra variable:
01 CLAVE-SELECT.
   05 PARTE1 PIC X(5).
   05 PARTE2 PIC X(5).
01 CLAVE-ENTERA REDEFINES CLAVE-SELECT PIC X(10).

O bien:
01 CLAVE-SELECT.
   05 PARTE1 PIC X(5).
   05 PARTE2 PIC X(5).
01 CLAVE-ENTERA PIC X(10).
(...)
MOVE CLAVE-SELECT TO CLAVE-ENTERA


Puede darse el caso de que el campo del WHERE no sea una clave única de la tabla, y que la SELECT nos recupere más de un registro.
Si sólo queremos saber que existe "alguno" que cumple la condición, podemos solventar este problema añadiendo a la SELECT la sentencia FETCH FIRST ROW ONLY:

EXEC SQL
   SELECT CAMPO1,
          CAMPO2,
          PRI_CAMPO3,
          SEC_CAMPO3
     INTO :TABLA1-CAMPO1,
          :TABLA1-CAMPO2,
          :TABLA1-PRI-CAMPO3,
          :TABLA1-SEC-CAMPO3
     FROM TABLA1
    WHERE CAMPO1 = :CLAVE-SELECT
    ORDER BY CAMPO1
    FETCH FIRST ROW ONLY
END-EXEC


De este modo aunque recupere más de un registro no dará error, y nos devolverá el primero que encuentre.
Si no lo hiciésemos la consulta devolvería un SQLCODE = -811.

Una vez codificada la SELECT validaremos el código SQL que nos devuelve para comprobar que todo ha ido bien:

EVALUATE TRUE
   WHEN SQLCODE EQUAL ZEROES
      DISPLAY 'TODO VA BIEN'
   WHEN SQLCODE EQUAL +100
      DISPLAY 'NO ENCONTRE NADA'
   WHEN OTHER
      DISPLAY 'ALGO HA IDO MAL. EL SQLCODE ES: 'SQLCODE
      PERFORM 90000-FINAL
END-EVALUATE


Si la SELECT nos devuelve un SQLCODE distinto de cero y de 100, lo normal es finalizar la ejecución del programa.

Una vez comprobado que todo ha ido bien, informaremos los campos de SALIDA con la información recuperada de la SELECT:
MOVE TABLA1-CAMPO1     TO SAL-CAMPO1
MOVE TABLA1-CAMPO2     TO SAL-CAMPO2
MOVE TABLA1-PRI-CAMPO3 TO SAL-PRI-CAMPO3
MOVE TABLA1-SEC-CAMPO3 TO SAL-SEC-CAMPO3


En el párrafo de final devolveremos el control al programa llamante:
FINAL

GOBACK.


Y se terminó nuestra rutina de consulta : )

Alta: INSERT.

Cuando queremos dar de alta/insertar un registro en una tabla, tendremos que informar un mínimo de campos de la tabla(se deben informar al menos aquellos que pertenezcan a la clave única).
Estos campos vendrán informados desde el programa llamante y los recibiremos en la linkage.

Veamos un ejemplo:
LINKAGE SECTION.
01 ENTRADA.
   05 ENT-CAMPO1 PIC X(10).
   05 ENT-CAMPO2 PIC X.
   05 ENT-PRI-CAMPO3 PIC XX.
   05 ENT-SEC-CAMPO3 PIC XX.


En el inicio del programa podemos poner una validación de los campos obligatorios, para comprobar que vienen informados, y en caso de no ser así finalizar la ejecución.

Informaremos las variables HOST con las que realizaremos el INSERT:
MOVE ENT-CAMPO1     TO TABLA1-CAMPO1
MOVE ENT-CAMPO2     TO TABLA1-CAMPO2
MOVE ENT-PRI-CAMPO3 TO TABLA1-PRI-CAMPO3
MOVE ENT-SEC-CAMPO3 TO TABLA1-SEC-CAMPO3


Realizamos el INSERT:
EXEC SQL
   INSERT INTO TABLA1
         (CAMPO1,
          CAMPO2,
          PRI_CAMPO3,
          SEC_CAMPO3)
   VALUES(:TABLA1-CAMPO1,
          :TABLA1-CAMPO2,
          :TABLA1-PRI-CAMPO3,
          :TABLA1-SEC-CAMPO3)
END-EXEC


Es importante colocar los campos definidos en VALUES en el orden correcto.

Ahora validaremos el SQLCODE devuelto para comprobar que todo ha ido bien:
EVALUATE TRUE
   WHEN SQLCODE EQUAL ZEROES
      DISPLAY 'TODO HA IDO BIEN'
   WHEN SQLCODE EQUAL -803
      DISPLAY 'EL REGISTRO YA EXISTE EN LA TABLA'
   WHEN OTHER
      DISPLAY 'LA HEMOS LIAO PARDA. EL SQLCODE ES: 'SQLCODE
      PERFORM 90000-FINAL
END-EVALUATE


Una vez comprobado que todo ha ido bien, devolveremos el control al programa llamante:
FINAL
GOBACK.


Modificación: UPDATE.

Puede ocurrir que queramos modificar un solo campo o todos. Si no sabemos qué campos van a venir modificados lo normal es actualizarlos todos. Para ello debemos haber recuperado previamente toda la información del registro, o podríamos estar actualizando algunos campos con ceros o espacios.

En una rutina de modificación ONLINE se debe controlar siempre el TIMESTAMP de modificación del registro, para asegurarnos que no ha sido modificado mientras nosotros lo estábamos visualizando.

Veamos el ejemplo:
LINKAGE SECTION.
01 ENTRADA.
   05 ENT-CAMPO1 PIC X.
   05 ENT-CAMPO2 PIC X.
   05 ENT-CAMPO3 PIC XX.
   05 ENT-CAMPO4 PIC XX.
   05 ENT-TIMESTAMP PIC X(26).


La información de la linkage la habrá obtenido el programa llamante realizando una consulta del registro y con la información de los campos modificados.

Antes de realizar el UPDATE realizaremos una nueva consulta del registro:

EXEC SQL
   SELECT CAMPO1,
          TIMESTAMP
     INTO :TABLA2-CAMPO1,
          :TABLA2-TIMESTAMP
     FROM TABLA2
    WHERE CAMPO1 = :ENT-CAMPO1
    ORDER BY CAMPO1
END-EXEC


Validaremos el SQLCODE devuelto.
Si todo ha ido bien:
Compararemos el TIMESTAMP obtenido en TABLA2-TIMESTAMP con el recibido por linkage.
Recordad que un campo timestamp tiene este aspecto: 'AAAA-MM-DD-HH.MM.SS.MMMMMM'
Donde:
AAAA: año
MM: mes
DD: día
HH: hora
MM: minutos
SS: segundos
MMMMMM: milésimas de segundo

2010-12-01-22.15.01.012345

En total 26 posiciones.

IF ENT-TIMESTAMP NOT EQUAL TABLA2-TIMESTAMP
   DISPLAY 'ERROR: REGISTRO MODIFICADO POR OTRO USUARIO'
   PERFORM 90000-FINAL
END-IF


Esto significa que mientras nosotros visualizábamos el registro, otra persona lo modificó.
En caso de que los TIMESTAMP coincidan, informaremos las variables HOST del update con la información recibida por linkage.
MOVE ENT-CAMPO1 TO TABLA2-CAMPO1
MOVE ENT-CAMPO2 TO TABLA2-CAMPO2
MOVE ENT-CAMPO3 TO TABLA2-CAMPO3
MOVE ENT-CAMPO4 TO TABLA2-CAMPO4


Procederemos a hacer el UPDATE:

EXEC SQL
   UPDATE TABLA2
      SET (CAMPO2 = :TABLA2-CAMPO2,
           CAMPO3 = :TABLA2-CAMPO3,
           CAMPO4 = :TABLA2-CAMPO4,
           TIMESTAMP = CURRENT TIMESTAMP)
    WHERE CAMPO1 = :TABLA2-CAMPO1
END-EXEC


Tened en cuenta que no se deben modificar los campos de la tabla que pertenezcan a la clave única.

Validaremos el SQLCODE devuelto.
EVALUATE TRUE
   WHEN SQLCODE EQUAL ZEROES
      DISPLAY 'TODO HA IDO BIEN'
   WHEN SQLCODE EQUAL +100
      DISPLAY 'EL REGISTRO QUE SE QUIERE MODIFICAR NO EXISTE'
   WHEN OTHER
      DISPLAY 'YA LA HEMOS VUELTO A LIAR. EL SQLCODE ES: 'SQLCODE
      PERFORM 90000-FINAL
END-EVALUATE


Y devolveremos el control al programa llamante.

Puede darse el caso de que queramos modificar algún campo de la clave del registro. Normalmente para que pueda hacerse debe existir un campo clave o ISN a mayores que será numérico y autoincrementable y que identificará inequívocamente cada registro de la tabla.
Por ejemplo:


Donde CAMPO1 será el índice único y CLAVE la clave autoincrementable.
Ahora podríamos modificar el campo CAMPO1 accediendo a la tabla por el campo CLAVE.
Por ejemplo, si queremos modificar el campo CAMPO1 del registro con CLAVE = 14:
MOVE '5'  TO CAMPO1
MOVE 'X'  TO CAMPO2
MOVE 'YY' TO CAMPO3
MOVE 'ZZ' TO CAMPO4

EXEC SQL
   UPDATE TABLA2
     SET (CAMPO1 = :TABLA2-CAMPO1,
          CAMPO2 = :TABLA2-CAMPO2,
          CAMPO3 = :TABLA2-CAMPO3,
          CAMPO4 = :TABLA2-CAMPO4,
          TIMESTAMP = CURRENT TIMESTAMP)
    WHERE CLAVE = :TABLA2-CLAVE
END-EXEC


y el registro con CLAVE =14 quedaría:


Baja: DELETE.

Este tipo de baja también se denomina "baja física", en contraste con la "baja lógica", donde el registro no se borra, sino que se actualiza un campo "estado" indicando que el registro en cuestión "está de baja" ^^.

Para realizar un DELETE sólo necesitaremos el valor del campo clave del registro que queremos eliminar.

LINKAGE SECTION
01 ENTRADA.
   05 CAMPO-CLAVE PIC X.
(...)

EXEC SQL
   DELETE FROM TABLA3
    WHERE CAMPO1 = :CAMPO-CLAVE
END-EXEC


Validaremos el SQLCODE devuelto y devolveremos el control al programa llamante.

Os dejo los enlaces a los ejemplos completos de SELECT, INSERT, UPDATE y DELETE.

viernes, 3 de diciembre de 2010

LOAD y UNLOAD: carga y descarga.



LOAD

La utilidad "LOAD" se utiliza para realizar "cargas" de datos a una tabla DB2.
La carga se realizará desde un fichero que puede venir con el formato de la tabla o no.

La estructura general de un paso de LOAD sería algo así:

//*================================================
//* CARGA DE LA TABLA TBPRU00
//* Donde los ficheros SORTWKXX son temporales para
//* ordenar la entrada
//* el SYSREC es el fichero con los datos para la carga
//* y el SORTOUT es temporal para ordenar la salida
//*======================================================
//LOADTBL EXEC DSNUPROC,SYSTEM=DSNP,UID=LOADIND
//DSNUPROC.SORTWK01 DD DSN=&&SORTWK01,
//    DISP=(NEW,DELETE,DELETE),
//    SPACE=(4000,(1000,700),,,ROUND),
//    UNIT=SYSDA
//DSNUPROC.SORTWK02 DD DSN=&&SORTWK02,
//    DISP=(NEW,DELETE,DELETE),
//    SPACE=(4000,(1000,700),,,ROUND),
//    UNIT=SYSDA
//DSNUPROC.SORTWK03 DD DSN=&&SORTWK03,
//    DISP=(NEW,DELETE,DELETE),
//    SPACE=(4000,(1000,700),,,ROUND),
//    UNIT=SYSDA
//DSNUPROC.SORTWK04 DD DSN=&&SORTWK04,
//    DISP=(NEW,DELETE,DELETE),
//    SPACE=(4000,(1000,700),,,ROUND),
//    UNIT=SYSDA
//DSNUPROC.SYSUT1 DD DSN=&&SYSUT1,
//    DISP=(NEW,DELETE,DELETE),
//    SPACE=(4000,(5000,2000),,,ROUND),
//    UNIT=SYSDA
//DSNUPROC.SYSREC DD DSN=XXXX.LOAD.TABLA,DISP=SHR
//DSNUPROC.SORTOUT DD DSN=&&SORT1,UNIT=SYSDA,
//    SPACE=(4000,(5000,2000),,,ROUND)
//SYSIN DD *


1. Vamos a ver en primer lugar el caso más sencillo, con un fichero de carga que tenga el mismo formato que la tabla:
//SYSIN DD *
LOAD


A continuación tendríamos varias opciones:
//*Si vamos a añadir registros
RESUME YES
INTO TABLE TBPRU00
LOG NO NOCOPYPEND
/*


//*Si vamos a reemplazar registros (se borrarán todos los que haya)
RESUME NO REPLACE
INTO TABLE TBPRU00
LOG NO NOCOPYPEND
/*


Utilizamos la opción RESUME para indicar si vamos a cargar datos en una tabla vacía o no:
RESUME YES: La tabla no está vacía, añadimos los registros de nuestro fichero.
RESUME NO: La tabla está vacía. Por si no lo estuviese, indicamos con REPLACE que reemplace los registros existentes por los de nuestro fichero.
La opción NOCOPYPEND se utiliza para quitar el estado COPY de la tabla y que quede accesible después de la LOAD.

2. Para el caso en que el fichero no tenga el mismo formato que la tabla tendríamos un paso de JCL similar, pero después de indicar el nombre de la tabla, indicaríamos para cada campo de la tabla, las posiciones que tiene el mismo campo en el fichero, y le daríamos el formato adecuado.

Vamos a suponer que nuestra tabla tiene el siguiente formato:
CAMPO1 S9(9) COMP-3.
CAMPO2 X(30).
CAMPO3 X(60).
CAMPO4 DATE
CAMPO5 S9(9) COMP-3.


Y que nuestro fichero tiene este otro:

Descripción campo        Posición
CAMPO1 PIC S9(9) COMP-3. 1
CAMPO2 PIC X(2).         6
CAMPO3 PIC X(1).         8
CAMPO4 PIC X(10).        9
CAMPO5 PIC 9(8).         19


En nuestra LOAD quedaría:
//SYSIN DD *
LOAD
RESUME NO REPLACE
INTO TABLE TBPRU00
(CAMPO1 POSITION(1:5) DECIMAL
,CAMPO2 POSITION(6:7) CHAR (2)
,CAMPO3 POSITION(8) CHAR (1)
,CAMPO4 POSITION(9:18) DATE EXTERNAL(10)
,CAMPO5 POSITION(19:26) DECIMAL EXTERNAL
)
LOG NO NOCOPYPEND


Un JCL completo podría ser:

//*===============================================
//* CARGA DE LA TABLA TBPRU00 *
//*===============================================
//LOADTBL EXEC DSNUPROC,SYSTEM=DSN1,UID=LOADIND
//DSNUPROC.SORTWK01 DD DSN=&&SORTWK01,
//    DISP=(NEW,DELETE,DELETE),
//    SPACE=(4000,(1000,700),,,ROUND),
//    UNIT=SYSDA
//DSNUPROC.SORTWK02 DD DSN=&&SORTWK02,
//    DISP=(NEW,DELETE,DELETE),
//    SPACE=(4000,(1000,700),,,ROUND),
//    UNIT=SYSDA
//DSNUPROC.SORTWK03 DD DSN=&&SORTWK03,
//    DISP=(NEW,DELETE,DELETE),
//    SPACE=(4000,(1000,700),,,ROUND),
//    UNIT=SYSDA
//DSNUPROC.SORTWK04 DD DSN=&&SORTWK04,
//    DISP=(NEW,DELETE,DELETE),
//    SPACE=(4000,(1000,700),,,ROUND),
//    UNIT=SYSDA
//DSNUPROC.SYSUT1 DD DSN=&&SYSUT1,
//    DISP=(NEW,DELETE,DELETE),
//    SPACE=(4000,(5000,2000),,,ROUND),
//    UNIT=SYSDA
//DSNUPROC.SYSREC DD DSN=XXXX.LOAD.TABLA,DISP=SHR
//DSNUPROC.SORTOUT DD DSN=&&SORT1,UNIT=SYSDA,
//    SPACE=(4000,(5000,2000),,,ROUND)
//SYSIN DD *
LOAD
RESUME YES
INTO TABLE TBPRU00
LOG NO NOCOPYPEND
/*


Otros pasos útiles/opcionales cuando hacemos una LOAD:

Quitar estado COPY-pending
//*===================================================*
//* ANULAR COPY Y RECOVERY PENDIENTES *
//*==================================================*
//REPMES EXEC DSNUPROC,UID=TSPRU00
//SYSIN DD *
REPAIR SET TABLESPACE DBPRU.TSPRU00 NORCVRPEND
REPAIR SET TABLESPACE DBPRU.TSPRU00 NOCOPYPEND
/*


O también
//****************************************************
//* ABRE PARA R/W
//****************************************************
//ARRANGEN EXEC PGM=IKJEFT01,DYNAMNBR=20
//SYSTSPRT DD SYSOUT=*
//SYSPRINT DD SYSOUT=*
//SYSOUT DD SYSOUT=*
//SYSUDUMP DD SYSOUT=4,DEST=JESTC3
//SYSTSIN DD *
DSN SYSTEM(DSN1)
-START DB(DBPRU) SPACE(TSPRU00) ACCESS(FORCE) <--tablespace

-START DB(DBPRU) SPACE(INPRU001) ACCESS(FORCE) <--índice /*

Poner tablespace/índices a UT (sólo permite acceder a la tabla a las utilitys)
//************************************************
//* STO: STOP STA: START
//******************************************************
//DSNTIAD1 EXEC PGM=IKJEFT01,DYNAMNBR=20
//SYSTSPRT DD SYSOUT=*
//SYSPRINT DD SYSOUT=*
//SYSOUT DD SYSOUT=*
//SYSTSIN DD *
DSN SYSTEM (DSN1)
-STO DATABASE(EXAMP001) SPACENAM(EXAMPTLF)
-STA DATABASE(EXAMP001) SPACENAM(EXAMPTLF) ACCESS(UT)
-STO DATABASE(EXAMP001) SPACENAM(IXAMPTLF)
-STA DATABASE(EXAMP001) SPACENAM(IXAMPTLF) ACCESS(UT)
END
/*


Poner tablespaces/índices a RW
//*************************************************
//* REALIZA UN START DE LECTURA/ESCRITURA SOBRE LA TABLA
//*******************************************************
//ARRANGEN EXEC PGM=IKJEFT01,DYNAMNBR=20
//SYSTSPRT DD SYSOUT=*
//SYSPRINT DD SYSOUT=*
//SYSOUT DD SYSOUT=*
//SYSUDUMP DD SYSOUT=4,DEST=JESTC3
//SYSTSIN DD *
DSN SYSTEM (DSN1)
-STA DATABASE(EXAMP001) SPACENAM(EXAMPTLF) ACCESS(RW)
-STA DATABASE(EXAMP001) SPACENAM(IXAMPTLF) ACCESS(RW)
END
/*


Es recomendable hacer siempre una descarga previa, por lo que pueda pasar :P

UNLOAD

La utilidad "UNLOAD" sirve para realizar "descargas" de información de tablas.

La estructura general de un paso de UNLOAD sería algo así:

//*=================================================*
//* DESCARGA DE LA TABLA TBPRU00 *
//* Donde SYSPRINT contendrá mensajes de error
//* SYSPUNCH las sentencias de LOAD para una posterior
//* recarga de datos
//* y SYSRECnn es el fichero donde guardaremos la información
//*=======================================================*
//UNLOAD EXEC PGM=IKJEFT01,DYNAMNBR=20
//SYSTSPRT DD SYSOUT=*
//SYSTSIN DD *
DSN SYSTEM(DSN1)
RUN PROGRAM(DSNTIAUL) PLAN(DSNTIAUL) -
LIB('LIBRERIA.DE.TU.INSTALACION')
//SYSPRINT DD SYSOUT=*
//SYSUDUMP DD SYSOUT=*
//SYSPUNCH DD SYSOUT=*
//SYSREC00 DD DSN=XXXX.UNLOAD.TBPRU00,
//    DISP=(NEW,CATLG,DELETE),UNIT=SYSDA,
//    SPACE=(CYL,(100,50),RLSE)
//SYSIN DD *


En la SYSIN escribiremos la query para seleccionar los datos que queremos descargar. Por ejemplo:
SELECT *
FROM DSN1.TBPRU00 <-- El prefijo cambia según la instalación

WHERE CAMPO1 = 112 ORDER BY CAMPO2 ;

Esta SELECT se puede complicar tanto como queramos (SUBSELECTs, JOINs, etc).

Por supuesto, lo mejor en estos casos es buscar un JCL de carga o descarga que ya exista en tu entorno de explotación, y adaptarlo a tus necesidades ^^