viernes, 19 de diciembre de 2008

Estas bromeando, ¿afinar sin tener el código fuente?

Es sabido por todo DBA que los problemas de desempeño son en gran medida atribuibles a código ineficiente, pero allí nos tienen recibiendo las quejas por lo lenta que es la base de datos; no nos queda más que encontrar este código y proceder a corregirlo, pero ¿qué podemos hacer si no tenemos siquiera acceso al código fuente? Lo más que podíamos hacer era hacer ajustes a las estadísticas o a algunos parámetros, quizás crear algún índice, pero nada más, afortunadamente con Oracle 10g se nos presenta una poderosa herramienta para situaciones como ésta y como siempre, nada mejor para entender cómo funciona que mediante un ejemplo práctico.

Imaginemos que luego de hacer las inspecciones del caso encontramos que el query problemático es el siguiente:

select e.ename, d.dname 
from emp e, dept d 
where e.deptno = to_number(d.deptno)

--------------------------------------------------------------
| Id | Operation          | Name | Rows | Bytes | Cost (%CPU)|
--------------------------------------------------------------
|  0 | SELECT STATEMENT   |      |   14 |   308 |     7  (15)|
|* 1 |  HASH JOIN         |      |   14 |   308 |     7  (15)|
|  2 |   TABLE ACCESS FULL| DEPT |    4 |    52 |     3   (0)|
|  3 |   TABLE ACCESS FULL| EMP  |   14 |   126 |     3   (0)|
--------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - access("E"."DEPTNO"=TO_NUMBER(TO_CHAR("D"."DEPTNO")))

La tabla dept tiene como clave primaria a la columna deptno, pero el índice asociado no se está usando debido a que se le está aplicando el operador to_number a dicha columna. Si tuviésemos acceso al código procederíamos a remover este operador para que quede algo así:

select e.ename, d.dname 
from emp e, dept d 
where e.deptno = d.deptno

--------------------------------------------------------------------------
| Id | Operation                    | Name    | Rows | Bytes | Cost(%CPU)|
--------------------------------------------------------------------------
|  0 | SELECT STATEMENT             |         |   14 |   308 |     4  (0)|
|  1 |  NESTED LOOPS                |         |   14 |   308 |     4  (0)|
|  2 |   TABLE ACCESS FULL          | EMP     |   14 |   126 |     3  (0)|
|  3 |   TABLE ACCESS BY INDEX ROWID| DEPT    |    1 |    13 |     1  (0)|
|* 4 |    INDEX UNIQUE SCAN         | PK_DEPT |    1 |       |     0  (0)|
--------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   4 - access("E"."DEPTNO"="D"."DEPTNO")

Como no tenemos acceso al código lo más que podríamos hacer es crear un índice funcional para to_number(deptno), pero coincidirán en que es una solución poco elegante y eficiente, más aun cuando Oracle nos ofrece la solución perfecta por medio del nuevo package
dbms_advanced_rewrite, el cual nos permite cambiar una sentencia por otra de forma transparente.

Para empezar a usarlo debemos en primer lugar otorgar los privilegios requeridos al usuario que emplearemos para hacer este mapeo, que para nuestro ejemplo será el usuario scott.

SYS@orcl> grant execute on sys.dbms_advanced_rewrite to scott;

Grant succeeded.

SYS@orcl> grant create materialized view to scott;

Grant succeeded.

A continuación hacemos uso del procedimiento declare_rewrite_equivalence al interior de dbms_advanced_rewrite, debemos proporcionar un nombre único, la sentencia original y la sentencia por la cual deseamos cambiarla.

SCOTT@orcl> begin
  2  sys.dbms_advanced_rewrite.declare_rewrite_equivalence(
  3    name => 'use_index',
  4    source_stmt => 'select e.ename, d.dname from emp e, dept d 
  5      where e.deptno = to_number(d.deptno)',
  6    destination_stmt => 'select e.ename, d.dname from emp e, dept d 
  7      where e.deptno = d.deptno');
  8  end;
  9  /

PL/SQL procedure successfully completed.

Deben tener en cuenta que ambas sentencias deben coincidir en la cantidad de columnas y en los tipos de datos o de otra forma tendrian un error similar a este:

ERROR at line 1:
ORA-30390: the source statement is not equivalent to the destination statement
ORA-06512: at "SYS.DBMS_ADVANCED_REWRITE", line 29
ORA-06512: at "SYS.DBMS_ADVANCED_REWRITE", line 185
ORA-06512: at line 2

Pues bien, veamos ahora si es verdad tanta belleza, para lo cual ejecutamos nuevamente el código original.

SCOTT@orcl> select e.ename, d.dname from emp e, dept d
  2  where e.deptno = to_number(d.deptno);

ENAME      DNAME
---------- --------------
SMITH      RESEARCH
ALLEN      SALES
WARD       SALES
JONES      RESEARCH
MARTIN     SALES
BLAKE      SALES
CLARK      ACCOUNTING
SCOTT      RESEARCH
KING       ACCOUNTING
TURNER     SALES
ADAMS      RESEARCH
JAMES      SALES
FORD       RESEARCH
MILLER     ACCOUNTING

14 rows selected.

--------------------------------------------------------------
| Id | Operation          | Name | Rows | Bytes | Cost (%CPU)|
--------------------------------------------------------------
|  0 | SELECT STATEMENT   |      |   14 |   308 |     7  (15)|
|* 1 |  HASH JOIN         |      |   14 |   308 |     7  (15)|
|  2 |   TABLE ACCESS FULL| DEPT |    4 |    52 |     3   (0)|
|  3 |   TABLE ACCESS FULL| EMP  |   14 |   126 |     3   (0)|
--------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - access("E"."DEPTNO"=TO_NUMBER(TO_CHAR("D"."DEPTNO")))

Como diría Condorito: ¡exijo una explicación! Simple, el parámetro query_rewrite_integrity controla las posibilidades de reemplazar una sentencia por otra, y por defecto tiene el valor enforced y para nuestro caso es requerido que sea trusted. Hagamos el cambio y reintentemos con nuestro query anterior.

SCOTT@orcl> alter session set query_rewrite_integrity=trusted;

Session altered.

SCOTT@orcl> select e.ename, d.dname from emp e, dept d 
  2  where e.deptno = to_number(d.deptno);

ENAME      DNAME
---------- --------------
SMITH      RESEARCH
ALLEN      SALES
WARD       SALES
JONES      RESEARCH
MARTIN     SALES
BLAKE      SALES
CLARK      ACCOUNTING
SCOTT      RESEARCH
KING       ACCOUNTING
TURNER     SALES
ADAMS      RESEARCH
JAMES      SALES
FORD       RESEARCH
MILLER     ACCOUNTING

14 rows selected.

--------------------------------------------------------------------------
| Id | Operation                    | Name    | Rows | Bytes | Cost(%CPU)|
--------------------------------------------------------------------------
|  0 | SELECT STATEMENT             |         |   14 |   308 |     4  (0)|
|  1 |  NESTED LOOPS                |         |   14 |   308 |     4  (0)|
|  2 |   TABLE ACCESS FULL          | EMP     |   14 |   126 |     3  (0)|
|  3 |   TABLE ACCESS BY INDEX ROWID| DEPT    |    1 |    13 |     1  (0)|
|* 4 |    INDEX UNIQUE SCAN         | PK_DEPT |    1 |       |     0  (0)|
--------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   4 - access("E"."DEPTNO"="D"."DEPTNO")

¡Finalmente funcionó!, vemos como Oracle ha reemplazado un query por otro y ahora sí se usa la clave primaria de la tabla dept. Pero qué tal si éste no es el único código con problemas sino que hay otros similares, ¿se hará o no la transformación? pues hagamos la prueba.

SCOTT@orcl> select e.ename, d.dname from emp e, dept d 
  2  where e.deptno = to_number(d.deptno)
  3  and e.ename = 'KING';

ENAME      DNAME
---------- --------------
KING       ACCOUNTING

---------------------------------------------------------------
| Id | Operation          | Name | Rows  | Bytes | Cost (%CPU)|
---------------------------------------------------------------
|  0 | SELECT STATEMENT   |      |     1 |    22 |     7  (15)|
|* 1 |  HASH JOIN         |      |     1 |    22 |     7  (15)|
|* 2 |   TABLE ACCESS FULL| EMP  |     1 |     9 |     3   (0)|
|  3 |   TABLE ACCESS FULL| DEPT |     4 |    52 |     3   (0)|
---------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - access("E"."DEPTNO"=TO_NUMBER(TO_CHAR("D"."DEPTNO")))
   2 - filter("E"."ENAME"='KING')

¡Ya no funciona! ¿Quiere decir que tendremos que crear tantos mapeos como sentencias hayan? Afortunadamente no, es cuestión de crear el mapeo indicándole a Oracle que no sea tan estricto, para ello usamos el parámetro rewrite_mode cambiándolo de su valor por defecto text_match por el valor general. Primero eliminemos el mapeo que habíamos creado y lo creamos nuevamente con el ajuste requerido.

SCOTT@orcl> execute sys.dbms_advanced_rewrite.drop_rewrite_equivalence('use_index')

PL/SQL procedure successfully completed.

SCOTT@orcl> begin
  2  sys.dbms_advanced_rewrite.declare_rewrite_equivalence(
  3    name => 'use_index',
  4    source_stmt => 'select e.ename, d.dname from emp e, dept d
  5      where e.deptno = to_number(d.deptno)',
  6    destination_stmt => 'select e.ename, d.dname from emp e, dept d
  7      where e.deptno = d.deptno',
  8    rewrite_mode => 'GENERAL');
  9  end;
 10  /

PL/SQL procedure successfully completed.

Ahora reintentamos con el query anterior.

SCOTT@orcl> select e.ename, d.dname from emp e, dept d
  2  where e.deptno = to_number(d.deptno)
  3  and e.ename = 'KING';

ENAME      DNAME
---------- --------------
KING       ACCOUNTING

---------------------------------------------------------------------------
| Id | Operation                    | Name    | Rows | Bytes | Cost (%CPU)|
---------------------------------------------------------------------------
|  0 | SELECT STATEMENT             |         |    1 |    22 |     4   (0)|
|  1 |  NESTED LOOPS                |         |    1 |    22 |     4   (0)|
|* 2 |   TABLE ACCESS FULL          | EMP     |    1 |     9 |     3   (0)|
|  3 |   TABLE ACCESS BY INDEX ROWID| DEPT    |    1 |    13 |     1   (0)|
|* 4 |    INDEX UNIQUE SCAN         | PK_DEPT |    1 |       |     0   (0)|
---------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("E"."ENAME"='KING')
   4 - access("E"."DEPTNO"="D"."DEPTNO")

¡Bien! Las cosas vuelven a la normalidad, Oracle hace nuevamente el cambio aún cuando el código proporcionado no es idéntico al indicado durante el mapeo, pero vayamos un poco más allá, hagamos la prueba usando bind variables, después de todo estamos conscientes de que usar literales usualmente no es una buena práctica, ¿seguirá Oracle haciendo el cambio?

SCOTT@orcl> variable ename varchar2(14)
SCOTT@orcl> execute :ename := 'KING'

PL/SQL procedure successfully completed.

SCOTT@orcl> select e.ename, d.dname from emp e, dept d 
  2  where e.deptno = to_number(d.deptno)
  3  and e.ename = :ename;

ENAME      DNAME
---------- --------------
KING       ACCOUNTING

---------------------------------------------------------------------------
| Id | Operation                    | Name    | Rows | Bytes | Cost (%CPU)|
---------------------------------------------------------------------------
|  0 | SELECT STATEMENT             |         |    1 |    22 |     4   (0)|
|  1 |  NESTED LOOPS                |         |    1 |    22 |     4   (0)|
|* 2 |   TABLE ACCESS FULL          | EMP     |    1 |     9 |     3   (0)|
|  3 |   TABLE ACCESS BY INDEX ROWID| DEPT    |    1 |    13 |     1   (0)|
|* 4 |    INDEX UNIQUE SCAN         | PK_DEPT |    1 |       |     0   (0)|
---------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("E"."ENAME"=:ENAME)
   4 - access("E"."DEPTNO"="D"."DEPTNO")

¡Perfecto! Todo está saliendo a pedir de boca, pero sigamos forzando la situación y démosle a Oracle un nuevo query para ponerlo a prueba.

SCOTT@orcl> variable empno number
SCOTT@orcl> execute :empno := 7839

PL/SQL procedure successfully completed.

SCOTT@orcl> select e.ename, d.dname from emp e, dept d 
  2  where e.deptno = to_number(d.deptno)
  3  and e.ename = :ename
  4  and e.empno = :empno;

ENAME      DNAME
---------- --------------
KING       ACCOUNTING

--------------------------------------------------------------------------
| Id | Operation                    | Name   | Rows | Bytes | Cost (%CPU)|
--------------------------------------------------------------------------
|  0 | SELECT STATEMENT             |        |    1 |    26 |     4   (0)|
|  1 |  NESTED LOOPS                |        |    1 |    26 |     4   (0)|
|* 2 |   TABLE ACCESS BY INDEX ROWID| EMP    |    1 |    13 |     1   (0)|
|* 3 |    INDEX UNIQUE SCAN         | PK_EMP |    1 |       |     0   (0)|
|* 4 |   TABLE ACCESS FULL          | DEPT   |    1 |    13 |     3   (0)|
--------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("E"."ENAME"=:ENAME)
   3 - access("E"."EMPNO"=TO_NUMBER(:EMPNO))
   4 - filter("E"."DEPTNO"=TO_NUMBER(TO_CHAR("D"."DEPTNO")))

¿Qué pasó? Esto ya parece una telenovela pero afortunadamente la explicación es bastante sencilla: para que la transformación pueda realizarse, se debe hacer referencia a las columnas que están en la sentencia original y si observamos con detenimiento, la columna empno no aparece en ella. Hagamos entonces un mapeo en el que si aparezca, y es más haremos que no sea explícito sino implícito.

SCOTT@orcl> execute sys.dbms_advanced_rewrite.drop_rewrite_equivalence('use_index')

PL/SQL procedure successfully completed.

SCOTT@orcl> begin
  2  sys.dbms_advanced_rewrite.declare_rewrite_equivalence (
  3    name => 'use_index',
  4    source_stmt => 'select e.*, d.dname from emp e, dept d
  5      where e.deptno = to_number(d.deptno)',
  6    destination_stmt => 'select e.*, d.dname from emp e, dept d 
  7      where e.deptno = d.deptno',
  8    rewrite_mode => 'GENERAL');
  9  end; 
 10  /

PL/SQL procedure successfully completed.

Ahora sí, al indicar e.* allí estará desde luego la columna empno, hagamos nuevamente la prueba.

SCOTT@orcl> select e.ename, d.dname from emp e, dept d 
  2  where e.deptno = to_number(d.deptno)
  3  and e.ename = :ename
  4  and e.empno = :empno;

ENAME      DNAME
---------- --------------
KING       ACCOUNTING

---------------------------------------------------------------------------
| Id | Operation                    | Name    | Rows | Bytes | Cost (%CPU)|
---------------------------------------------------------------------------
|  0 | SELECT STATEMENT             |         |    1 |    26 |     2   (0)|
|  1 |  NESTED LOOPS                |         |    1 |    26 |     2   (0)|
|* 2 |   TABLE ACCESS BY INDEX ROWID| EMP     |    1 |    13 |     1   (0)|
|* 3 |    INDEX UNIQUE SCAN         | PK_EMP  |    1 |       |     0   (0)|
|  4 |   TABLE ACCESS BY INDEX ROWID| DEPT    |    4 |    52 |     1   (0)|
|* 5 |    INDEX UNIQUE SCAN         | PK_DEPT |    1 |       |     0   (0)|
---------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("E"."ENAME"=:ENAME)
   3 - access("E"."EMPNO"=TO_NUMBER(:EMPNO))
   5 - access("E"."DEPTNO"="D"."DEPTNO")

Simplemente ¡magnífico!. No tuvimos que escribir en nuestro query e.* sino que escogimos algunas de las columnas de emp como son e.ename y e.empno y Oracle no encontró problemas en hacer la conversión.

Qué lástima, justo cuando la cosa se ponía interesante llegamos al final de esta demostración, pero espero que les haya motivado a investigar más sobre el tema y desde luego agreguen a dbms_advanced_rewrite a su arsenal de armas en la eterna guerra contra el código ineficiente, ¡suerte en la batalla!

Posts Relacionados:
Siga leyendo >>

¿Te pareció interesante este artículo?, ¿te quedaron algunas dudas?, ¿quieres sugerirme un tema a tratar?, pues déjame tus comentarios o envíame un email y para que NO te pierdas ningún Post, suscríbete por email ahora mismo!

martes, 2 de diciembre de 2008

Nuevas formas de rastrear el código

Una de las tareas habituales del DBA es hacer seguimiento al código, para ello normalmente se identificaba la sesión deseada y se habilitaba el trace para la misma, pero ahora que prevalecen las aplicaciones web, ¿cómo hacer seguimiento cuando existe un pool de conexiones que es compartido por todos los usuarios? Pues si hemos tomado la precaución de instrumentar nuestro código, tal como les recomendara en un Post previo, la tarea aún es posible y nada complicada, como veremos a continuación.

Primero estableceremos el escenario que nos servirá para demostrar cómo hacer seguimiento, para ellos asumiremos la existencia de un usuario de base de datos llamado "appweb", tendremos 2 sesiones en sql*plus desde las cuales ejecutaremos un par de scripts que contienen el código que nos interesa capturar.

script1.sql
declare
   v_dummy number;
begin
   dbms_session.set_identifier( client_id => 'juan.perez' );
   dbms_application_info.set_module(
      module_name => 'Agregacion',
      action_name => 'Empleados');

   select count(*)
     into v_dummy
     from emp;

   dbms_application_info.set_module(
      module_name => 'Agregacion',
      action_name => 'Departamentos');

   select count(*)
     into v_dummy
     from dept;

   dbms_application_info.set_module(
      module_name => NULL,
      action_name => NULL );
   dbms_session.set_identifier( client_id => NULL );
end;
/

script2.sql
declare
   v_dummy varchar2(20);
begin
   dbms_session.set_identifier( client_id => 'john.doe' );
   dbms_application_info.set_module(
      module_name => 'Primeros',
      action_name => 'Empleados');

   select ename
     into v_dummy
     from emp
    where rownum < 2;

   dbms_application_info.set_module(
      module_name => 'Primeros',
      action_name => 'Departamentos');

   select dname
     into v_dummy
     from dept
    where rownum < 2;

   dbms_application_info.set_module(
      module_name => NULL,
      action_name => NULL );
   dbms_session.set_identifier( client_id => NULL );
end;
/

El uso de dbms_application_info ya nos es conocido, si no es así te recomiendo leer primero este Post en el cual lo detallo; la novedad viene a ser el uso de dbms_session.set_identifier, que sirve para asignar una cadena de hasta 64 caracteres y que es visible en v$session bajo la columna client_identifier.

SYS@orcl > execute dbms_session.set_identifier( 'Juan.Perez' )

PL/SQL procedure successfully completed.

SYS@orcl > select username, client_identifier 
  2  from v$session where client_identifier is not null;

USERNAME CLIENT_IDENTIFIER
-------- -----------------
SYS      Juan.Perez

En una aplicación web, cada vez que un usuario desea interactuar con la base de datos, se solicita una conexión disponible del pool de conexiones y una vez obtenida se ejecuta el código SQL, pues bien, para poder identificar al usuario, es necesario agregar una llamada a dbms_session.client_identifier, tal como pueden ver en el código de script1 y script2.

Si deseamos hacer trace al usuario 'juan.perez', que es un usuario de aplicación, no un usuario de la base de datos, podemos hacer uso del package dbms_monitor. Este package aparece con 10g y ha venido a reemplazar a dbms_system y dbms_support, por lo que si aún los usan les recomiendo dejar de hacerlo.

SYS@orcl > execute dbms_monitor.client_id_trace_enable( 'juan.perez' )

PL/SQL procedure successfully completed.

Ahora simularemos la interacción de una aplicación web, ejecutando los scripts desde dos sesiones establecidas con el usuario appweb, que vendría a ser el usuario del pool de conexiones.

APPWEB(1)@orcl > @script1

PL/SQL procedure successfully completed.

APPWEB(2)@orcl > @script2

PL/SQL procedure successfully completed.

Ya que Oracle ha recibido la instrucción de hacer seguimiento al código que sea ejecutado por 'juan.perez', conforme esto ocurra se iran generando trace files en el directorio señalado en user_dump_dest. Aquí no podemos usar directamente tkprof, debemos ejecutar un paso previo que consiste en llamar a trcsess.

[udump]$ trcsess
oracle.ss.tools.trcsess.SessTrcException: SessTrc-00002: Session Trace Usage error: Wrong parameters passed.
trcsess [output=<output file name>]  [session=<session ID>] [clientid=<clientid>] [service=<service name>] [action=<action name>] [module=<module name>] <trace file names>

output=<output file name> output destination default being standard output.
session=<session Id> session to be traced.
Session id is a combination of session Index & session serial number e.g. 8.13.
clientid=<clientid> clientid to be traced.
service=<service name> service to be traced.
action=<action name> action to be traced.
module=<module name> module to be traced.
<trace_file_names> Space separated list of trace files with wild card '*' supported.

Vemos que trcess nos permite separar de entre muchos trace files las sentencias que vamos filtrando con los parámetros disponibles. Empezaremos por filtrar las sentencias ejecutadas por 'juan.perez'.

[udump]$ ls *trc
orcl_ora_6839.trc  orcl_ora_6901.trc
[udump]$ trcsess output=juan.perez.trc clientid='juan.perez' *trc
[udump]$ tkprof juan.perez.trc juan.perez.txt sys=no

TKPROF: Release 10.2.0.4.0 - Production on Mon Dec 1 08:10:21 2008

Copyright (c) 1982, 2007, Oracle.  All rights reserved.

juan.perez.txt
SELECT COUNT(*)
FROM
 EMP
SELECT COUNT(*)
FROM
 DEPT

El objetivo se ha cumplido, hemos logrado aislar las sentencias que corresponden a 'juan.perez', procedamos ahora a solicitar el seguimiento de las sentencias generadas por el módulo 'Primeros'.

SYS@orcl > execute dbms_monitor.serv_mod_act_trace_enable(-
> service_name => 'orcl.oracle.com',-
> module_name => 'Primeros' );

PL/SQL procedure successfully completed.

Ahora simularemos nuevamente la interacción con la base de datos y obtendremos de los trace files aquellas sentencias que nos interesan por corresponder a dicho módulo.

APPWEB(1)@orcl > @script1

PL/SQL procedure successfully completed.

APPWEB(2)@orcl > @script2

PL/SQL procedure successfully completed.

[udump]$ trcsess output=Primeros.trc module='Primeros' *trc
[udump]$ tkprof Primeros.trc Primeros.txt sys=no

TKPROF: Release 10.2.0.4.0 - Production on Mon Dec 1 08:37:37 2008

Copyright (c) 1982, 2007, Oracle.  All rights reserved.

Primeros.txt
SELECT ENAME
FROM
 EMP WHERE ROWNUM < 2

SELECT DNAME
FROM
 DEPT WHERE ROWNUM < 2

Las cosas marchan como se esperaba, trcess logró separar las sentencias que corresponden al módulo 'Primeros'. De manera similar lo podemos solicitar a nivel de acción, veamos el ejemplo para 'Departamentos'.

[udump]$ trcsess output=Departamentos.trc action='Departamentos' *trc
[udump]$ tkprof Departamentos.trc Departamentos.txt sys=no

TKPROF: Release 10.2.0.4.0 - Production on Mon Dec 1 08:41:19 2008

Copyright (c) 1982, 2007, Oracle.  All rights reserved.

Departamentos.txt
SELECT DNAME
FROM
 DEPT WHERE ROWNUM < 2

SELECT COUNT(*)
FROM
 DEPT

Hasta ahora todo bien y muy simple, veamos ahora un ejemplo final en el que se combinan varios criterios de filtrado.

[udump]$ trcsess output=john.doe.Empleados.trc clientid='john.doe' action='Empleados' *trc

[udump]$ tkprof john.doe.Empleados.trc john.doe.Empleados.txt sys=no

TKPROF: Release 10.2.0.4.0 - Production on Mon Dec 1 08:44:07 2008

Copyright (c) 1982, 2007, Oracle.  All rights reserved.

john.doe.Empleados.txt
SELECT ENAME
FROM
 EMP WHERE ROWNUM < 2

A estas alturas ya habrán notado la enorme ventaja que nos representa haber instrumentado las aplicaciones, así como el poder de dbms_monitor complementado con trcess. Ahora bien, conforme vamos agregando criterios de seguimiento, estos se van registrando en la base de datos y están disponibles con la vista dba_enabled_traces.

SYS@orcl > select trace_type, primary_id, qualifier_id1
  2  from dba_enabled_traces;

TRACE_TYPE       PRIMARY_ID        QUALIFIER_ID1
---------------- ----------------- -------------
CLIENT_ID        juan.perez
SERVICE_MODULE   orcl.oracle.com   Primeros

Recuerden que hacer seguimiento consume recursos, por lo que es conveniente que consulten dba_enabled_traces regularmente para ver que no esté activo algún seguimiento que ya no necesitan y de ser así procedan a desactivarlo.

SYS@orcl > execute dbms_monitor.client_id_trace_disable('juan.perez')

PL/SQL procedure successfully completed.

SYS@orcl > execute dbms_monitor.serv_mod_act_trace_disable(-
> service_name => 'orcl.oracle.com',-
> module_name => 'Primeros' );

PL/SQL procedure successfully completed.

En conclusión, para estos tiempos en que las aplicaciones web con sus pools de conexiones son la regla, la labor de seguimiento del código se puede complicar si no hemos tomado la previsión de instrumentar nuestras aplicaciones, de forma que sea fácil identificar el origen y ejecutante de las sentencias. Cuesta un poco de trabajo, pero deben planteárselo como un estándar a seguir para todo nuevo desarrollo. Aún me sorprende encontrar muchas instalaciones donde esta medida no se ha tomado y hacer segumiento resulta más complicado de lo que debiera.

No olviden tampoco que deben usar dbms_monitor, sencillamente no tiene reemplazo y junto con trcsess y tkprof son nuestros aliados en el seguimiento del código. Pero esto no es todo, en un siguiente Post veremos otros beneficios que podemos obtener si instrumentamos las aplicaciones, estén atentos.

Posts Relacionados:
Siga leyendo >>

¿Te pareció interesante este artículo?, ¿te quedaron algunas dudas?, ¿quieres sugerirme un tema a tratar?, pues déjame tus comentarios o envíame un email y para que NO te pierdas ningún Post, suscríbete por email ahora mismo!

viernes, 28 de noviembre de 2008

¿Cómo deshacernos de un cursor en el Shared Pool?

Seguramente en alguna oportunidad has necesitado que un procedimiento almacenado o una secuencia permanezca en el Shared Pool y has terminado usando el package dbms_shared_pool, mientras que si lo que deseabas era deshacerte de ellos, la única alternativa era recurrir a alter system flush shared_pool, sentencia que limpia todo el Shared Pool con lo que ello implica. La buena noticia es que con la aparición de 11g se introdujo un método adicional al package dbms_shared_pool, mismo que permite la eliminación selectiva de objetos residentes en el Shared Pool, pero ¿sabías que este método también está disponible en 10gR2?, pues así es, pero su uso requiere de un paso adicional, que explicaré a continuación con algunos ejemplos.

El método del cual estamos hablando es el procedimiento purge, que permite eliminar del Shared Pool cursores, secuencias, packages, triggers, etc. Como les indicara, apareció con 11g pero viene incluido también con 10.2.0.4, y para las versiones 10.2.0.2 y 10.2.0.3 lo puedes obtener como un patch, el 5614566, pero debes tener en cuenta que en esta forma no está disponible para todas las plataformas.

La mejor forma de aprender a usarlo es mediante algunos ejemplos, para ello empezaremos viendo la forma de eliminar un cursor específico, en este caso se trata del cursor asociado al siguiente query:

SELECT /*+ mi query */
       dname, count(*) employees
  FROM dept JOIN emp USING ( deptno )
 GROUP BY dname;

DNAME         EMPLOYEES
------------ ----------
ACCOUNTING            3
RESEARCH              5
SALES                 6

Para poder eliminar el curso del Shared Pool, primero debemos identificarlo y la forma más simple de lograrlo es consultando v$sql.

SYS@orcl > SELECT address, hash_value, executions
  2  FROM v$sql WHERE sql_text LIKE 'SELECT /*+ mi query */%';

ADDRESS  HASH_VALUE EXECUTIONS
-------- ---------- ----------
30996000 1856305823          1

Con esta información ya podemos hacer uso del procedimiento purge y luego verificamos repitiendo la consulta anterior.

SYS@orcl > exec dbms_shared_pool.purge('30996000, 1856305823','C');

PL/SQL procedure successfully completed.

SYS@orcl > /

ADDRESS  HASH_VALUE EXECUTIONS
-------- ---------- ----------
30996000 1856305823          1

¿Qué pasó? Pues que en Oracle 10gR2, no es posible usar directamente el procedimiento purge, primero hay que habilitar el evento asociado al bug que ha venido a solucionar, es decir el 5614566. Lo haremos a continuación y repetiremos el procedimiento anterior.

SYS@orcl > alter session set events '5614566 trace name context forever';

Session altered.

SYS@orcl > exec dbms_shared_pool.purge('30996000, 1856305823','C');

PL/SQL procedure successfully completed.

SYS@orcl > SELECT address, hash_value, executions
  2  FROM v$sql WHERE sql_text LIKE 'SELECT /*+ mi query */%';

no rows selected

Ahora sí funcionó todo a la perfección, logramos eliminar el cursor que deseábamos, sin tocar nada más del Shared Pool. Lo mismo se puede hacer con un procedimiento almacenado, solo que en lugar de alimentar la dirección que ocupa en el Shared Pool, es suficiente con proporcionar el nombre del mismo.

SCOTT@orcl > execute demo_prc

PL/SQL procedure successfully completed.

SYS@orcl > SELECT namespace, type, sharable_mem
  2  from v$db_object_cache
  3  where owner = 'SCOTT' and name = 'DEMO_PRC';

NAMESPACE        TYPE         SHARABLE_MEM
---------------- ------------ ------------
TABLE/PROCEDURE  PROCEDURE           13710

SYS@orcl > execute dbms_shared_pool.purge('SCOTT.DEMO_PRC','P')

PL/SQL procedure successfully completed.

SYS@orcl > /

NAMESPACE        TYPE         SHARABLE_MEM
---------------- ------------ ------------
TABLE/PROCEDURE  NOT LOADED              0

Algo similar ocurre si queremos deshacernos de una secuencia, solo basta con proporcionar el nombre del mismo.

SCOTT@orcl > select demo_seq.nextval from dual;

   NEXTVAL
----------
         1

SYS@orcl > SELECT namespace, type, sharable_mem
  2  from v$db_object_cache
  3  where owner = 'SCOTT' and name = 'DEMO_SEQ';

NAMESPACE        TYPE         SHARABLE_MEM
---------------- ------------ ------------
TABLE/PROCEDURE  SEQUENCE             1422

SYS@orcl > execute dbms_shared_pool.purge('SCOTT.DEMO_SEQ','Q')

PL/SQL procedure successfully completed.

SYS@orcl > /

NAMESPACE        TYPE         SHARABLE_MEM
---------------- -----------  ------------
TABLE/PROCEDURE  NOT LOADED              0

Con esto concluimos la presentación de este novedoso método, que de seguro nos ha de servir en muchas oportunidades, sobre todo cuando queramos deshacernos de aquellos cursores indeseables producto de algún problema con el bind peeking, tal como les comentara en un Post previo.

Para complementar lo aquí expuesto, les recomiendo la lectura del Note 457309.1 How To Flush an Object out the Library Cache, y el Note 751876.1 DBMS_SHARED_POOL.PURGE Is Not Working On 10.2.0.4.

Posts Relacionados:
Siga leyendo >>

¿Te pareció interesante este artículo?, ¿te quedaron algunas dudas?, ¿quieres sugerirme un tema a tratar?, pues déjame tus comentarios o envíame un email y para que NO te pierdas ningún Post, suscríbete por email ahora mismo!

sábado, 22 de noviembre de 2008

Hazte la vida más fácil, instrumenta tus aplicaciones

La instrumentación se refiere a la capacidad de supervisar y medir el rendimiento de una aplicación. Si nuestra aplicaciones están instrumentadas, en cualquier momento podremos saber qué rutina es la que consume más recursos, en qué porcentaje de avance está aquel proceso batch que parece no terminar, podríamos hacer trace a un módulo, independientemente del usuario que lo esté ejecutando, etc. Luego de varios años de venir realizando consultorías, he encontrado que muy pocos implementan una instrumentación de sus aplicaciones, quizás sea porque no saben que esto es posible, por ello a continuación les mostraré cómo hacerlo y los beneficios inmediatos que pueden lograrse con muy poco esfuerzo.

La pieza central para lograr la instrumentación de las aplicaciones viene a ser el package dbms_application_info. Empezaremos por conocer los valores que podemos establecer y los métodos principales que usaremos en nuestro proceso de instrumentación.

module_name
Es el nombre del módulo que está actualmente en ejecución. Se trata de una cadena de hasta 48 bytes, si es más larga se trunca silenciosamente.

action_name
Es el nombre de la acción vigente para el módulo vigente. Se trata de unan cadena de hastas 32 bytes, si es más larga se trunca silenciosamente.

set_module
Es el procedimiento que permite establecer el nombre del módulo o aplicación actual.
dbms_application_info.set_module(
   module_name=> 'Facturacion',
   action_name=> 'Obtencion.Numero.Factura' );

set_action
Es el precedimiento que permite establecer el nombre de la acción actual dentro del módulo actual.
dbms_application_info.set_action(
   action_name=> 'Calculo.Impuestos' );

set_session_longops
Es el precedimiento que permite registrar una fila en v$session_longops, como un medio para hacer seguimiento al progreso de las operaciones.
dbms_application_info.set_session_longops(
   rindex => v_rindex, --identificador de la fila en v$session_longops
   slno => v_slno, --para uso interno, no modificar
   op_name => 'Recalculo', --nombre que le asignamos a la tarea
   target_desc => 'Cheque', --descripción del objeto manipulado
   sofar => v_sofar, --indicador de cuanto trabajo ya se ha hecho
   totalwork => v_totalwork, --indicador del total de trabajo por hacer
   context => v_nro_cheque, --cualquier numero que se desee almacenar
   units => 'Cheques'); --unidad en que se representa sofar y totalwork

Veamos ahora un ejemplo en el que se combinan estos tres métodos y luego comentaremos lo que allí se hace.

DECLARE
   v_rindex    PLS_INTEGER;
   v_slno      PLS_INTEGER;
   v_totalwork NUMBER;
   v_sofar     NUMBER;
   v_obj       PLS_INTEGER;
   v_dname     dept.dname%TYPE;
BEGIN
   dbms_application_info.set_module (
      module_name => 'Listado.Empleados',
      action_name=> 'Total.Empleados' );

   v_rindex := dbms_application_info.set_session_longops_nohint;
   v_sofar := 0;

   SELECT count(*)
     INTO v_totalwork
     FROM emp;

   dbms_application_info.set_action (
      action_name=> 'Bucle.Empleados' );

   FOR c_emp IN (
      SELECT empno, ename, deptno
        FROM emp )
   LOOP
      SELECT dname INTO v_dname
        FROM dept
       WHERE deptno = c_emp.deptno;
      dbms_output.put_line(
          c_emp.empno||':'||c_emp.ename||':'||v_dname);

      v_sofar := v_sofar + 1;
      dbms_application_info.set_session_longops(
         rindex => v_rindex,
         slno => v_slno,
         op_name => 'Procesando Empleados',
         target_desc => 'Empleado',
         sofar => v_sofar,
         totalwork => v_totalwork,
         context => c_emp.empno,
         units => 'Empleados');

      dbms_lock.sleep( 2 ); --artificio para hacer lento el bucle
   END LOOP;

   dbms_application_info.set_module (
      module_name => '',
      action_name=> '' );
END;

En esta rutina se empieza por calcular el total de filas a procesar y por cada iteración vamos actualizando la fila creada en v$session_longops, de forma que podemos saber cuánto se ha avanzado y tener una estimación del tiempo restante de proceso.

SYS@orcl > SELECT message, ROUND(sofar/totalwork,2)*100 "%Avance",
  2  time_remaining falta, context empno
  3   from v$session_longops
  4  where time_remaining > 0;
MESSAGE
-----------------------------------------------------------
   %Avance      FALTA    CONTEXT
---------- ---------- ----------
Procesando Empleados: Empleado : 1 out of 14 Empleados done
         7         30       7369

SYS@orcl > /
MESSAGE
-----------------------------------------------------------
   %Avance      FALTA    CONTEXT
---------- ---------- ----------
Procesando Empleados: Empleado : 3 out of 14 Empleados done
        21         11       7521

SYS@orcl > /
MESSAGE
-----------------------------------------------------------
   %Avance      FALTA    CONTEXT
---------- ---------- ----------
Procesando Empleados: Empleado : 13 out of 14 Empleados done
        93          2       7902

SYS@orcl > /

no rows selected

También hemos identificando la rutina en sí (set_module) y las diferentes secciones al interior del mismo (set_action), esta información es visible desde algunas vistas del catálogo, como es el caso de v$sql.

SYS@orcl > SELECT action, executions, sql_text
  2  FROM v$sql
  3* WHERE module = 'Listado.Empleados';
ACTION               EXECUTIONS SQL_TEXT
-------------------- ---------- -----------------------------------------
Bucle.Empleados               1 SELECT EMPNO, ENAME, DEPTNO FROM EMP
Total.Empleados               1 SELECT COUNT(*) FROM EMP
Nombre.Departamento          14 SELECT DNAME FROM DEPT WHERE DEPTNO = :B1

Ahora la labor de hallar el origen de una sentencia SQL se simplifica pues podemos identificar rápidamente el módulo y la acción de la cual proviene, de otra forma tendriamos que enviar la sentencia SQL al equipo de desarrollo para que ellos a su vez identifiquen la aplicación de la cual proviene, labor cuyo grado de dificultad depende del nivel de control que se tenga sobre el código fuente.

Si todas nuestras aplicaciones están debidamente instrumentadas, en todo momento podemos saber cómo los diversos módulos y acciones consumen los recursos, para ellos nos valemos adicionalmente de v$active_session_history.

SYS@orcl > SELECT b.name service, NVL(module,'Sin Nombre') module,
  2  ROUND(100*(COUNT(*)/sum(COUNT(*)) OVER()), 1) pct
  3  FROM v$active_session_history a, v$services b
  4  WHERE a.service_hash = b.name_hash
  5  AND session_type <> 'BACKGROUND'
  6  AND sample_time > systimestamp - 1/288 --ultimos 5 minutos
  7  GROUP BY b.name, module
  8* ORDER BY pct desc;
SERVICE          MODULE               PCT
---------------- ------------------ ------
SYS$USERS        Facturacion           53
SYS$USERS        Almacenes           44.1
orcl.oracle.com  OEM.DefaultPool      1.9
orcl.oracle.com  OEM.Loader            .6
orcl.oracle.com  Sin Nombre            .4

Observamos el detalle a nivel de module pero también es posible obtenerlo a nivel de action.
SYS@orcl > SELECT b.name service, NVL(module,'Sin Nombre') module,
  2  NVL(action,'Sin Nombre') action,
  3  ROUND(100*(COUNT(*)/sum(COUNT(*)) OVER()), 1) pct
  4  FROM v$active_session_history a, v$services b
  5  WHERE a.service_hash = b.name_hash
  6  AND session_type <> 'BACKGROUND'
  7  AND sample_time > systimestamp - 1/288
  8  GROUP BY b.name, module, action
  9* ORDER BY pct desc;
SERVICE          MODULE           ACTION                       PCT
---------------- ---------------- -------------------------- -----
SYS$USERS        Facturacion      Calculo.Impuestos             53
SYS$USERS        Almacenes        Consulta.Inventario         44.1
orcl.oracle.com  OEM.DefaultPool  /database/instance/sitemap   1.2
orcl.oracle.com  OEM.DefaultPool  /logon                        .7
orcl.oracle.com  OEM.Loader       XMLLoader0                    .6
orcl.oracle.com  Sin Nombre       Sin Nombre                    .4

Esta información también la podemos encontrar en Enterprise Manager, pero esta vez mostrada en forma gráfica (Performance > Top Consumers).


Hasta aquí llegaremos por el momento, pero ya hemos hecho un gran avance. Usando dbms_application_info se simplifica la identificación de la aplicación a la cual pertenecen las sentencias SQL, también se posibilita saber el avance de los procesos de larga duración, de una forma muy precisa. Pero ésta no es toda la utilidad, en un próximo Post veremos beneficios adicionales de la instrumentación de las aplicaciones, estén atentos.

Posts Relacionados:
Siga leyendo >>

¿Te pareció interesante este artículo?, ¿te quedaron algunas dudas?, ¿quieres sugerirme un tema a tratar?, pues déjame tus comentarios o envíame un email y para que NO te pierdas ningún Post, suscríbete por email ahora mismo!

domingo, 16 de noviembre de 2008

¿Cómo recuperar un spfile perdido?

El server parameter file o spfile, es un archivo binario que contiene los parámetros de inicialización. Este archivo existe en el servidor de base de datos y ha venido a reemplazar al otrora parameter file o pfile. Antes de la aparición del spfile, todo cambio a los parámetros debíamos hacerlos permanentes mediante la edición del archivo pfile. Si olvidábamos hacerlo, con el siguiente inicio de la base de datos los cambios eran descartados. Con la aparición del spfile ya no es necesario preocuparse por esto, los cambios se registran automáticamente, sin necesidad de ediciones. El archivo spfile es leído al momento de iniciar la instancia, por lo que su ausencia lo impide, afortunadamente es bastante fácil superar su pérdida, veremos a continuación un par de formas de recuperarlo.


a) Usando el Alert.log
Cada vez que hacemos un cambio a un parámetro de inicialización, aparte de grabarse en el spfile, también se deja una constancia en el alert.log. Al iniciar la base de datos, en el alert.log se registra la relación de parámetros de inicialización con valores modificados. Luego, nuestra primera fuente para reconstruir un spfile perdido es desde luego el alert.log.

alertorcl.log:
. . .
Starting up ORACLE RDBMS Version: 10.2.0.4.0.
System parameters with non-default values:
  processes                = 150
  __shared_pool_size       = 125829120
  __large_pool_size        = 4194304
  __java_pool_size         = 4194304
  __streams_pool_size      = 8388608
  nls_territory            = AMERICA
  filesystemio_options     = SETALL
  sga_target               = 205520896
  control_files            = /u02/oradata/orcl/control01.ctl
  db_block_size            = 8192
  __db_cache_size          = 58720256
  compatible               = 10.2.0.4.0
  log_archive_format       = %t_%s_%r.dbf
  db_create_file_dest      = /u02/oradata
  db_recovery_file_dest    = /u01/app/oracle/flash_recovery_area
  db_recovery_file_dest_size= 4294967296
  undo_management          = AUTO
  undo_tablespace          = UNDOTBS1
  remote_login_passwordfile= EXCLUSIVE
  audit_sys_operations     = FALSE
  db_domain                = oracle.com
  job_queue_processes      = 10
  background_dump_dest     = /u01/app/oracle/admin/orcl/bdump
  user_dump_dest           = /u01/app/oracle/admin/orcl/udump
  core_dump_dest           = /u01/app/oracle/admin/orcl/cdump
  audit_file_dest          = /u01/app/oracle/admin/orcl/adump
  audit_trail              = XML, EXTENDED
  db_name                  = orcl
  open_cursors             = 300
  pga_aggregate_target     = 67108864
PMON started with pid=2, OS id=5963
. . .

Procedemos a extraer la relación de parámetros y los copiamos en un archivo, mismo que puede estar ubicado en cualquier directorio y tener cualquier nombre o extensión, pero que para este ejemplo llamaremos initorcl.ora. Una vez creado podemos construir un spfile tomándolo como insumo.

SYS@orcl> create spfile from pfile='/home/oracle/initorcl.ora';

File created.
SYS@orcl> startup;
ORACLE instance started.

Total System Global Area  205520896 bytes
Fixed Size                  1266608 bytes
Variable Size             142609488 bytes
Database Buffers           58720256 bytes
Redo Buffers                2924544 bytes
Database mounted.
Database opened.

Acá estamos asumiendo que la base de datos estaba abajo, pero si aún está operativa entonces tendremos que hacerlo con un paso extra.

SYS@orcl> create spfile from pfile='/home/oracle/initorcl.ora';
create spfile from pfile='/home/oracle/initorcl.ora'
*
ERROR at line 1:
ORA-32002: cannot create SPFILE already being used by the instance

SYS@orcl> create spfile='?/dbs/spfileorcl.bak' from pfile='/home/oracle/initorcl.ora';

File created.

$ mv $ORACLE_HOME/dbs/spfileorcl.bak $ORACLE_HOME/dbs/spfileorcl.ora

El resultado final es un nuevo spfile que incluye los cambios más recientes.


b) Usando backups obtenidos con RMAN
Si por alguna razón no podemos hacer uso del alert.log, nuestra siguiente alternativa es recurrir a los backups obtenidos con RMAN. Cada vez que obtenemos un backup que, de forma directa o indirecta, involucre al tablespace System, tanto el controlfile como el spfile son automáticamente respaldados. Esto también ocurre si hemos configurado RMAN con "controlfile autobackup on", en este caso no importa si el backup incluye o no al tablespace System. Asumiendo que la base de datos está operativa, el procedimiento a seguir para restaurar el spfile es:

RMAN> restore spfile to '?/dbs/spfileora.bak' from autobackup;

Starting restore at 05/11/2008 17:42:25
allocated channel: ORA_DISK_1
channel ORA_DISK_1: sid=143 devtype=DISK

recovery area destination: /u01/app/oracle/flash_recovery_area
database name (or database unique name) used for search: ORCL
channel ORA_DISK_1: autobackup found in the recovery area
channel ORA_DISK_1: autobackup found: /u01/app/oracle/flash_recovery_area/ORCL/autobackup/2008_11_05/o1_mf_s_670006939_4k45zd4k_.bkp
channel ORA_DISK_1: SPFILE restore from autobackup complete
Finished restore at 05/11/2008 17:42:29

$ mv $ORACLE_HOME/dbs/spfileorcl.bak $ORACLE_HOME/dbs/spfileorcl.ora

Si la base de datos está abajo, el procedimiento es algo distinto.

RMAN> startup nomount;

startup failed: ORA-01078: failure in processing system parameters
LRM-00109: could not open parameter file '/u01/app/oracle/product/10.2.0/db_1/dbs/initorcl.ora'

starting Oracle instance without parameter file for retrival of spfile
Oracle instance started

Total System Global Area     159383552 bytes

Fixed Size                     1266344 bytes
Variable Size                 54529368 bytes
Database Buffers             100663296 bytes
Redo Buffers                   2924544 bytes

RMAN> restore spfile  from autobackup
2> recovery area '/u01/app/oracle/flash_recovery_area'
3> db_name 'ORCL';

Starting restore at 05/11/2008 17:49:50
using channel ORA_DISK_1

recovery area destination: /u01/app/oracle/flash_recovery_area
database name (or database unique name) used for search: ORCL
channel ORA_DISK_1: autobackup found in the recovery area
channel ORA_DISK_1: autobackup found: /u01/app/oracle/flash_recovery_area/ORCL/autobackup/2008_11_05/o1_mf_s_670006939_4k45zd4k_.bkp
channel ORA_DISK_1: SPFILE restore from autobackup complete
Finished restore at 05/11/2008 17:49:53

Observamos que RMAN es capaz de iniciar la instancia, aún cuando no existe el spfile, justamente porque asume que queremos restaurarlo de algún backup. Para lograr restaurar el spfile, debemos indicar la ubicación del Recovery Area, así como el nombre de la base de datos, con esos datos RMAN busca el backup más reciente.

En conclusión, el archivo spfile es requerido para iniciar la instancia, si lo hemos perdido podemos recurrir al alert.log o a los backups. En caso de que no podamos lograrlo, siempre queda como última alternativa el crear un pfile con el mínimo de parámetros y a partir de él un spfile, pero es una situación en la que difícilmente deberíamos caer si hemos sido suficientemente cuidadosos y precavidos.

Puedes complementar lo acá detallado, leyendo el Note 372996.1 Using RMAN to Restore and Recover a Database When the Repository and Spfile/Init.ora Files Are Also Lost.

Siga leyendo >>

¿Te pareció interesante este artículo?, ¿te quedaron algunas dudas?, ¿quieres sugerirme un tema a tratar?, pues déjame tus comentarios o envíame un email y para que NO te pierdas ningún Post, suscríbete por email ahora mismo!

jueves, 13 de noviembre de 2008

Postergando la validación de los constraints.

Usamos Oracle para tener nuestra información segura y accesible, pero si ésta no es consistente pierde su valor. Por esta razón normalmente implementamos un mínimo de reglas que deben cumplir nuestros datos, esto mediante el uso de constraints. A saber son: not null, unique, primary key, foreign key y check; por defecto estas restricciones se validan en cuanto tratamos de modificar la información, lo cual en ocasiones nos puede limitar y obligarnos a tomar caminos alternativos, que pueden pasar incluso por deshabilitar los constraints de forma temporal. Esto introduce la posibilidad de generar información inconsistente, afortunadamente existe una forma de postergar estas validaciones, sin sacrificar la integridad de la información, y que aún sigue siendo poco conocida.

Para lograrlo nuestro objetivo de violar temporalmente las reglas impuestas por los constraints, debemos crearlos de una forma especial, conocida como deferrable constraints. Veamos la diferencia con el siguiente ejemplo, en el cual se crea un foreign key constraint con validación inmediata y luego con la modalidad postergada.

transaccion.sql
BEGIN
  INSERT INTO emp (
     empno, ename, job, mgr, hiredate, sal, comm, deptno )
  VALUES (
     8000, 'BROWN', 'MANAGER', NULL,
     TO_DATE('15-JAN-1982', 'DD-MON-YYYY'), 3300, NULL, 50);
  INSERT INTO dept ( deptno, dname, loc )
  VALUES ( 50, 'MARKETING', 'MIAMI');
  COMMIT;
END;
/

SCOTT@orcl > ALTER TABLE emp ADD CONSTRAINT emp_fk_dept
  2  FOREIGN KEY(deptno) REFERENCES dept;

Table altered.

SCOTT@orcl > @transaccion
BEGIN
*
ERROR at line 1:
ORA-02291: integrity constraint (SCOTT.EMP_FK_DEPT) violated - parent key not found
ORA-06512: at line 2

Aún cuando el resultado final del bloque BEGIN/END es información consistente, la inmediatez de la validación impide ingresar una fila en la table emp referenciando a un deptno aún no existente en la tabla dept. Redefinamos entonces el constraint para que sea postergable.

SCOTT@orcl > ALTER TABLE emp ADD CONSTRAINT emp_fk_dept
  2  FOREIGN KEY(deptno) REFERENCES dept
  3  DEFERRABLE INITIALLY DEFERRED;

Table altered.

SCOTT@orcl > @transaccion
PL/SQL procedure successfully completed.

Ahora sí se permite concluir la transacción, la validación se ha postergado al momento de hacer el commit, si en ese momento aún subsistiese alguna inconsistencia entonces recién se aborta la transacción.

SCOTT@orcl > DELETE dept WHERE deptno = 50;

1 row deleted.

SCOTT@orcl > commit;
commit
*
ERROR at line 1:
ORA-02091: transaction rolled back
ORA-02292: integrity constraint (SCOTT.EMP_FK_DEPT) violated - child record found

Si bien permitir que la validación se postergue al momento de concluir la transacción resulta útil, por una cuestión de orden resulta más conveniente que ésta no sea la conducta por defecto, sino más bien la excepción, para ello debemos crear el constraint de otra forma.

SCOTT@orcl > ALTER TABLE emp ADD CONSTRAINT emp_fk_dept
  2  FOREIGN KEY(deptno) REFERENCES dept
  3  DEFERRABLE INITIALLY IMMEDIATE;

Como su nombre lo sugiere, IMMEDIATE conlleva a la validación del constraint en forma simultánea con la modificación de los datos, pero al ser el constraint DEFERRABLE, se permite postergar la validación para casos especiales, que se habilitan con una sintaxis adicional. Veámoslo con el ejemplo.

SCOTT@orcl > @transaccion
BEGIN
*
ERROR at line 1:
ORA-02291: integrity constraint (SCOTT.EMP_FK_DEPT) violated - parent key not found
ORA-06512: at line 2

SCOTT@orcl > set constraint emp_fk_dept deferred;

Constraint set.

SCOTT@orcl > @transaccion

PL/SQL procedure successfully completed.

Con la inclusión de set constraint .. deferred, habilitamos temporalmente, y sólo para las transacciones al interior de la sesión vigente, la postergación del constraint "emp_fk_dept". Podemos consignar toda una lista de constraints a postergar temporalmente o en su defecto indicar que lo sean todos los que puedan tratar de validarse en las futuras transacciones (siempre que hayan sido creadas como deferrable).

set constraint constraint_1, constraint_2, ..., constraint_n deferred;

set constraint all deferred;

Tenemos entonces en los deferrable constraints, una forma soportada de violar temporalmente las reglas que normalmente son validadas de inmediato, postergándolas hasta el momento en que concluímos la transacción (commit). Todo constraint puede ser postergado, pero en el caso particular del primary key y del unique, los índices que sirven para validar el cumplimiento de estos constraints son creados como índices no únicos, cuando normalmente son únicos. Esto implica que ocuparán algo más de espacio y su mantenimiento requerirá de algo más de CPU, pero usualmente en tan pequeña magnitud que puede ser despreciado si lo comparamos con los eventuales beneficios.

Puedes leer más del tema en el manual Database Concepts y en el Note 73647.1 Deferred Constraints Example, también les recomiendo leer este Post por Richard Foote, en el que expone algunas razones por las que puede resultar inconveniente crear algunos tipos de constraints como postergables.

Siga leyendo >>

¿Te pareció interesante este artículo?, ¿te quedaron algunas dudas?, ¿quieres sugerirme un tema a tratar?, pues déjame tus comentarios o envíame un email y para que NO te pierdas ningún Post, suscríbete por email ahora mismo!

martes, 11 de noviembre de 2008

¿Cómo importar datos sin un dump de por medio?

Todos hemos usado alguna vez los utilitarios export e import, pero con la aparición de Oracle10g vino como novedad Data Pump. Aún muchos no se han animado a usarlo, pero eventualmente tendrán que hacerlo ya que Oracle ha anunciado que en futuras versiones sólo se proporcionará import. Pues bien, la verdad es que si investigamos un poco y empezamos a usarlo, notaremos que mucho de los problemas por lo que pasamos al tratar de hacer algunas cosas medio rebuscadas con export e import, ahora son posibles con Data Pump y se los iré mostrando poco a poco, pero para empezar veamos cómo ahora es posible exportar los datos de un base de datos e importarlos en otra remota sin necesidad de crear un dump, e incluso crear allí el usuario destino si es que éste aún no existe, a ver ¡traten de hacer eso con export e import!

Para hacer esta demostración contaremos con una base de datos 10.2 bajo Solaris 10 como origen, y otra base de datos 11.1 bajo RHEL 4.7 como destino. Debemos empezar por crear un database link en la base de datos destino que nos permita comunicarnos con la base de datos origen

SYSTEM@orcl > create database link solaris
  2  connect to system identified by oracle
  3  using 'solaris';

Database link created.

SYSTEM@orcl > select banner from v$version@solaris;

BANNER
----------------------------------------------------------------
Oracle Database 10g Enterprise Edition Release 10.2.0.2.0 - Prod
PL/SQL Release 10.2.0.2.0 - Production
CORE    10.2.0.2.0      Production
TNS for Solaris: Version 10.2.0.2.0 - Production
NLSRTL Version 10.2.0.2.0 - Production

Ya tenemos el database link operativo, procedamos a deshacernos del usuario scott que usualmente se instala como parte de los ejemplos.

SYSTEM@orcl > drop user scott cascade;

User dropped.

Hagamos la importación de los datos.
[oracle@linux ~]$ impdp system/oracle nologfile=y network_link=solaris schemas=scott

Import: Release 11.1.0.6.0 - Production on Tuesday, 11 November, 2008 9:58:03

Copyright (c) 2003, 2007, Oracle.  All rights reserved.

Connected to: Oracle Database 11g Enterprise Edition Release 11.1.0.6.0 - Production
With the Partitioning, OLAP, Data Mining and Real Application Testing options
Starting "SYSTEM"."SYS_IMPORT_SCHEMA_01":  system/******** nologfile=y network_link=solaris schemas=scott
Estimate in progress using BLOCKS method...
Processing object type SCHEMA_EXPORT/TABLE/TABLE_DATA
Total estimation using BLOCKS method: 192 KB
Processing object type SCHEMA_EXPORT/USER
Processing object type SCHEMA_EXPORT/SYSTEM_GRANT
Processing object type SCHEMA_EXPORT/ROLE_GRANT
Processing object type SCHEMA_EXPORT/DEFAULT_ROLE
Processing object type SCHEMA_EXPORT/PRE_SCHEMA/PROCACT_SCHEMA
Processing object type SCHEMA_EXPORT/TABLE/TABLE
. . imported "SCOTT"."DEPT"                                   4 rows
. . imported "SCOTT"."EMP"                                   14 rows
. . imported "SCOTT"."SALGRADE"                               5 rows
. . imported "SCOTT"."BONUS"                                  0 rows
Processing object type SCHEMA_EXPORT/TABLE/INDEX/INDEX
Processing object type SCHEMA_EXPORT/TABLE/CONSTRAINT/CONSTRAINT
Processing object type SCHEMA_EXPORT/TABLE/INDEX/STATISTICS/INDEX_STATISTICS
Processing object type SCHEMA_EXPORT/TABLE/CONSTRAINT/REF_CONSTRAINT
Processing object type SCHEMA_EXPORT/TABLE/STATISTICS/TABLE_STATISTICS
Job "SYSTEM"."SYS_IMPORT_SCHEMA_01" successfully completed at 09:58:17

No hay mensajes de error, pero igual comprobaremos.

[oracle@linux ~]$ sqlplus scott/tiger

SQL*Plus: Release 11.1.0.6.0 - Production on Tue Nov 11 09:58:23 2008

Copyright (c) 1982, 2007, Oracle.  All rights reserved.


Connected to:
Oracle Database 11g Enterprise Edition Release 11.1.0.6.0 - Production
With the Partitioning, OLAP, Data Mining and Real Application Testing options

SCOTT@orcl > select table_name, num_rows
  2  from user_tables;

TABLE_NAME                       NUM_ROWS
------------------------------ ----------
SALGRADE                                5
BONUS                                   0
EMP                                    14
DEPT                                    4

Todo funcionó de acuerdo a lo esperado, hemos logrado sin mayor problema:
  • Usar un cliente de una versión superior (11g) para extraer datos de una versión inferior(10gR2), algo que con exp usualmente nos daría algunos errores de compatibilidad.
  • Creación del usuario receptor de los datos, esto es posible porque hemos empleado un usuario privilegiado, system en este caso, de otra forma el usuario debe existir previamente.
  • Transferir los datos desde una base de datos remota sin necesidad de generar un archivo dump como paso intermedio, gracias a la inclusión del parámetro network_link.

Puedes encontrar información adicional en el Note 351598.1 Export/Import DataPump: The Minimum Requirements to Use Export DataPump and Import DataPump (System Privileges) y en la documentación oficial.

Siga leyendo >>

¿Te pareció interesante este artículo?, ¿te quedaron algunas dudas?, ¿quieres sugerirme un tema a tratar?, pues déjame tus comentarios o envíame un email y para que NO te pierdas ningún Post, suscríbete por email ahora mismo!

lunes, 10 de noviembre de 2008

¿Cómo acelerar el acceso a los file systems?

Estas usando file systems para tu base de datos y todo opera en aparente normalidad, pero ¿sabías que por defecto Oracle no aprovecha todas las mejoras que los file systems modernos tienen implementados? Pues esa es la cruda realidad, si te interesa saber cómo puedes sacarle el jugo al I/O de tus discos con solo cambiar un parámetro, entonces debemos empezar por algo de teoría no Oracle.


File Buffer Cache
Un archivo no es más que una colección de bits almacenados en un medio persistente. Cuando un proceso requiere acceder a los datos de un archivo, el sistema operativo los lleva a la memoria principal, donde el proceso puede hacer uso del mismo, modificarlo, y luego solicitar que sea nuevamente guardado en disco. Pero teniendo en mente que los discos son mucho más lentos que la memoria principal, los sistemas operativos hacen normalmente uso de un buffer en memoria, llamado file buffer cache. Como resultado, el sistema operativo primero intenta obtener los datos del buffer cache, si no la encuentra allí la lee de disco y la coloca en el buffer cache. De forma similar, las escrituras también pasan por el buffer cache de forma que las futuras lecturas puedan ser satisfechas sin necesidad de acceder a los discos.

Direct I/O
Hasta acá el uso del file buffer cache parecería beneficioso, pero no olvidemos que Oracle ya tiene su propia implementación: el database buffer cache. Al ser el propio Oracle quien controla qué bloques requieren de permanecer o no en el cache, bajo un algoritmo LRU con el que no cuenta el sistema operativo, la existencia del file buffer cache puede resultar indeseable e innecesario al tener que viajar los datos primero al file buffer cache y luego al database buffer cache, conllevando a un consumo adicional de CPU y también de memoria principal, la cual ya no está disponible para Oracle. Tomando esto en cuenta, hace su aparición el Direct I/O, como una forma de evitar el uso del file buffer cache, de hecho es la forma con que se interactúa con los raw devices (porciones de disco no formateados); en la actualidad prácticamente todos los sistemas operativos soportan direct I/O y en algunos casos incluso versiones más sofisticadas como concurrent i/o provisto por IBM con JFS2.

Habilitando Direct I/O
En Oracle existe una forma de controlar la forma en que se interactúa con los file systems, se trata de filesystemio_options, que acepta como valores:
  • none. La grabación será síncrona con bloqueo, es decir la aplicación espera a que la llamada al sistema se complete antes de poder hacer una nueva llamada. Es universalmente soportada pero es la más forma más lenta.
  • asynch. La aplicación no espera a que la llamada al sistema se complete, puede realizar otras tareas mientras espera la confirmación de la llamada previa.
  • directio. La grabación es síncrona sin pasar por el file buffer cache (las dos modalidades previas hacen uso del file buffer cache).
  • setall. La grabación es asíncrona y sin pasar por el file buffer cache. Presenta la posibilidad de máximo desempeño.

Dependiendo de la plataforma Oracle le asigna a éste parámetro el valor por defecto none (ejm. Linux) o asynch (ejm. Solaris), para habilitar direct I/O los valores a escoger han de ser directio o mejor aún setall (asynch + directio). Este parámetro no se puede cambiar online por lo que luego de aplicar el cambio en el spfile.ora debes reiniciar la base de datos para que entre en efecto.

SYS@orcl > show parameter filesystemio_options

NAME                                 TYPE        VALUE
------------------------------------ ----------- ------------------------------
filesystemio_options                 string      none

SYS@orcl > alter system set filesystemio_options=setall scope=spfile;

System altered.

SYS@orcl > shutdown immediate
Database closed.
Database dismounted.
ORACLE instance shut down.

SYS@orcl > startup
ORACLE instance started.

Total System Global Area  209715200 bytes
Fixed Size                  1279360 bytes
Variable Size             109054592 bytes
Database Buffers           96468992 bytes
Redo Buffers                2912256 bytes
Database mounted.
Database opened.

Estamos probando con Linux, de allí el valor none, ahora para verificar que estamos trabajando con asynch y directio en simultáneo, podemos valernos de algunos utilitarios propios de Linux, pero que tienen sus similares en otros sistemas operativos. Primero veamos la situación original, es decir cuando filesystemio_options=none.

SYS@orcl> shutdown immediate
Database closed.
Database dismounted.
ORACLE instance shut down.
SYS@orcl> startup mount;
ORACLE instance started.

Total System Global Area  205520896 bytes
Fixed Size                  1266608 bytes
Variable Size             142609488 bytes
Database Buffers           58720256 bytes
Redo Buffers                2924544 bytes
Database mounted.

$ ps -ef | grep dbw
oracle   28532     1  0 16:56 ?        00:00:00 ora_dbw0_orcl
SYS@orcl> alter database open;

Database altered.
$ more /tmp/trace_dbwr.out
. . .
open("/u02/oradata/ORCL/datafile/o1_mf_system_3wqn5ypm_.dbf", O_RDWR|O_SYNC|O_LARGEFILE) = 17
. . .

$ cat /proc/slabinfo | grep kio
kioctx   18  30  256  15  1 : tunables  120  60  0 : slabdata  2  2  0
kiocb     0   0  128  31  1 : tunables  120  60  0 : slabdata  0  0  0

Ahora con filesystemio_options=setall.

$ more /tmp/trace_dbwr.out
. . .
open("/u02/oradata/ORCL/datafile/o1_mf_system_3wqn5ypm_.dbf", O_RDONLY|O_DIRECT|O_LARGEFILE) = 17
. . .
$ cat /proc/slabinfo | grep kio
kioctx   17  30  256  15  1 : tunables  120  60  0 : slabdata  2  2 0
kiocb     7  31  128  31  1 : tunables  120  60  0 : slabdata  1  1 0

Que DBWR abra los datafiles con O_DIRECT es señal del uso de Direct I/O, mientras que la presencia de valores distintos de cero para kiocb lo es del uso de Asynch I/O.

¿File buffer cache o Direct I/O?
La decisión no es simple, antes de optar por uno u otro debemos tener en cuenta cómo puede afectar a nuestras aplicaciones, para empezar vemos el caso de una aplicación simulada que es intensiva en lecturas aleatorias, lo cual es típico de las aplicaciones OLTP.

Random.sh
sqlplus / as sysdba << EOF
alter system flush buffer_cache;
exit;
EOF
sqlplus /nolog @Random.sql &
sqlplus /nolog @Random.sql &
sqlplus /nolog @Random.sql &
sqlplus /nolog @Random.sql &

Random.sql
connect test/test
set timing on
alter session set events '10046 trace name context forever, level 8';
begin
  for i in 1..200000 loop
     execute immediate
        'select data from testio where id = :id'
     using ROUND(dbms_random.value(1,200000));
  end loop;
end;
/
exit

Analizando los tiempos con tkprof:

filesystemio_options=none

select data
from
 testio where id = :id

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        4      0.00       1.38          0          0          0           0
Execute 800000     51.29     172.13          1          1          0           0
Fetch        0      0.00       0.00          0          0          0           0
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total   800004     51.30     173.51          1          1          0           0

===
filesystemio_options=setall

select data
from
 testio where id = :id

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        4      0.00       0.00          0          0          0           0
Execute 800000     38.84     128.18          0          0          0           0
Fetch        0      0.00       0.00          0          0          0           0
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total   800004     38.85     128.18          0          0          0           
0

Ahora con una aplicación intensiva en Full Table Scans, típico de las aplicaciones DSS.

Full.sh
sqlplus / as sysdba << EOF
alter system flush buffer_cache;
exit;
EOF
sqlplus /nolog @Full.sql &
sqlplus /nolog @Full.sql &
sqlplus /nolog @Full.sql &
sqlplus /nolog @Full.sql &

Full.sql
connect test/test
set timing on
alter session set events '10046 trace name context forever, level 8';
declare v_data testio.data%type;
begin
for i in 1..25 loop
select max(data) into v_data from testio;
end loop;
end;
/
exit

Resumen de traces analizados con tkprof.

filesystemio_options=none

SELECT MAX(DATA)
FROM
 TESTIO

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        4      0.00       0.00          0          0          0           0
Execute    100      0.01       0.01          0          0          0           0
Fetch      100     61.89     232.97     503813    1446600          0         100
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total      204     61.91     232.99     503813    1446600          0         100

Elapsed times include waiting on following events:
  Event waited on                             Times   Max. Wait  Total Waited
  ----------------------------------------   Waited  ----------  ------------
  db file sequential read                       146        0.00          0.00
  db file scattered read                      13771        0.67         19.53 

====
filesystemio_options=setall

SELECT MAX(DATA)
FROM
 TESTIO

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        4      0.01       1.22          1          1          0           0
Execute    100      0.02       0.06          0          0          0           0
Fetch      100     70.63     366.62     259438    1446600          0         100
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total      204     70.66     367.91     259439    1446601          0         100

Elapsed times include waiting on following events:
  Event waited on                             Times   Max. Wait  Total Waited
  ----------------------------------------   Waited  ----------  ------------
  read by other session                       20820        0.24        133.64
  db file scattered read                       8942        0.99        100.00 
  latch: cache buffers chains                   193        0.02          0.73
  db file sequential read                       302        0.07          1.39

Con filesystemio_options=setall las lecturas aleatorias mejoraron en 26%, mientras que los Full Table Scans empeoraron en casi 58%!

Conclusiones
Revisando los resultados de los escenarios simulados, podemos concluir que, ante la activación de direct I/O, las aplicaciones intensivas en acceso aleatorio a los discos se ven beneficiadas mientras que las intensivas en Full Table Scans se ven perjudicadas, de allí que no debemos saltar a la conclusión de que con solo habilitar el direct I/O nuestra base de datos será mágicamente más rápida, es posible que así sea, como también es posible que el impacto sea nulo e incluso adverso, primero hagan las pruebas del caso durante un periodo de carga típica antes de decidir dejarlo permanentemente. Lo que sí es seguro que mejorará el desempeño es el uso de filesystemio_options=asynch, por lo que es lo mínimo con lo que deberías configurar tu base de datos.

Cada plataforma tiene sus particularidades en cuanto a la aplicabilidad de los diversos valores para filesystemio_options e incluso hay algunos bugs específicos para ciertas combinaciones de versión de Oracle + versión de Sistema Operativo, por ello es conveniente que te informes más del tema. Te recomiendo la lectura de los Notes 462072.1 File System's Buffer Cache versus Direct I/O, 555601.1 How To Verify Whether DIRECTIO is Being Used, 237299.1 How To Check if Asynchronous I/O is Working On Linux y 432854.1 Asynchronous I/O Support on OCFS/OCFS2 and Related Settings: filesystemio_options, disk_asynch_io, finalmente Boost application performance using asynchronous I/O.

Post Relacionados:

Siga leyendo >>

¿Te pareció interesante este artículo?, ¿te quedaron algunas dudas?, ¿quieres sugerirme un tema a tratar?, pues déjame tus comentarios o envíame un email y para que NO te pierdas ningún Post, suscríbete por email ahora mismo!

domingo, 9 de noviembre de 2008

ORA-00257 archiver error, ¿aún cuando ya hice espacio?

Conscientes de que Oracle9i está de salida, la empresa Acme finalmente hizo la actualización a la versión 10gR2; las cosas no podían ir mejor hasta que algunos usuarios empiezan a reportar que sus sesiones están suspendidas y otros que no pueden conectarse, obteniendo el error ORA-00257: archiver error. Connect internal only, until freed.

El problema resulta evidente, ya antes había pasado, se toma la rápida decisión de mover algunos archivelogs antiguos a otro directorio; ahora hay espacio pero el problema parece no resolverse, ¿qué puede estar pasando? Pues que ahora están usando Flash Recovery Area (FRA) y eso cambia las cosas.

Con Oracle 10g aparece Flash Recovery Area como un espacio en el cual se depositan por defecto los archivelogs y backups, Oracle se encarga de gestionarlo y exige que su mantenimiento se haga sin recurrir a comandos del Sistema Operativo.

Para este caso en particular, como la liberación de espacio se ha hecho mediante el traslado de archivelogs a otro directorio usando el Sistema Operativo, Oracle permanece sin ser notificado de tal cambio y por tanto asume que el problema de falta de espacio persiste, y así nos lo hace saber mediante mensajes en el alert.ora.

Sat Nov  8 13:58:46 2008
Thread 1 advanced to log sequence 26
Current log# 2 seq# 18 mem# 0: /u01/oradata/orcl/redo02.log
Sat Nov  8 13:58:46 2008
Errors in file /opt/oracle/admin/orcl/bdump/orcl_arc0_21093.trc:
ORA-19815: WARNING: db_recovery_file_dest_size of 209715200 bytes is 100.00% used, and has 0 remaining bytes available.
Sat Nov  8 13:58:46 2008
************************************************************************
You have following choices to free up space from flash recovery area:
1. Consider changing RMAN RETENTION POLICY. If you are using Data Guard,
then consider changing RMAN ARCHIVELOG DELETION POLICY.
2. Back up files to tertiary device such as tape using RMAN
BACKUP RECOVERY AREA command.
3. Add disk space and increase db_recovery_file_dest_size parameter to
reflect the new space.
4. Delete unnecessary files using RMAN DELETE command. If an operating
system command was used to delete files, then use RMAN CROSSCHECK and
DELETE EXPIRED commands.
************************************************************************
Sat Nov  8 13:58:46 2008
Errors in file /opt/oracle/admin/orcl/bdump/orcl_arc0_21093.trc:
ORA-19809: limit exceeded for recovery files
ORA-19804: cannot reclaim 51228672 bytes disk space from 209715200 limit
ARC0: Error 19809 Creating archive log file to '/fra/ORCL/archivelog/2008_11_08/o1_mf_1_25_0_.arc'
ARC0: Failed to archive thread 1 sequence 25 (19809)
ARCH: Archival stopped, error occurred. Will continue retrying
Sat Nov  8 13:58:46 2008
ORACLE Instance orcl - Archival Error
Sat Nov  8 13:58:46 2008
ORA-16038: log 1 sequence# 25 cannot be archived
ORA-19809: limit exceeded for recovery files
ORA-00312: online log 1 thread 1: '/u01/oradata/orcl/redo01.log'

Como podemos observar, Oracle encuentra el problema y no solo lo reporta sino que también nos da un resumen de las acciones que podemos tomar para superar el problema. La alternativa (4) es reveladora, si queremos hacer espacio eliminando archivos debemos usar el comando delete de RMAN para lograrlo.

RMAN> list archivelog all;


List of Archived Log Copies
Key     Thrd Seq     S Low Time            Name
------- ---- ------- - ------------------- ----
18      1    23      A 08/11/2008 13:20:58 /fra/ORCL/archivelog/2008_11_08/o1_mf_1_23_4kdsx5y2_.arc
19      1    24      A 08/11/2008 13:31:33 /fra/ORCL/archivelog/2008_11_08/o1_mf_1_24_4kdsx6th_.arc
20      1    25      A 08/11/2008 13:45:11 /fra/ORCL/archivelog/2008_11_08/o1_mf_1_25_4kdc7vrd_.arc

RMAN> delete noprompt archivelog until sequence 24;

released channel: ORA_DISK_1
allocated channel: ORA_DISK_1
channel ORA_DISK_1: sid=142 devtype=DISK

List of Archived Log Copies
Key     Thrd Seq     S Low Time            Name
------- ---- ------- - ------------------- ----
18      1    23      A 08/11/2008 13:20:58 /fra/ORCL/archivelog/2008_11_08/o1_mf_1_23_4kdsx5y2_.arc
19      1    24      A 08/11/2008 13:31:33 /fra/ORCL/archivelog/2008_11_08/o1_mf_1_24_4kdsx6th_.arc
deleted archive log
archive log filename=/fra/ORCL/archivelog/2008_11_08/o1_mf_1_23_4kdsx5y2_.arc recid=18 stamp=670289494
deleted archive log
archive log filename=/fra/ORCL/archivelog/2008_11_08/o1_mf_1_24_4kdsx6th_.arc recid=19 stamp=670289494
Deleted 2 objects

Volviendo a nuestro escenario, intentamos eliminar los archivelogs pero el esfuerzo es en vano.

RMAN> delete noprompt archivelog until sequence 24;

released channel: ORA_DISK_1
allocated channel: ORA_DISK_1
channel ORA_DISK_1: sid=142 devtype=DISK

List of Archived Log Copies
Key     Thrd Seq     S Low Time            Name
------- ---- ------- - ------------------- ----
18      1    23      A 08/11/2008 13:20:58 /fra/ORCL/archivelog/2008_11_08/o1_mf_1_23_4kdsx5y2_.arc
19      1    24      A 08/11/2008 13:31:33 /fra/ORCL/archivelog/2008_11_08/o1_mf_1_24_4kdsx6th_.arc

RMAN-06207: WARNING: 2 objects could not be deleted for DISK channel(s) due
RMAN-06208:          to mismatched status.  Use CROSSCHECK command to fix status
RMAN-06210: List of Mismatched objects
RMAN-06211: ==========================
RMAN-06212:   Object Type   Filename/Handle
RMAN-06213: --------------- ---------------------------------------------------
RMAN-06214: Archivelog      /fra/ORCL/archivelog/2008_11_08/o1_mf_1_23_4kdsx5y2_.arc
RMAN-06214: Archivelog      /fra/ORCL/archivelog/2008_11_08/o1_mf_1_24_4kdsx6th_.arc

Nuevamente junto con el mensaje se nos proporciona la solución: hacer un crosscheck.

RMAN> crosscheck archivelog all;

released channel: ORA_DISK_1
allocated channel: ORA_DISK_1
channel ORA_DISK_1: sid=142 devtype=DISK
validation failed for archived log
archive log filename=/fra/ORCL/archivelog/2008_11_08/o1_mf_1_23_4kdsx5y2_.arc recid=18 stamp=670288851
validation failed for archived log
archive log filename=/fra/ORCL/archivelog/2008_11_08/o1_mf_1_24_4kdsx6th_.arc recid=19 stamp=670288858
Crosschecked 2 objects

Finalmente removemos del catálogo de RMAN los archivelogs reportados como no encontrados en el paso anterior.

RMAN> delete noprompt expired archivelog all;

released channel: ORA_DISK_1
allocated channel: ORA_DISK_1
channel ORA_DISK_1: sid=142 devtype=DISK

List of Archived Log Copies
Key     Thrd Seq     S Low Time            Name
------- ---- ------- - ------------------- ----
18      1    23      X 08/11/2008 13:20:58 /fra/ORCL/archivelog/2008_11_08/o1_mf_1_23_4kdsx5y2_.arc
19      1    24      X 08/11/2008 13:31:33 /fra/ORCL/archivelog/2008_11_08/o1_mf_1_24_4kdsx6th_.arc
deleted archive log
archive log filename=/fra/ORCL/archivelog/2008_11_08/o1_mf_1_23_4kdsx5y2_.arc recid=18 stamp=670288851
deleted archive log
archive log filename=/fra/ORCL/archivelog/2008_11_08/o1_mf_1_24_4kdsx6th_.arc recid=19 stamp=670288858
Deleted 2 EXPIRED objects

Revisamos el alert.log y el problema aparece como resuelto.

Sat Nov  8 14:21:03 2008
ARC1: Archiving not possible: No primary destinations
ARC1: Failed to archive thread 1 sequence 26 (4)
Sat Nov  8 14:21:04 2008
db_recovery_file_dest_size of 200 MB is 27.83% used. This is a
user-specified limit on the amount of space that will be used by this
database for recovery-related files, and does not reflect the amount of
space available in the underlying filesystem or ASM diskgroup.
Sat Nov  8 14:21:15 2008
Archiver process freed from errors. No longer stopped
Sat Nov  8 14:21:15 2008
Thread 1 advanced to log sequence 27
  Current log# 1 seq# 20 mem# 0: /u01/oradata/orcl/redo03.log

Las sesiones vuelven a la vida y los usuarios pueden iniciar nuevas conexiones, todo ha regresado a la normalidad; podemos finalmente respirar con alivio y nos queda la enseñanza de que si bien usar Flash Recovery Area es altamente recomendado, debemos tener presente no manipular sus contenidos por medios ajenos a los proporcionados por Oracle.

Si deseas profundizar en el tema, te recomiendo leer el Note 278308.1 How to Resolve ORA-00257: Archiver is Stuck Error in 10g? y el Note 315098.1 How is the space pressure managed in the Flash Recovery Area - An Example.

Posts Relacionados:
Siga leyendo >>

¿Te pareció interesante este artículo?, ¿te quedaron algunas dudas?, ¿quieres sugerirme un tema a tratar?, pues déjame tus comentarios o envíame un email y para que NO te pierdas ningún Post, suscríbete por email ahora mismo!

jueves, 6 de noviembre de 2008

¿Cómo renombrar con Rman datafiles nombrados con caracteres especiales?

Faltaba espacio en un tablespace y optaste por agregar un datafile nuevo, resuelto el problema regresas a tus asuntos y te olvidas del tema. Luego, mientras de forma rutinaria inspeccionas los contenidos del filesystem, reparas en que el nuevo datafile tiene un nombre algo raro, tiene caracteres especiales, recuerdas que al escribir el nombre lo hiciste con tanta prisa que te equivocaste un par de veces pero al final el nombre estaba OK, o al menos en tu pantalla así parecía; bueno pues, igual hay que resolver el problema, pero ¿por dónde empezar?

La primera intención es renombrar el archivo desde el sistema operativo, pero encuentras problemas para lograrlo, ¿por qué no ayudarnos de nuestro viejo conocido Rman? Afortunadamente el procedimiento es bastante simple:
  1. Primero invocamos Rman y verificamos la situación inicial.
    $ rman target /
    RMAN> report schema;
    
    using target database control file instead of recovery catalog
    Report of database schema
    
    List of Permanent Datafiles
    ===========================
    File Size(MB) Tablespace       RB segs Datafile Name
    ---- -------- ---------------- ------- ------------------------
    1    480      SYSTEM           ***     /u01/oradata/orcl/system01.dbf
    2    30       UNDOTBS1         ***     /u01/oradata/orcl/undotbs01.dbf
    3    230      SYSAUX           ***     /u01/oradata/orcl/sysaux01.dbf
    4    5        USERS            ***     /u01/oradata/orcl/users01.dbf
    5    100      EXAMPLE          ***     /u01/oradata/orcl/example01.dbf
    6    10       DEMO             ***     /u01/oradata/orcl/�¡Ã³demo01.dbf
    
    List of Temporary Files
    =======================
    File Size(MB) Tablespace           Maxsize(MB) Tempfile Name
    ---- -------- -------------------- ----------- --------------------
    1    20       TEMP                 32767       /u01/oradata/orcl/temp01.dbf
  2. Procedemos a poner el tablespace afectado fuera de linea.
    RMAN> sql "alter tablespace demo offline";
  3. Obtenemos una copia del datafile, referenciándolo por su file#.
    RMAN> copy datafile 6 to '/u01/oradata/orcl/demo01.dbf';
    
    Starting backup at 06/11/2008 20:40:28
    allocated channel: ORA_DISK_1
    channel ORA_DISK_1: sid=142 devtype=DISK
    channel ORA_DISK_1: starting datafile copy
    input datafile fno=00006 name=/u01/oradata/orcl/�¡Ã³demo01.dbf
    output filename=/u01/oradata/orcl/demo01.dbf tag=TAG20081106T204029 recid=4 stamp=670106430
    channel ORA_DISK_1: datafile copy complete, elapsed time: 00:00:01
    Finished backup at 06/11/2008 20:40:30
    
  4. Ahora intercambiamos los archivos.
    RMAN> switch datafile 6 to copy;
    
    datafile 6 switched to datafile copy "/u01/oradata/orcl/demo01.dbf"
    
  5. Finalizamos poniendo el tablespace en linea.
    RMAN> sql "alter tablespace demo online";
    
    sql statement: alter tablespace demo online
    
  6. Verificamos.
    RMAN> report schema;
    
    using target database control file instead of recovery catalog
    Report of database schema
    
    List of Permanent Datafiles
    ===========================
    File Size(MB) Tablespace       RB segs Datafile Name
    ---- -------- ---------------- ------- ------------------------
    1    480      SYSTEM           ***     /u01/oradata/orcl/system01.dbf
    2    30       UNDOTBS1         ***     /u01/oradata/orcl/undotbs01.dbf
    3    230      SYSAUX           ***     /u01/oradata/orcl/sysaux01.dbf
    4    5        USERS            ***     /u01/oradata/orcl/users01.dbf
    5    100      EXAMPLE          ***     /u01/oradata/orcl/example01.dbf
    6    10       DEMO             ***     /u01/oradata/orcl/demo01.dbf
    List of Temporary Files
    =======================
    File Size(MB) Tablespace           Maxsize(MB) Tempfile Name
    ---- -------- -------------------- ----------- --------------------
    1    20       TEMP                 32767       /u01/oradata/orcl/temp01.dbf

El problema ha sido resuelto!

Puedes revisar el Note:743616.1 Rename Datafiles Using Rman, para ver otros escenarios posibles.

Posts Relacionados:
Siga leyendo >>

¿Te pareció interesante este artículo?, ¿te quedaron algunas dudas?, ¿quieres sugerirme un tema a tratar?, pues déjame tus comentarios o envíame un email y para que NO te pierdas ningún Post, suscríbete por email ahora mismo!

sábado, 1 de noviembre de 2008

Cursor sharing, histograms y bind peeking, lo que deberías saber para evitar sorpresas.

"Si fuese a escribir un libro sobre cómo construir aplicaciones Oracle no escalables, entonces "No Use Bind Variables" sería el título del primer y último capítulo." (Thomas Kyte)
No es infrecuente toparnos con aplicaciones que hacen caso omiso del principio de usar bind variables y vemos como el desempeño se va al suelo por la elevada tasa de hard parsing que se introduce. Casi sin pensarlo nos refugiamos en el uso del parámetro cursor_sharing como nuestra única alternativa para capear el temporal, pero ¿sabías que bajo determinadas circunstancias lo único que se logra es exacerbar el problema?

Para poder comprenderlo mejor, empecemos por recordar algo de teoría sobre el parámetro cursor_sharing. De acuerdo a la documentación podemos asignarle uno de tres valores:
  • Force. Fuerza a las sentencias que pueden diferir en algunos literales, pero que de otra forma son idénticas, a compartir un mismo cursor.
  • Similar. Causa que las sentencias que difieren en algunos literales, pero que de otra forma son idénticas, compartan un mismo cursor, a menos que los literales afecten ya sea el significado de la sentencia o el grado al cual el plan de ejecución es optimizado.
  • Exact (default). Sólo permite que sentencias con un texto idéntico compartan un mismo cursor.

Ahora, para ir viendo el efecto de ir modificando este parámetro, consideraremos una aplicación hipotética que genera sentencias de este tipo:

select avg(sal) from emp where deptno = 10
select avg(sal) from emp where deptno = 20
select avg(sal) from emp where deptno = 30
select avg(sal) from emp where deptno = 40
select avg(sal) from emp where deptno = 50

Asumiremos también que la tabla emp cuenta con un índice en la columna deptno y que hay una distribución no uniforme de sus contenidos:

SCOTT@orcl > select deptno, count(*)
2  from emp group by deptno;

DEPTNO   COUNT(*)
---------- ----------
10     200000
20          1
30          1
40          1
50          1

Finalmente nos aseguramos de contar con estadísticas actualizadas:

SCOTT@orcl > exec dbms_stats.gather_table_stats( ownname=>'SCOTT', tabname=>'EMP', cascade=>TRUE, method_opt => 'for all indexed columns size auto' )

Teniendo en cuenta que por defecto cursor_sharing=exact, al inspeccionar el Shared Pool no es sorpresa encontrar que cada sentencia de nuestra aplicación hipotética genere su propio cursor:

SYS@orcl > select sql_text, executions, child_number, plan_hash_value
2  from v$sql where sql_text like 'select max(sal)%';

SQL_TEXT                  CHILD_NUMBER EXECUTIONS PLAN_HASH_VALUE
------------------------- ------------ ---------- ---------------
select max(sal) from emp             0          1      2233664114
where deptno = 40

select max(sal) from emp             0          1      2083865914
where deptno = 10

select max(sal) from emp             0          1      2233664114
where deptno = 20

select max(sal) from emp             0          1      2233664114
where deptno = 50

select max(sal) from emp             0          1      2233664114
where deptno = 30

Si observamos con cuidado, notaremos que al consultar por deptno=10, el plan de ejecución es distinto que para los demás casos. Esto se explica por el hecho de que el histograma existente permite al optimizador deducir que un full table scan es lo óptimo, para los demás casos escoge más bien el uso del índice existente por la columna dept. Esta hipótesis es corroborada inspeccionando los planes de ejecución:

Plan hash value: 2083865914

----------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)|
----------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |       |       |   283 (100)|
|   1 |  SORT AGGREGATE    |      |     1 |    16 |            |
|*  2 |   TABLE ACCESS FULL| EMP  |   202K|  3162K|   283   (4)|
----------------------------------------------------------------

Plan hash value: 2233664114

-----------------------------------------------------------------------------
| Id  | Operation                    | Name    | Rows  | Bytes | Cost (%CPU)|
-----------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |         |       |       |     4 (100)|
|   1 |  SORT AGGREGATE              |         |     1 |    16 |            |
|   2 |   TABLE ACCESS BY INDEX ROWID| EMP     |     1 |    16 |     4   (0)|
|*  3 |    INDEX RANGE SCAN          | EMP_DEP |     1 |       |     3   (0)|
-----------------------------------------------------------------------------

Ha llegado el momento de hacer modificaciones, el primer cambio a experimentar será cursor_sharing=force, ejecutamos los queries y volvemos a inspeccionar el Shared Pool:

SQL_TEXT                  CHILD_NUMBER EXECUTIONS PLAN_HASH_VALUE
------------------------- ------------ ---------- ---------------
select max(sal) from emp             0          5      2083865914
where deptno = :"SYS_B_0"

Lo primero que notamos es que ahora solamente se emplea un cursor, lo cual nos alienta a creer que hemos mejorado la situación, pero luego nos percatamos que el plan de ejecución corresponde al del Full Table Scan, es decir hemos trasladado el problema desde el Shared Pool hacia el Buffer Cache, aumentando la cantidad de lecturas físicas y lógicas. ¿A qué se debe esto? Ha llegado la hora de hablar de Bind Peeking.

Bind Peeking fue introducido en Oracle 9i y consiste en que el optimizador le da un vistazo a los valores de las bind variables en la primera invocación del cursor, esto le permite determinar la selectividad y escoger un plan adecuado, a partir de allí toda invocación de este cursor ya no pasa por esta inspección y el plan de ejecución es automáticamente aplicado.

Alteremos ahora el orden de los queries para corroborar la teoría, dejaremos la consulta por deptno=10 para el final y miren lo que encontramos en el Shared Pool:

SQL_TEXT                  CHILD_NUMBER EXECUTIONS PLAN_HASH_VALUE
------------------------- ------------ ---------- ---------------
select max(sal) from emp             0          5      2233664114
where deptno = :"SYS_B_0"

Todas los queries comparten el mismo cursor, pero ahora con el plan de ejecución que usa el índice por deptno.

Llegamos a la conclusión que usar cursor_sharing=force es como jugar a la ruleta rusa, dependiendo qué sentencia llegue primero al Shared Pool su plan de ejecución será el que prevalezca con resultados nada convenientes. Pero ¿y si usamos cursor_sharing=similar? ¿Será la solución mágica que estamos esperando?, pues ejecutemos nuevamente los queries e inspeccionemos el Shared Pool:

SQL_TEXT                  CHILD_NUMBER EXECUTIONS PLAN_HASH_VALUE
------------------------- ------------ ---------- ---------------
select max(sal) from emp             0          1      2083865914
where deptno = :"SYS_B_0"

select max(sal) from emp             1          1      2233664114
where deptno = :"SYS_B_0"

select max(sal) from emp             2          1      2233664114
where deptno = :"SYS_B_0"

select max(sal) from emp             3          1      2233664114
where deptno = :"SYS_B_0"

select max(sal) from emp             4          1      2233664114
where deptno = :"SYS_B_0"

Se ha creado un cursor por cada sentencia, igual que cuando cursor_sharing=exact, pero los textos son iguales y se ve que tienen bind variables, ¿qué pasó entonces? Pues con cursor_sharing=similar los cursores son compartidos siempre y cuando el optimizador considere seguro hacerlo así. Para nuestro escenario, el optimizador considera inseguro compartir los cursores debido a la existencia de un histograma para la columna deptno, para probarlo eliminamos el histograma, ejecutamos las sentencias e inspeccionamos el Shared Pool:

SCOTT@orcl > exec dbms_stats.gather_table_stats( ownname=>'SCOTT', tabname=>'EMP', cascade=>FALSE, method_opt => 'for columns deptno size 1' )

SQL_TEXT                  CHILD_NUMBER EXECUTIONS PLAN_HASH_VALUE
------------------------- ------------ ---------- ---------------
select max(sal) from emp             0          5      2083865914
where deptno = :"SYS_B_0"

Todas las sentencias comparten el mismo cursor, pero al carecer de histogramas el Optimizador no es capaz de identificar que algunas sentencias se benefician del uso del índice por deptno, y termina escogiendo Full Table Scan (FTS) al creer que hay un único valor:

SCOTT@orcl > select num_distinct, histogram from user_tab_cols
2  where table_name = 'EMP' and column_name = 'DEPTNO';

NUM_DISTINCT HISTOGRAM
------------ ---------------------------------------------
1 NONE

Si ahora borramos totalmente las estadísticas de la columna deptno, obtenemos otro resultado:

SCOTT@orcl > exec dbms_stats.delete_column_stats( ownname => 'SCOTT', tabname => 'EMP', colname=>'DEPTNO')

SQL_TEXT                  CHILD_NUMBER EXECUTIONS PLAN_HASH_VALUE
------------------------- ------------ ---------- ---------------
select max(sal) from emp             0          5      2233664114
where deptno = :"SYS_B_0"

Ahora el Optimizador opta por usar el índice por deptno, explicado por el hecho de que ahora no cuenta con información y asume que la selectividad es alta:

SCOTT@orcl > select num_distinct, histogram from user_tab_cols
2  where table_name = 'EMP' and column_name = 'DEPTNO';

NUM_DISTINCT HISTOGRAM
------------ ---------------------------------------------
NONE

Resumiendo, con cursor_sharing=similar en caso de existir histogramas los cursores no serán compartidos y al no poder recurrir a información sobre la distribución de los datos, el Optimizador puede escoger un plan poco eficiente.

Existe un detalle final que no podemos pasar por alto y es que por defecto Oracle genera las estadísticas con histogramas, lo cual ciertamente afecta el posible efecto benéfico de emplear cursor_sharing=similar.

SYS@orcl > select dbms_stats.get_param('method_opt') from dual;
DBMS_STATS.GET_PARAM('METHOD_OPT')
----------------------------------
FOR ALL COLUMNS SIZE AUTO

En conclusión, nada es mejor que desarrollar las aplicaciones haciendo uso debido de bind variables, tratar de corregir el error inicial de no usarlos resulta en una tarea larga y costosa, si creemos que es facilmente superable con el parámetro cursor_sharing, pues nos equivocamos, en el mejor de los casos es únicamente un paliativo, una aspirina con la que pretendemos dar tratamiento a un cáncer, así que a hacer un mejor seguimiento a los equipos de desarrollo de aplicaciones, y si estamos por adquirir una aplicacion empaquetada, a revisar el código fuente, no vaya a ser que terminemos adquiriendo un problema en lugar de una solución.

Si deseas profundizar en el tema, revisa los Notes: 377847.1 Unsafe Peeked Bind Variables and Histograms, 261020.1 High Version Count with CURSOR_SHARING = SIMILAR or FORCE, 296377.1 Troubleshooting Guide to high version_counts, 430208.1 Bind Peeking By Example y 369427.1 Case Study: The Mysterious Performance Drop.

Posts Relacionados:
Siga leyendo >>

¿Te pareció interesante este artículo?, ¿te quedaron algunas dudas?, ¿quieres sugerirme un tema a tratar?, pues déjame tus comentarios o envíame un email y para que NO te pierdas ningún Post, suscríbete por email ahora mismo!