Capturar errores de usuario en la base de datos

Hoy vamos a ver como facilitarnos un poco el control del código de aplicación de la base de datos.

En muchas ocasiones, los desarrolladores no llevan a cabo correctamente la captura de errores , lo que produce una serie quejas «no me va y no me da error».
Para lidiar con estas situaciones podemos crear un pequeño trigger que registre en la base de datos que y quien genera errores en la base de datos, para poder seguirlos y depurar estos errores.

Vamos a suponer que:

  • Tenemos un tablespace específico llamado CONTROL donde guardaremos nuestros datos
  • Tenemos un usuario llamado CONTROL que será el propietario del código

Primero crearemos la tabla que contendrán los errores:

-- Creamos la tabla 
create table CONTROL.oraerror (
identificador   NUMBER,
FECHA           DATE,
ESQUEMA         VARCHAR2(30),
USUARIO         VARCHAR2(50),
SQL_ID          VARCHAR2 (13) DEFAULT NULL,
SQL_HASH_VALUE  NUMBER DEFAULT NULL,
err_nr          NUMBER(10),
err_msg         VARCHAR2(4000),
stmt            CLOB
) TABLESPACE CONTROL;

--
-- Creamos una secuencia para el oraerror
create sequence CONTROL.oraerror_seq
start with 1
increment by 1
minvalue 1
nomaxvalue
nocache
nocycle;

Y después crearemos nuestro trigguer

CREATE OR REPLACE TRIGGER CONTROL.after_error
 AFTER SERVERERROR ON DATABASE
 DECLARE
 id             NUMBER;
 vSerial        NUMBER DEFAULT NULL;
 ESQUEMA        VARCHAR2(30);
USUARIO         VARCHAR2(50);
 SQL_ID         VARCHAR2 (13) DEFAULT NULL;
 SQL_HASH_VALUE NUMBER DEFAULT NULL;
  sql_text      ORA_NAME_LIST_T;
 v_stmt         CLOB;
 n              NUMBER;
 vsid           NUMBER DEFAULT NULL;
BEGIN
 -- BUscamos ID 
SELECT dbms_debug_jdwp.current_session_serial serial# INTO vSerial 
      FROM DUAL;
SELECT oraerror_seq.nextval,SYS_CONTEXT ('USERENV', 'sid') INTO  id, vsid
     FROM dual;
SELECT  SQL_ID, SQL_HASH_VALUE,USERNAME,OSUSER INTO SQL_ID, SQL_HASH_VALUE,ESQUEMA,USUARIO
   FROM sys.v_$session   WHERE sid = vsid AND serial# = vSerial;
n:= ora_sql_txt(sql_text);

IF n >= 1
 THEN
 FOR i IN 1..n LOOP
 v_stmt := v_stmt || sql_text(i);
 END LOOP;
END IF;
--
 FOR n IN 1..ora_server_error_depth LOOP
      INSERT  /*+ APPEND */ 
      INTO CONTROL.oraerror VALUES(id, sysdate, ESQUEMA, USUARIO, SQL_ID,SQL_HASH_VALUE,  ora_server_error(n), ora_server_error_msg(n), v_stmt);
  END LOOP;
 --
END ;

Con esto tendrémos en nuestra tabla todos los errores que dela base de datos.
Hay que tener en cuenta que, muchos de los errores pueden ser correctos, como los errores :

  • ORA-01017: nombre de usuario/contraseña no válidos; conexión denegada
  • ORA-00001: unique constraint (XXXX) violated (error asumible en cargas masivas)
  • ORA-03113: end-of-file on communication channel ORA-03113: end-of-file on communication channel

Con lo que habrá que seguirlos con cuidado y ver en que caso son reprochables o asumibles

Parámetro compatible en las bases de datos

Hoy vamos a ver uno de los parámetros mas sencillos que hay pero que puede provocarnos algún que otro quebradero de cabeza. El parámetro «compatible»

Uno de los parámetros del init.ora de nuestras bases de datos es el de la compatibilidad.
Este parámetro afecta al funcionamiento interno de la base de datos afectando no solamente al modo de trabajo del optimizador sino también pudiendo afectar a la manera en la que Oracle maneja físicamente algunas estructuras de datos.

¿Cual es la principal consecuencia de esto?
Que NO es posible la vuelta atrás.
Desde Oracle 9i , la versión de base de datos está compuesta por 5 dígitos cuyo significado es:
Opciones del número

En nuestro afán de tener la base de datos al último nivel de funcionalidad podemos tender a subir siempre el nivel de compatibilidad al máximo.
Esta costumbre no solo la desaconsejamos aquí, sino que es una de los advices que dan desde Oracle «Use only the first 3 digits for the compatible parameter unless there would be some very specific instructions to do otherwise.».

La razón es que, como comentábamos al principio, el parámetro compatible no es algo que pueda deshacerse ya que afecta a la estructura física de la base de datos con lo que, en caso de tener que hacer marcha atrás hacia una versión compatible inferior deberá de hacerse un downgrade completo de la base de datos.

Para mas información, como siempre en soporte de Oracle:

  • About Oracle Database Release Numbers
  • Note 733987.1 How To Change The COMPATIBLE Parameter And What Is The Significance? (Doc ID 733987.1)
  • Note 1563364.1 What is the Relationship between the COMPATIBLE Initialization Parameter and the Optimizer (Doc ID 1563364.1)
  • Note 1458741.1 COMPATIBLE Parameter – Explanation, Usage and Advise (Doc ID 1458741.1)

Mover tablas con subparticiones

Supongamos que tenemos una tabla llamada AGENTE , esta tabla agente está particionada por número de agente, y a su vez, particionada por trimestre.
Así pues tendremos que la estructura es mas o menos
PARTICIONADO

Y queremos mover estas particiones a un nuevo tablespace (TS_NEW). Lo primero que se nos pasa por la cabeza es el ejecutar:

alter table PROPIETARIO.AGENTE move partition  AG1  tablespace TS_NEW;

Pero Oracle nos devolverá el error

Error at line 1
ORA-14257: no se puede mover una partición que no sea de Rango, Lista, Sistema o Hash

Lo primero que nos viene a la cabeza es el preguntarnos donde está el error e ir a comprobar que tipo de partición tenemos, sin embargo, si aplicamos un poco el sentido común, veremos que lo que nos está indicando este error es que debemos de mover nuestra tabla subparticion a subparticion .
La sintaxsis correcta será:

alter table PROPIETARIO.AGENTE 
	move subpartition  PRIMER1
			tablespace TS_NEW;
alter table PROPIETARIO.AGENTE 
	move subpartition  PRIMER2 
				tablespace TS_NEW;
alter table PROPIETARIO.AGENTE
		move subpartition
			PRIMER3 tablespace TS_NEW;

Esta tarea puede ser realmente tediosa si nuestra tabla tiene un gran número de particiones y subparticiones, con lo que, lo mejor será hacer un script para moverla.
Lo primero que ha de hacerse en estos casos, es guardar la manera de volver a la situacion actual, esto lo conseguiremos con la salida del comando

select 'alter table'||
	    table_owner||
		'.'||
		table_name||
		' move subpartition '
		||subpartition_name||
		' tablespace '
		|| tablespace_name||
		' ;'
     		from  DBA_TAB_SUBPARTITIONS
   			where table_name='TABLA'

Una vez hemos guardado esta salida, podemos pasar a generar el script que nos moverá todas las subparticiones al nuevo tablespace TS_NEW

select 'alter table '
		||table_owner||
		'.'
		||table_name||
		' move subpartition '
		||subpartition_name||
		' tablespace 
		TS_NEW;' 
		    from  DBA_TAB_SUBPARTITIONS 
			 where table_name='TABLA'

Como siempre, mas información en la documentación de Oracle

Mantenimiento de las tablas de auditoría SYS.AUD$

Hoy vamos a ver algo tan básico como es el mantenimiento de las tablas de auditoria. Los registros de auditoria de la base de datos es una de esas cosas que tienden a crecer indiscriminadamente llenándonos los tablespaces del sistema sin que nos demos cuenta. Hoy vamos a ver como limitar estos registros a los de los últimos 100 días (algo mas de 3 meses).

En las versiones anteriores (9i,10g ) el mantenimiento de las tablas de auditoria era un poco «por tu cuenta y riesgo», había documentación de como hacerlo pero decía que no era soportada ( por ejemplo la nota Note: 1019377.6 Script to move SYS.AUD$ table out of SYSTEM tablespace ).
Afortunadamente en la versión 11g Oracle ha creado el paquete DBMS_AUDIT_MGMT facilitándonos la labor.

Lo primero que tenemos que ver es donde se encuentran las tabas de auditoria, para ello usaremos la consulta

SELECT table_name, tablespace_name
FROM dba_tables
WHERE table_name IN ('AUD$', 'FGA_LOG$')
ORDER BY table_name;

TABLE_NAME                     TABLESPACE_NAME
------------------------------ ------------------------------
AUD$                           SYSTEM
FGA_LOG$                       SYSTEM

O con la llamada al paquete

column parameter_name format a30
column parameter_value format a20
SELECT * FROM dba_audit_mgmt_config_params;

PARAMETER_NAME            PARAMETER_VALUE      AUDIT_TRAIL
------------------------ ------------------ ----------------------
DB AUDIT TABLESPACE            TS_AUD         STANDARD AUDIT TRAIL
DB AUDIT TABLESPACE            TS_AUD         FGA AUDIT TRAIL
AUDIT FILE MAX SIZE            10000          OS AUDIT TRAIL
AUDIT FILE MAX SIZE            10000          XML AUDIT TRAIL
AUDIT FILE MAX AGE             5              OS AUDIT TRAIL
AUDIT FILE MAX AGE             5              XML AUDIT TRAIL
DB AUDIT CLEAN BATCH SIZE      10000          STANDARD AUDIT TRAIL
DB AUDIT CLEAN BATCH SIZE      10000          FGA AUDIT TRAIL
OS FILE CLEAN BATCH SIZE       1000           OS AUDIT TRAIL
OS FILE CLEAN BATCH SIZE       1000           XML AUDIT TRAIL

10 filas seleccionadas.

Por defecto, estas tablas estarán en el tablespace SYSTEM, es muy importante el mover estas tablas fuera de este tablespace si vamos a trabajar sobre ellas,ya uqe, sino Oracle usará el tablespace SYSAUX,algo que no queremos que pase bajo ningún concepto.
Lo primero que vamos a hacer es crear un tablespace para la auditoria, así que, vamos a ver el tamaño que necesitamos y a crear un tablespace con suficiente tamaño:

SQL> select sum(bytes)/1024/1024 Mb 
        from dba_segments 
          where segment_name in ('AUD$', 'FGA_LOG$');
-------
200,0625
SQL> create tablespace TS_AUD
   datafile '\oracle\oradata\instancia\ts_audit01.dbf' size 250M;

Una vez tengamos el tablespace creado, podemos usar la llamada DBMS_AUDIT_MGMT.set_audit_trail_location para mover las tablas actuales al nuevo tablespace

BEGIN
 DBMS_AUDIT_MGMT.set_audit_trail_location(
 audit_trail_type => DBMS_AUDIT_MGMT.AUDIT_TRAIL_AUD_STD,
 audit_trail_location_value => 'TS_AUD');
END;
 
BEGIN
 DBMS_AUDIT_MGMT.set_audit_trail_location(
 audit_trail_type => DBMS_AUDIT_MGMT.AUDIT_TRAIL_FGA_STD,
 audit_trail_location_value => 'TS_AUD');
END;

Con estos pasos, ya tenemos las tablas de auditoria en un tablespace especifico llamado TS_AUD, es en este momento cuando podemos comenzar con las tareas de mantenimiento de los registros de auditoría.

Como decíamos al principio, vamos ha configurar la base de datos para que se borren los registros anteriores a 100 días.

Lo primero que tendremos que hacer es inicializar el paquete con DBMS_AUDIT_MGMT.init_cleanup
En nuestro caso lo lanzaremos sobre AUDIT_TRAIL_AUD_STD Ya que lo que queremos limpiar son los registros de las tablas de auditoria de la base de datos, si quisiéramos limpiar otro (por ejemplo los del S.O seguiríamos la tabla Audit Trail Types)

BEGIN
 DBMS_AUDIT_MGMT.init_cleanup(
 audit_trail_type => DBMS_AUDIT_MGMT.AUDIT_TRAIL_AUD_STD,
 default_cleanup_interval => 24 /* horas*/);
END;

Tras esto, con la propiedad SET_LAST_ARCHIVE_TIMESTAMP indicamos cual es la fecha del ultimo registro que queremos guardar.(en nuestro caso borraremos todo lo anterior a 100 días)

BEGIN
  DBMS_AUDIT_MGMT.SET_LAST_ARCHIVE_TIMESTAMP(
    audit_trail_type  => DBMS_AUDIT_MGMT.AUDIT_TRAIL_AUD_STD,
    last_archive_time => SYSTIMESTAMP-100 /*100 días*/);
END;

Antes de ejecutar el trabajo, vamos a ver cual es el registro más antiguo:

SQL> select min(ntimestamp#) from sys.aud$;
-------------------------------------------
17/10/13 00:00:50,119000

Ejecutamos el purgado con la llamada

BEGIN
  DBMS_AUDIT_MGMT.CLEAN_AUDIT_TRAIL(
   audit_trail_type=> DBMS_AUDIT_MGMT.AUDIT_TRAIL_AUD_STD,
   use_last_arch_timestamp => TRUE);
END;

Tras ejecutar esto, el último registro debería de corresponderse con la fecha SYSDATE-100.
Si lo comprobamos tendremos que

SQL> select to_date(SYSTIMESTAMP-100) from dual 
--------
11/11/13
SQL> select min(ntimestamp#) from sys.aud$;
------------------------------------------
11/11/13 16:46:08,889000

Con lo que, efectivamente, habremos borrado todos los registros anteriores a 100 días.

A partir de ahora, podemos, o bien llevar a cabo los borrados de manera puntual con la llamada anterior, o planificarlos con en un job de purgado con la llamada

BEGIN
  DBMS_AUDIT_MGMT.CREATE_PURGE_JOB(
    audit_trail_type  => DBMS_AUDIT_MGMT.AUDIT_TRAIL_AUD_STD,
    audit_trail_purge_interval => 24 /* horas */,  
    audit_trail_purge_name => 'PURGADO_DE_TABLAS_AUDITORIA',
    use_last_arch_timestamp=> TRUE);
END;

Como siempre, podemos encontrar mas información en soporte Oracle en las notas :
Documentación del paquete DBMS_AUDIT_MGMT
Note 72460.1 Moving AUD$ to Another Tablespace and Adding Triggers to AUD$
Note: 1019377.6 Script to move SYS.AUD$ table out of SYSTEM tablespace
Note: 166301.1 How to Reorganize SYS.AUD$ Table
Note: 731908.1 New Feature DBMS_AUDIT_MGMT To Manage And Purge Audit Information
Note: 73408.1 How to Truncate, Delete, or Purge Rows from the Audit Trail Table SYS.AUD$