Sei sulla pagina 1di 48

TEMA 2: Herramientas de rendimiento

NOTA: Todas las pruebas de este estudio se harán bajo el usuario SCOTT y usando el programa
de Oracle SQL* Plus como interfaz. Éste se inicia tecleando en una ventana MS-DOS:
sqlplusw.exe. Previamente se habrá concedido a este usuario los roles SELECT_CATALOG_ROLE y
PLUSTRACE para que pueda ver el diccionario de datos como usuario DBA mediante el comando:
GRANT select_catalog_role,plustrace TO SCOTT;

Repaso a algunas herramientas.-


En este tema veremos las herramientas que debemos usar diariamente. Son herramientas que cada día
deben emplearse a fin ordenar los tests, realizar debug de procesos, ajustar algoritmos, etc. Todas las
herramientas que se verán son de modo texto, como los buenos programadores .

Para el ajuste de aplicaciones uso mis herramientas favoritas para asegurarme de que la aplicación es tan
rápida y escalable como sea posible. Estas herramientas para programadores son:

SQL*Plus.

Explain Plan.

AutoTrace.

TkProf.

DBMS_PROFILER.

Describiré el principal uso de cada herramienta, explicaré como prepararla inicialmente y veremos algunos
consejos útiles para interpretar sus resultados.

Pero antes de eso debo aclarar que todas las pruebas en este cursillo las haremos como el usuario SCOTT, con
contraseña TIGER. También trabajaremos en un tablespace que si no está creado lo podemos crear así:

CREATE TABLESPACE USERS


LOGGING
DATAFILE 'C:\ORACLE\ORADATA\<nombre_de_la_BD>\USERS.dbf' SIZE 200M EXTENT
MANAGEMENT LOCAL SEGMENT SPACE MANAGEMENT AUTO;

Si en nuestra base de datos no existe el usuario SCOTT siempre lo podemos crear (o recrear si hemos metido
la pata) con el script %ORACLE_HOME%\ rdbms\admin\ utlsampl.sql como usuario con privilegios DBA .
SQL*Plus.-
SQL* Plus tiene el don de la ubiqüidad, siempre está disponible y siempre es el mismo. Si puedo manejar
SQL*Plus en tu máquina Windows también podré hacerlo en un servidor Unix, Linux o en cualquier mainframe sin
ningun entrenamiento. Pero realmente ¿para que se emplea SQL*Plus?

AutoTrace. Es un método muy simple de obtener el plan de ejecución de una consulta, para ver sus
estadísticas, etc.

Como una herramienta de script. Mucha gente usa shell scripts para automatizar SQL* Plus. Puedo
escribir por ejemplo un script en SQL*Plus ara automatizar una operación de exportación de datos en
cualquier plataforma. No necesito volver a escribirlo simplemente porque usé Unix en primer lugar.

Preparando SQL*Plus
La preparación previa de SQL* Plus es extremadamente sencilla. De hecho ya estará hecha. Cada instalación
de servidor la tiene hecha al igual que cada instalación de cliente.

En Windows hay dos versiones de SQL* Plus: una versión gráfica (programa sqlplusw.exe) y una versión de
texto (progama sqlplus.exe). No hay ningún beneficio real de la versión gráfica sobre la de texto y ésta última
tiene los días contados.

Ajustando el entorno SQL*Plus


SQL* Plus tiene la habilidad de ejecutar automáticamente un script (o dos) cuando se pone en marcha. Estos
scripts pueden ser usados para ajustar el entorno de SQL* Plus declarando ciertas útiles variables. Estos scripts
son glogin.sql (global loging.sql) y login.sql y se encuentran en la rutas $ORACLE_HOME\sqlplus\ admin (Linux) o
%ORACLE_HOME%\ sqlplus\ admin (Windows). Con este script login.sql os daré una idea de lo que puede
contener y para que sirve cada parámetro de los más usados:

REM Desactivar la salida de SQL*Plus:


set termout off

REM Definir el editor por defecto:


define _editor=wordpad.exe

REM Activar la salida generada por la ejecución de bloques PL/SQL que usan
REM DBMS_OUTPUT para mostrar algo
set serveroutput on SIZE 1000000

REM Definiciones de formato y longitud de las columnas más consultadas:


COLUMN object_name format a30
COL segment_name format a30
COL file_name format a40
COL name format a30
COL what format a30
COL plan_plus_exp format a100

REM Eliminación de los espacios en blanco sobrantes


set TRIMSPOOL ON

REM Definición de cuantos caracteres deben mostrarse de las columnas LONG


set long 5000

REM Definición de la longitud de línea, a partir de la cual SQL*Plus corta:


set linesize 131

REM Definición a partir de cuantas filas SQL*Plus mostrará los encabezados de


REM columna
set pagesize 9999

REM Reactivar la salida de SQL*Plus:


set termout on
Además cuando estoy aburrido del tipo de letra tipo Terminal de sqlplusw.exe o necesito ver más datos
por pantalla cambio su tipo y tamaño definiendo en el entorno del sistema, antes de ejecutarlo esto:
set SQLPLUS_FONT= Courier New
set SQLPLUS_FONT_CHARSET=WestEurope
set SQLPLUS_FONT_SIZE=16

Obviamente existe un amplio manual en el que se muestran en resto de opciones útiles que se pueden
parametrizar en SQL*Plus.
Explain Plan.-
Explain Plan es un comando SQL que se usa para que Oracle retorne el plan de ejecución que tendría una
sentencia SQL si fuera ejecutada ahora mismo. Es importante entender que el plan ejecutado sería si lo
ejecutáramos en la sesión actual, con los ajustes actuales.

Explain Plan no puede retornar el plan de ejecución que fue usado por una sentencia en el pasado porque la
ejecución de esa sentencia tuvo lugar en una sesión diferente con ajustes diferentes. Por ejemplo una consulta
ejecutada en una sesión con un valor alto de sort area puede usar un plan de ejecución diferente que si se
ejecutara la misma consulta en una sesión con un sort area más pequeña. (Como se verá en este tema Oracle9i
proporciona varias vías para ver el plan de ejecución usado por una sentencia cuando fue ejecutada.)

Preparando Explain Plan


Para ejecutar Explain Plan se requieren antes una serie de scripts de $ORACLE_HOME/ rdbms/ admin :

utlxplan.sql (UTiLity eXplain PLAN table), que contiene la sentencia de creación de la tabla PLAN_TABLE.
En esta tabla Explain Plan almacena los planes de ejecución que solicitados.

utlxplp.sql (UtiLity eXplain Plan Parallel), que muestra los contenidos de la tabla PLAN_TABLE incluyendo
información específica sobre planes de ejecución en paralelo.

utlxpls.sql (UtiLity eXplan Plan Serial), que muestra los contenidos de la tabla PLAN_TABLE para los
planes de ejecución normales, no serializados.

También es importante el paquete DBMS_XPLAN, que se encarga de consultar la tabla PLAN_TABLE de forma
fácil y cómoda. Cualquier usuario puede crear su propia tabla PLAN_TABLE ejecutando el primer script de esta
forma:

@?/rdbms/admin/utlxplan.sql

Aunque lo más preferible puede ser crear una tabla PLAN_TABLE general, única para toda la base de datos y
accesible por todos los usuarios de la siguiente forma en el esquema SYSTEM :

CREATE GLOBAL TEMPORARY table PLAN_TABLE (


statement_id varchar2(30),
timestamp date,
remarks varchar2(80),
operation varchar2(30),
options varchar2(255),
object_node varchar2(128),
object_owner varchar2(30),
object_name varchar2(30),
object_instance numeric,
object_type varchar2(30),
optimizer varchar2(255),
search_columns number,
id numeric,
parent_id numeric,
position numeric,
cost numeric,
cardinality numeric,
bytes numeric,
other_tag varchar2(255),
partition_start varchar2(255),
partition_stop varchar2(255),
partition_id numeric,
other long,
distribution varchar2(30),
cpu_cost numeric,
io_cost numeric,
temp_space numeric,
access_predicates varchar2(4000),
filter_predicates varchar2(4000) )
ON COMMIT PRESERVE ROWS;

CREATE PUBLIC SYNONYM plan_table FOR plan_table;


GRANT ALL ON plan_table TO public;

Como se puede apreciar se creará una tabla temporal que nunca se llenará y en la que cada usuario no verá
ninguna de las acciones de los demás pero podrá operar perfectamente.

Usando Explain Plan


Ahora estamos listos para explicar como funciona una consulta en nuestra tabla PLAN_TABLE. El formato del
comando es muy sencillo:

EXPLAIN PLAN
[ SET statement_id = texto ]
[ INTO [esquema.]tabla ]
FOR [ sentencia SQL ];

El texto entre corchetes es opcional y no lo usaremos en nuestras pruebas. statement_id permite almacenar
varios planes de ejecución diferentes identificados por texto . esquema.tabla permite almacenar el plan de
ejecución en otra tabla si se desea.

Ahora crearemos una tabla de test:

CREATE TABLE t (
collection_year INT,
data VARCHAR2(25) )
PARTITION BY RANGE (collection_year)
(PARTITION PART_99 VALUES LESS THAN (2000) TABLESPACE users,
PARTITION PART_00 VALUES LESS THAN (2001) TABLESPACE users,
PARTITION PART_01 VALUES LESS THAN (2002) TABLESPACE users,
PARTITION PART_02 VALUES LESS THAN (2003) TABLESPACE users,
PARTITION the_rest VALUES LESS THAN (MAXVALUE) TABLESPACE users );

Esto crea una simple tabla particionada por rango. He elegido este tipo de tabla para mostrar porque Explain
Plan es todavía relevante, incluso aunque AutoTrace parece ser mucho más fácil de usar. Ahora realizaré una
consulta sobre esta tabla vacía:

EXPLAIN PLAN FOR


SELECT * FROM t WHERE collection_year = 2002;

Ahora consultaré el plan de ejecución adoptado para esta consulta mediante el script utlxpls.sql :

@?\rdbms\admin\utlxpls
PLAN_TABLE_OUTPUT
---------------------------------------------------------------------------------
---------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Pstart| Pstop |
---------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 27 | 3 (34)| | |
|* 1 | TABLE ACCESS FULL| T | 1 | 27 | 3 (34)| 4 | 4 |
---------------------------------------------------------------------------------

Predicate Information (identified by operation id):


---------------------------------------------------
1 - filter("T"."COLLECTION_YEAR"=2002)

12 filas seleccionadas.

Esto muestra que Oracle evaluó el plan. En pocas palabras diríamos que se realizó un full-table scan sobre
la tabla T. También podemos ver que el coste de realizar este paso fue de 3 (COST= 3), el número esperado de
filas retornadas y cuantos bytes de insformación serán retornados. El optimizador de Oracle está adivinando esta
información ya que no hemos analizado la tabla. También podemos ver que sólo está accediendo a la partición 4,
como se muestra en las columnas Pstart (Partition Start) y Pstop (Partition Stop). Así que aunque se está
realizando un full-table scan no estamos leyendo toda la tabla debido a que se están eliminando particiones de
la consulta. La información sobre los predicados es suministrada por el paquete DBMS_XPLAN y muestra los
criterios que se aplican en cada paso del plan de ejecución.

Una comparación con AutoTrace


Aunque no hemos llegado todavía a tratar la herramienta AutoTrace me adelantaré para ver los beneficios de
Explain Plan sobre ella. Activaré AutoTrace y reejecutaré la sentencia anterior:

SET AUTOTRACE TRACEONLY EXPLAIN


SELECT * FROM t WHERE collection_year = 2002;

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=3 Card=1 Bytes=27)
1 0 TABLE ACCESS (FULL) OF 'T' (Cost=3 Card=1 Bytes=27)

SET AUTOT OFF

Vemos lo fácil que es captar el plan de ejecución de una sentencia, pero ¡también vemos que falta una pieza
de información importante! La información sobre la eliminación de particiones no es mostrada. Con los scripts
Explain Plan hemos tenido información adicional, importante información.

En resumen, se debe considerar usar Explain Plan para ver los planes de ejecución si el plan necesita ser
revisado. AutoTrace normalmente debería servir para ver estadísticas.

Como leer un plan de ejecucion


Con frecuencia soy preguntado de este modo: ¿Cómo debo leer un plan de ejecución? Aquí quiero
presentar mi aproximación a la lectura de un plan. Echaremos un vistazo al resultado del plan de una sentencia
contra tablas del usuario SCOTT . Copiaré sus tablas EMP y DEPT y les añadiré una clave primaria para que estén
indexadas:

ALTER SESSION SET optimizer_mode=RULE;

CREATE TABLE emp2 TABLESPACE users


AS SELECT * FROM emp;

CREATE TABLE dept2 TABLESPACE users


AS SELECT * FROM dept;

CREATE TABLE salgrade2 TABLESPACE users


AS SELECT * FROM salgrade;

ALTER TABLE dept2 ADD CONSTRAINT dept2_pk PRIMARY KEY (deptno) USING INDEX;

ALTER TABLE emp2 ADD CONSTRAINT emp2_fk FOREIGN KEY (deptno) REFERENCES dept2;
EXPLAIN PLAN FOR
SELECT ename, dname, grade
FROM emp2, dept2, salgrade2
WHERE emp2.deptno = dept2.deptno
AND emp2.sal BETWEEN salgrade2.losal AND salgrade2.hisal;

Consultamos el plan de ejecución:

@?\rdbms\admin\utlxpls.sql
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------

--------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
--------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | |
| 1 | NESTED LOOPS | | | | |
| 2 | NESTED LOOPS | | | | |
| 3 | TABLE ACCESS FULL | SALGRADE2 | | | |
|* 4 | TABLE ACCESS FULL | EMP2 | | | |
| 5 | TABLE ACCESS BY INDEX ROWID| DEPT2 | | | |
|* 6 | INDEX UNIQUE SCAN | DEPT2_PK | | | |
--------------------------------------------------------------------------------

Predicate Information (identified by operation id):


---------------------------------------------------

4 - filter("EMP2"."SAL"<="SALGRADE2"."HISAL" AND
"EMP2"."SAL">="SALGRADE2"."LOSAL")
6 - access("EMP2"."DEPTNO"="DEPT2"."DEPTNO")

Note: rule based optimization

¿Cómo podemos imagir que ha sucedido en primer lugar, en segundo lugar, etc.? ¿Cómo ha sido evaluado el
plan? Primero os mostraré el pseudocódigo para la evaluación del plan y luego discutiremos como hemos llegado
a esta conclusión:

FOR salgrade2 IN (SELECT * FROM salgrade2) LOOP


FOR emp2 IN (SELECT * FROM emp2) LOOP
IF(emp2.sal BETWEEN salgrade2.losal AND salgrade2.hisal) THEN
SELECT * INTO dept2_rec
FROM dept2
WHERE dept2.deptno = emp2.deptno;

RETORNO DE LOS DATOS DE UNA FILA ...

END IF;
END LOOP;
END LOOP;

La manera en que he leído este plan es transformándolo en un gráfico de ordenaciones, un árbol de


evaluación. Para ello se deben entender las vías de acceso ( access paths ). Para información detallada sobre
todas las vías de acceso disponibles en Oracle os remito al manual de Oracle Performance and Tuning Guide .
Hay unas cuantas vías de acceso y las descripciones de la guía son muy comprensibles.

Para construir un árbol debemos empezar desde arriba, desde el paso 1, el cual será nuestro nodo-raíz en el
árbol. Después necesitamos hallar todas las cosas que dependen de este nodo-raíz. Eso es lo que ha sido
realizado en los puntos 2 y 5, los cuales están en el mismo nivel de indentación porque cuelgan del paso 1. A
continuación podemos ver que los pasos 3 y 4 proceden del paso 2, y que el paso 6 cuelga del paso 5. Poniendo
todo esto de forma iterativa podemos dibujar este árbol de evaluación:
Leyendo el árbol veremos que para tener el paso 1 antes necesitamos los pasos 2 y 5. Para tener completado
el paso 2 necesitamos que se hayan ejecutado los pasos 3 y 4. Así es como hemos llegado al pseudocódigo:

FOR salgrade2 IN (SELECT * FROM salgrade2) LOOP


FOR emp2 IN (SELECT * FROM emp2) LOOP

El full scan de la tabla SALGRADE2 es el punto 3. El full scan de la tabla EMP2 es el paso 4. El paso 2 es
un bucle anidado (= nested loop ) que podría ser el equivalente a los dos bucles FOR. Una vez que se ha
evaluado el paso 2 según la manera anterior podemos ver el paso 5. El paso 5 ejecuta primero el paso 6. El paso
6 es el paso del escaneo del índice (= index scan ). Estamos tomando la salida del paso 2 y la usamos para
realizar un escaneo por índice. Entonces la salida de ese escaneo es usada para acceder a la tabla DEPT2 por
ROWID. Eso es el resultado del paso 1, nuestro resultado.

Para hacerlo más interesante ejecutaremos una consulta equivalente, pero esta vez mezclaremos el orden de
las tablas en la cláusula FROM. Debido a que estoy usando el optimizador RBO (= Rule Based Optimizer ) el plan
de ejecución se verá afectado. (Esta es una de las razones por las que no debeis querer usar el optimizador
RBO.) El optimizador RBO es sensible al orden de las tablas en la cláusula FROM y las usará en el orden en el que
se le indiquen para elegir una tabla conductora (= driving table ) para la consulta si ninguno de los predicados lo
hace. Usaremos la misma lógica para construir su árbol del plan de ejecución y evaluar como procesa la consulta:

EXPLAIN PLAN FOR


SELECT ename, dname, grade
FROM salgrade2, dept2, emp2
WHERE emp2.deptno = dept2.deptno
AND emp2.sal BETWEEN salgrade2.losal AND salgrade2.hisal;

@?\rdbms\admin\utlxpls

PLAN_TABLE_OUTPUT
---------------------------------------------------------------------------------

---------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
---------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | |
| 1 | NESTED LOOPS | | | | |
| 2 | NESTED LOOPS | | | | |
| 3 | TABLE ACCESS FULL | EMP2 | | | |
| 4 | TABLE ACCESS BY INDEX ROWID| DEPT2 | | | |
|* 5 | INDEX UNIQUE SCAN | DEPT2_PK | | | |
|* 6 | TABLE ACCESS FULL | SALGRADE2 | | | |
---------------------------------------------------------------------------------

Predicate Information (identified by operation id):


---------------------------------------------------

5 - access("EMP2"."DEPTNO"="DEPT2"."DEPTNO")
6 - filter("EMP2"."SAL"<="SALGRADE2"."HISAL" AND
"EMP2"."SAL">="SALGRADE2"."LOSAL")

Note: rule based optimization


Aquí podemos ver que los pasos 2 y 6 cuelgan del paso 1, los pasos 3 y 4 cuelgan del paso 2 y el paso 5
cuelga del paso 4. El árbol de evaluación será como éste:

Empezando con los pasos 3 y 4 el pseudocódigo lógico sería éste:

FOR emp2 IN (SELECT * FROM emp2) LOOP


-- USANDO EL ÍNDICE
SELECT * FROM dept2 WHERE dept2.deptno = emp2.deptno

FOR salgrade IN (SELECT * FROM salgrade2) LOOP


IF(emp2. BETWEEEN salgrade2.losal AND salgrade2.hisal) THEN
MOSTRAR RESULTADO DE UNA FILA
END IF;
END LOOP;
END LOOP;

Y eso es todo. Si dibujáis un árbol gráfico y lo leéis desde abajo hacia arriba, de izquierda a derecha
obtendréis un buen entendimiento del flujo de los datos.

Evitando la trampa de Explain Plan


Explain Plan es una vía para obtener el plan de ejecución de una sentencia SQL como si la ejecutaras
actualmente y en tu sesión actual. No mostrará necesariamente que plan fue usado ayer para ejecutar una
sentencia o que plan será usado si otra sesión lo ejecutase en un futuro. En Oracle9i es fácil ver el plan de
ejecución actual de una sentencia que ha sido ejecutada mediante la vista V$SQL_PLAN.

Como ejemplo ejecutaremos la misma consulta en entornos diferentes de una manera que las
especificaciones de la sesión tendrán una diferencia material en el plan de ejecución. Para ello crearé la tabla T
con un índice y calcularé sus estadísticas:

CREATE TABLE t
TABLESPACE users
AS SELECT * FROM all_objects;

ALTER TABLE t ADD CONSTRAINT t_PK PRIMARY KEY(object_id);

exec dbms_stats.gather_table_stats (user,'T',method_opt=>'FOR ALL COLUMNS SIZE


AUTO',cascade=>TRUE)

Ahora, en una sesión en donde la aplicación ha cambiado el parámetro optimizer_index_cost_adj, un


parámetro que tiene gran influencia sobre el optimizador y sobre los planes que elegirá, un usuario ejecutó:

ALTER SESSION SET optimizer_index_cost_adj = 10;

SELECT * FROM t T1 WHERE object_id > 32000;


Supongamos que estamos interesados en saber como se ejecuta esa consulta a efectos de ajuste
(= "tuning"). Para ello realizaremos un EXPLAIN PLAN en nuestra otra sesión donde el cambio del parámetro no
fue hecho:

EXPLAIN PLAN FOR


SELECT * FROM t T1 WHERE object_id > 32000;

SELECT * FROM table(dbms_xplan.display);

--------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
--------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 17999 | 1476K| 32 |
|* 1 | TABLE ACCESS FULL | T | 17999 | 1476K| 32 |
--------------------------------------------------------------------

Predicate Information (identified by operation id):


---------------------------------------------------
1 - filter("T1"."OBJECT_ID">32000)

Note: cpu costing is off

Aparentemente esta consulta está realizando un "full-table scan", ¿no? Usando la información de la vista
V$SQL_PLAN podemos asegurarlo. En lugar de usar el comando EXPLAIN PLAN llenaremos la tabla PLAN_TABLE
con la información de esta consulta que está en la vista V$SQL_PLAN.

DELETE FROM PLAN_TABLE;

INSERT INTO plan_table


(statement_id,timestamp,remarks,operation,options,object_node,
object_owner,object_name,optimizer,search_columns,id,parent_id,
position,cost,cardinality,bytes,other_tag,partition_start,partition_stop,
partition_id,other,distribution,cpu_cost,io_cost,temp_space)
SELECT RAWTOHEX(address)|| _ ||child_number,sysdate, null, operation, options,
object_node, object_owner,object_name,optimizer,search_columns,id,parent_id,
position,cost,cardinality,bytes,other_tag,partition_start,partition_stop,
partition_id,other,distribution,cpu_cost,io_cost,temp_space
FROM v$sql_plan
WHERE (address,child_number) IN
(SELECT address,child_number FROM v$sql
WHERE sql_text = SELECT * FROM t T1 WHERE object_id > 32000
AND child_number = 0);

@?\rdbms\admin\utlxpls

---------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
---------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | |
| 1 | TABLE ACCESS BY INDEX ROWID | T | 235| 22560 | 21 (5)|
| 2 | INDEX RANGE SCAN | T_PK | 235| | 0 (0)|
---------------------------------------------------------------------------------

Aquí vemos que el plan de ejecución real fue un "index-range scan", no un "full-tble scan". Eso no es lo que
nos dijo Explain Plan. Por tanto la diferencias ambientales del entorno pueden tener un profundo efecto en un
plan de ejecución. Pueden ser tan sutiles como el simple cambio de un "full-table scan" a un "index-range scan"
como este, o llegando más lejos consultando diferentes objetos (por ejemplo la tabla T es A.T cuando la consulto
yo, pero es B.T cuando la consulta otro).
Para estar seguro de que se está viendo el plan real en Oracle9i es recomendable capturarlo desde la vista
dinámica V$SQL_PLAN. Alternativamente se puede encontrar en el archivo generado por SQL_TRACE, si se tiene
acceso a él, usando TKPROF para formatearlo. Esta técnica será discutida más adelante.

Usando DBMS_XPLAN y V$SQL_PLAN


Si se edita el script 'utlxpls.sql' se describirá que únicamente contiene una larga línea como ésta:

SELECT plan_table_output
FROM table( dbms_xplan.display('plan_table', null, 'serial'));

Si se edita el mismo script en otras versiones se verá una consulta grandísima. DBMS_XPLAN.DISPLAY es la
mejor manera de consultar y mostrar un plan de ejecución. Esta una función que simplemente retorna una
colección, la cual es la salida formateada de EXPLAIN PLAN más alguna información suplemental al final del
informe. Esto último es uno de los beneficios de usar el paquete DBMS_XPLAN.DISPLAY.

No obstante, si no se tiene acceso al script 'utlxpls.sql' la simple sentencia mostrada a continuación realizará
la misma función. De hecho el paquete DBMS_XPLAN es tan bueno ajustando su salida según las entradas que no
necesitamos especificar ninguna entrada, así como hace el script 'utlxpls.sql':

SELECT * FROM table(dbms_xplan.display);

Usando esta característica juntamente con la vista dinámica V$SQL_PLAN se pueden mostrar los planes de
ejecución de sentencias ya ejecutadas directamente desde la base de datos.

En la sección previa demostré como se puede usar un INSERT en la tabla PLAN_TABLE y ejecutar después el
script 'utlxpls.sql' o 'utlxplp.sql' para ver el plan. En Oracle9i rev.2 usando DBMS_XPLAN y creando una vista se
vuelve aún más fácil. Usando un esquema que ha recibido el permiso de SELECT sobre SYS.V_$SQL_PLAN se
puede crear esta vista:

CREATE OR REPLACE VIEW dynamic_plan_table AS


select RAWTOHEX(address) || '_' || child_number statement_id,
sysdate timestamp, operation, options, object_node, object_owner,
object_name, 0 object_instance, optimizer, search_columns, id,
parent_id, position, cost, cardinality, bytes, other_tag,
partition_start, partition_stop, partition_id, other, distribution,
cpu_cost, io_cost, temp_space, access_predicates,filter_predicates
FROM v$sql_plan;

Ahora se puede consultar cualquier plan de ejecución en la base de datos con una simple consulta sobre esta
vista:

SELECT plan_table_output
FROM table(dbms_xplan.display ( 'dynamic_plan_table',
(SELECT RAWTOHEX(address)||'_'||child_number x
FROM v$sql
WHERE sql_text='SELECT * FROM t T1 WHERE object_id > 32000' ),
'serial' ));

PLAN_TABLE_OUTPUT
------------------------------------------------------------------
| Id | Operation | Name|Rows| Bytes |Cst(%CPU)|
------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | |
| 1 | TABLE ACCESS BY INDEX ROWID| T |291 | 27936 | 25 (0)|
|* 2 | INDEX RANGE SCAN | T_PK|291 | | 2 (0)|
------------------------------------------------------------------

Predicate Information (identified by operation id):


---------------------------------------------------
2 - access("OBJECT_ID">32000)

13 rows selected.

El texto enfatizado en el código SQL es una consulta que toma STATEMENT_ID. En esta consulta se puede
usar cualquier valor para identificar el plan de la consulta exacta que se quiera revisar. El uso de esta técnica
(consultando la tabla V$ en vez de insertar los contenidos de V$SQL_PLAN en una "tabla real") es apropiado si se
está generando el plan de ejecución para esta consulta una única vez. El acceso a las tablas V$ puede ser muy
caro (en términos de uso de cerrojos) en un sistema muy ocupado. Así que si se planea ejecutar el EXPLAIN
PLAN de una sentencia muchas veces, la opción preferida será copiar la información a una tabla temporal de
trabajo.
AUTOTRACE.-
Un primo cercano de EXPLAIN PLAN es AUTOTRACE, que es una característica bastante ingeniosa de
SQL* Plus. EXPLAIN PLAN muestra lo que hará la base de datos cuando se solicite la ejecución de una consulta.
AUTOTRACE muestra cuanto trabajo costará ejecutar esa consulta, proporcionando algunas estadísticas
importantes sobre su ejecución actual. Una de las buenas cosas de AUTOTRACE es que es completamente
accesible a cualquier desarrollador en cualquier momento. TKPROF (que se verá más adelante) es una gran
herramienta de ajuste pero depende del acceso a los archivos de traza del servidor, los cuales pueden no ser
accesibles en todos los entornos.

Yo uso AUTOTRACE como mi primera herramienta de ajuste. Dada una consulta, las entradas representativas
(binds) para ella y el acceso a AUTOTRACE es todo lo que necesito. Ocasionalmente necesito "excavar" un poco
más hondo con TKPROF, pero la mayoría del tiempo AUTOTRACE y SQL*Plus son suficientes.

"Tengo una sentencia con un rendimiento muy pobre, ¿me puedes ayudar?"
Dame la consulta SQL, las variables bind que necesita y dame acceso a tu base de datos con AUTOTRACE
activado.

Es importante que cuando se revisen planes y se ajusten sentencias emular lo que la aplicación hace.
Menciono las variables bind ya que no necesito la consulta 'SELECT * FROM alguna_tabla WHERE column=55'
porque la aplicación ejecuta realmente ' SELECT * FROM alguna_tabla WHERE column=:variable_bind'. No se
puede optimizar una consulta con literales y esperar que una sentencia con variables bind tenga las mismas
características de rendimiento.

Preparando AUTOTRACE
La elegancia de AUTOTRACE está en su simplicidad. Una vez que el DBA prepara AUTOTRACE en una base
de datos cualquiera puede usarlo. Mi método preferido para ajustarlo es como sigue:

1. Entrar en el directorio $ORACLE_HOME/rdbms/admin.


2. Conectar a la base de datos con SQL* Plus mediante un esquema que posea los privilegios CREATE
TABLE y CREATE PUBLIC SYNONYM (por ejemplo como un DBA).
3. Hacer disponible universalmente la tabla PLAN_TABLE (como se vió en la sección anterior).
4. Salir de SQL*Plus y entrar en el directorio $ORACLE_HOME/sqlplus/admin.
5. Conectar a la base de datos con SQL*Plus como SYSDBA (sqlplus "/ as sysdba").
6. Ejecutar: @plustrce.sql
7. Ejecutar: GRANT plustrace TO public.

Haciéndolo público se permite que cualquiera pueda tracear usando SQL* Plus. De ese modo y sin excepción
puede usar AUTOTRACE. ¡Después de todo nadie quiere dar a los programadores una excusa para que ajusten
sus sentencias! (No obstante si se desea se puede reemplazar "public" por un usuario específico).

Usando AUTOTRACE
Ahora que la instalación se ha completado estamos listos para empezar a usar AUTOTRACE. AUTOTRACE
genera un informe después de cada sentencia DML (como INSERT, UPDATE, DELETE, SELECT y MERGE). Se
puede controlar ese informe mediante los siguientes comandos SET de SQL*Plus:

SET AUTOTRACE OFF, no se genera el informe AUTOTRACE. Es el modo por defecto. La consulta se
ejecuta de modo normal.
SET AUTOTRACE ON EXPLAIN, la consulta se ejecuta de modo normal y el informe AUTOTRACE sólo
muestra la ruta de ejecución.
SET AUTOTRACE ON STATISTICS, la consulta se ejecuta de modo normal y el informe AUTOTRACE
sólo muestra las estadísticas de ejecución.
SET AUTOTRACE ON, la consulta se ejecuta de modo normal y el informe AUTOTRACE incluye el plan de
ejecución del optimizador y las estadísticas de ejecución.
SET AUTOTRACE TRACEONLY, es como SET AUTOTRACE ON pero sin mostrar el resultado de la
consulta, Es útil para ajustar una consulta que retorna muchos datos.
SET AUTOTRACE TRACEONLY STATISTICS, como SET AUTOTRACE TRACEONLY pero sin mostrar el
plan de la consulta. Sólo muestra estadísticas.
SET AUTOTRACE TRACEONLY EXPLAIN, es como SET AUTOTRACE TRACEONLY pero sin mostrar las
estadísticas de ejecución, mostrando sólo el plan de la consulta. Además para las sentencias SELECT no
ejecuta la consulta, sólo la parsea y explica. Sin embargo las sentencias INSERT, UPDATE, DELETE y
MERGE son ejecutadas usando este modo.

Estas son sólo algunas de las opciones del comando AUTOTRACE. Consultar el manual de SQL* Plus para
tener más información sobre todas las opciones.

Formato de los resultados de AUTOTRACE


Ahora que AUTOTRACE ya ha sido instalado y sabemos como activarlo vamos a mirar los resultados que
produce. Mostraremos el plan de ejecución y las estadísticas. Como no tenemos que preocuparnos viendo los
datos los suprimiremos. Usaremos SCOTT/ TIGER y las tablas EMPT y DEPT teniendo SET AUTOTRACE
TRACEONLY activado durante la ejecución de la consulta:

SET AUTOTRACE TRACEONLY

SELECT * FROM emp FULL OUTER JOIN dept ON (emp.deptno = dept.deptno);

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=12 Card=384 Bytes=44
1 0 VIEW (Cost=12 Card=384 Bytes=44928)
2 1 UNION-ALL
3 2 HASH JOIN (OUTER) (Cost=6 Card=383 Bytes=25661)
4 3 TABLE ACCESS (FULL) OF 'EMP' (Cost=3 Card=14 Bytes=5
5 3 TABLE ACCESS (FULL) OF 'DEPT' (Cost=3 Card=82 Bytes=
6 2 HASH JOIN (ANTI) (Cost=6 Card=1 Bytes=33)
7 6 TABLE ACCESS (FULL) OF 'DEPT' (Cost=3 Card=82 Bytes=
8 6 TABLE ACCESS (FULL) OF 'EMP' (Cost=3 Card=14 Bytes=4

Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
13 consistent gets
0 physical reads
0 redo size
1582 bytes sent via SQL*Net to client
499 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
15 rows processed

Esta es probablemente la forma más habitual en que uso el comando AUTOTRACE, que viene a decir
"muéstrame las estadísticas pero no los datos". ¿Qué podemos encontrar en el plan de ejecución?
¡Aparentemente se ve un "full outer join" hace un montón de trabajo! Realiza un "outer join" de la tabla EMP
hacia DEPT y después UNION ALL todo eso con un "anti-join" de DEPT hacia EMP. Esa es la definición de un "full
outer join": dame todas y cada una de las filas de ambas tablas independientemente de que tengan o no su
emparejamiento en la otra tabla.

Se tiene un poco de control sobre el formato de este informe. Los valores por defecto (que se hallan en
$ORACLE_HOME/sqlplus/admin/ glogin.sql ) son los siguientes:

column id_plus_exp FOR 990 HEADING i


column parent_id_plus_exp FOR 990 HEADING p
column plan_plus_exp FOR a60
column object_node_plus_exp FOR a8
column other_tag_plus_exp FOR a29
column other_plus_exp FOR a44

Las columnas ID_PLUS_EXP y PARENT_ID_PLUS_EXP son los dos primeros números que se ven en el
resultado del EXPLAIN PLAN de antes. La columna PLAN_PLUS_EXP suele ser la más importante. Contiene la
descripción del paso en si del plan; por ejemplo "TABLE ACCESS (FULL) OF 'DEPT' (Cost= 2 Card= 4 Bytes= 72)".
Personalmente encuentro que 60 caracteres de ancho pueden ser pocos para muchos casos, así que yo los tengo
ampliados hasta 100 en mi 'login.sql'.

Los últimos tres ajustes controlan la información mostrada para planes de ejecución de consultas en paralelo.
La manera más fácil para ver que columnas afectan es ejecutando una consulta en paralelo con SET AUTOTRACE
TRACEONLY EXPLAIN y desactivarlas una a una. Aquí se muestra un script simple que hace esto (asumiendo que
tu sistema está preparado para consultas en parelo):

SET AUTOTRACE TRACEONLY EXPLAIN

SELECT /*+ parallel(emp 2) */ * FROM emp;

column object_node_plus_exp NOPRINT


column other_tag_plus_exp NOPRINT
column other_plus_exp NOPRINT

SET AUTOTRACE OFF

Entendiendo los resultados de AUTOTRACE


Hay dos posibles partes en los resultados de AUTOTRACE: el plan de ejecución y las estadísticas. Mirando
primero al plan veremos que su salida se parece a esta:

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=12 Card=384 Bytes=44928)
1 0 VIEW (Cost=12 Card=384 Bytes=44928)
2 1 UNION-ALL
3 2 HASH JOIN (OUTER) (Cost=6 Card=383 Bytes=25661)
4 3 TABLE ACCESS (FULL) OF 'EMP' (Cost=3 Card=14 Bytes=518)
5 3 TABLE ACCESS (FULL) OF 'DEPT' (Cost=3 Card=82 Bytes=2460)
6 2 HASH JOIN (ANTI) (Cost=6 Card=1 Bytes=33)
7 6 TABLE ACCESS (FULL) OF 'DEPT' (Cost=3 Card=82 Bytes=2460)
8 6 TABLE ACCESS (FULL) OF 'EMP' (Cost=3 Card=14 Bytes=42)

Muestra los resultados de una consulta ejecutada usando el optimizador basado en costo (= CBO). Se puede
decir que el optimizador CBO fue usado por la presencia de información entre paréntesis al final de los pasos: el
COSTe, la CARDinalidad y la cantidad de información en BYTES. En este plan la información CBO representa lo
siguiente:

Cost. El coste asignado a cada paso del plan por el optimizador CBO. Éste trabaja generando diferentes
planes/rutas de ejecución para la misma sentencia y asigna un coste a todas y cada una de ellas. El plan
con el coste menor gana. En el ejemplo anterior podemos ver que el coste de la consulta fue de 10.

Card. Es una abreviatura de "Cardinality" (= cardinalidad). Es el número estimado de filas que retornará
un determinado paso del plan. En el ejemplo anterior podemos ver que el optimizador estimaba que
serían 327 filas de la tabla EMP y 4 filas de la tabla DEPT.

Bytes. Es el tamaño en bytes de los datos que el optimizador CBO estima que retornará cada paso del
plan. Depende del número de filas (Card) y del tamaño estimado de las filas.
Si la información de "Cost", "Card" y "Bytes" no está presente es una señal clara de que la sentencia fue
ejecutada usando el optimizador RBO (= Rule Based Optimizer). Aquí hay un ejemplo que muestra la diferencia
entre usar RBO y CBO:

SET AUTOTRACE TRACEONLY EXPLAIN

SELECT * FROM dual;

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE
1 0 TABLE ACCESS (FULL) OF 'DUAL'

SELECT /*+ FIRST_ROWS */ * FROM dual;

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=HINT: FIRST_ROWS
(Cost=18 Card=8168 Bytes=16336)
1 0 TABLE ACCESS (FULL) OF 'DUAL' (Cost=18 Card=8168 Bytes=16336)

Podemos ver que la primera sentencia contra DUAL usó RBO porque no ha mostrado ni información sobre
Cost, ni Card ni Bytes. El optimizador RBO usa un grupo de reglas para optimizar una sentencia. No se preocupa
sobre el tamaño de los objetos (número de filas o cantidad de datos en bytes). Sólo se preocupa de las
estructuras en la base de datos (índices, clusters, tablas, etc.). Además no usa ni informa sobre los valores de
Cost, Card ni Bytes.

Continuando con la siguiente sección del informe AUTOTRACE podemos ver las siguientes estadísticas:

Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
13 consistent gets
0 physical reads
0 redo size
1582 bytes sent via SQL*Net to client
499 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
15 rows processed

La tabla del siguiente apartado explica brevemente que significa cada uno de estos ítems. Ahora miraremos
estas estadísticas en detalle y veremos que nos pueden contar sobre nuestras sentencias.

¿Qúe se debe mirar en el informe de AUTOTRACE?


Ahora que sabemos como hacer funcionar AUTOTRACE y como ajustar el modo en que se muestran los datos
en el informe, queda una cuestión: ¿qué estamos buscando exactamente? Generalmente miraremos las
estadísticas. Vamos a ver las estadísticas mostradas y que significan:

Estadística Significado
Número de sentencias SQL ejecutadas para completar la
Recursive calls
sentencia.
Db block gets Número total de bloques leídos desde el buffer cache.
Número de veces que una lectura consistente solicitó un bloque
Consistent gets
del buffer cache.
Physical reads Número de lecturas físicas desde los archivos de datos hacia el
buffer cache.
Redo size Cantidad de bytes generados de "redo".
Bytes sent via SQL*Net to client Número total de bytes enviados al cliente por el servidor.
Bytes received via SQL*Net from client Número total de bytes recibidos desde el cliente.
Número total de mensajes SQL*Net enviados a y recibidos por el
SQL*Net roundtrips to/from client cliente. Incluye los "viajes de ida y vuelta" para recuperar datos
de varios grupos de filas.
Sorts (memory) Ordenaciones hechas en la memoria de la sesión.
Ordenaciones que usan el tablespace temporal (en disco) porque
Sorts (disk)
las ordenaciones exceden el tamaño del área de ordenación.
Rows processed Cantidad de filas procesadas.

Recursive Calls. Esta estadística hace referencia a la ejecución de sentencias SQL aparte como un
efecto colateral de la sentencia a analizar. Por ejemplo se tendrán sentencias SQL recursivas si se ejecuta
un INSERT que activa un disparador el cual ejecuta una consulta. Se pueden ver sentencias recursivas
para otras operaciones, como el parseo de una consulta, la solicitud de espacio adicional al trabajar con
el espacio temporal, etc.

Un número alto de "recursive calls" en ejecuciones repetidas (para eliminar de nuestra consideración
el parseo y otros fenómenos de la primera ejecución) es algo a observar, para ver si se pueden reducir o
eliminar esas llamadas. Si puede ser evitado se debe intentar. Indica trabajo adicional, quizás trabajo
adicional innecesario, que se realiza en segundo plano. Miraremos las causas más frecuentes de un alto
número de llamadas recursivas y algunas soluciones.

Hard Parses. Si el número de llamadas recursivas anterior es inicialmente alto se puede ejecutar la
sentencia otra vez y ver si esta estadística se mantiene alta. Si no es así será indicación de que las
sentencias recursivas fueron hechas debido a un "hard parse" (= parseo duro o normal). Considerad el
siguiente ejemplo:

ALTER SYSTEM FLUSH shared_pool;

SET AUTOTRACE TRACEONLY STATISTICS

SELECT * FROM emp;

Statistics
----------------------------------------------------------
446 recursive calls
0 db block gets
77 consistent gets
14 physical reads
0 redo size
1353 bytes sent via SQL*Net to client
499 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
6 sorts (memory)
0 sorts (disk)
14 rows processed

SELECT * FROM scott.emp;

Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
4 consistent gets
0 physical reads
0 redo size
1353 bytes sent via SQL*Net to client
499 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
14 rows processed

Como se puede ver en este caso el 100% del SQL recursivo fue debido al parseo inicial de la consulta.
Oracle necesitó ejecutar muchas consultas (debido a que vaciamos la Shared Pool) para estimar los
objetos accedidos, sus permisos, etc. El número de llamadas recursivas fue desde centenares hasta cero,
y el número de operaciones I/ O lógicas (= "consisten gets") también cayó dramáticamente. Eso fue
debido a no tener que parsear "completamente" la consulta la segunda vez. Ampliemos con unos casos:

llamadas a funciones PL/ SQL, si las llamadas SQL recursivas permanecen altas se necesita
profundizar un poco para determinar el motivo. Una razón puede ser que se esté llamando a una
función PL/ SQL desde la sentencia SQL a analizar, ejecutando esta función muchas sentencias dentro
de si, o que se refiera a pseudocolumnas como USER, que implícitamente usa SQL. Todos las
sentencias SQL ejecutadas en una función PL/SQL cuentan como SQL recursivo. Aquí hay un ejemplo:

CREATE OR REPLACE FUNCTION some_function RETURN NUMBER AS


l_user VARCHAR2(30) DEFAULT user;
l_cnt NUMBER;
BEGIN
SELECT COUNT(*) INTO l_cnt FROM dual;
RETURN l_cnt;
END;
/

El código en negrita será contado como SQL recursivo cuando se ejecute esto. Lo siguiente es el
informe después de ejecutar la consulta (para tenerla parseada):

SET AUTOTRACE TRACEONLY STATISTICS

SELECT ename, some_function FROM emp;

Statistics
----------------------------------------------------------
28 recursive calls
...
14 rows processed

Como se puede ver hay 28 llamadas recursivas o lo que es lo mismo, 2 por cada fila obtenida. Como
posible solución se podría mantener la rutina PL/ SQL dentro de la consulta en si, usando por ejemplo un
CASE complejo o con una SELECT. La consulta anterior podría escribirse simplemente así:

SELECT ename, (SELECT COUNT(*) FROM DUAL) FROM emp;

Esto no incurrirá en sentencias SQL recursivas y realizará la misma operación.

Así como con la variable local USER recomiendo definir eso una vez por sesión (usando un paquete
PL/ SQL) en vez de referirse a esta pseudocolumna USER a través del código. Cada vez que se declara
una variable y un valor por defecto a USER se realizará una llamada SQL recursiva. Es mejor tener una
variable global de paquete cuyo valor por defecto sea USER y hacer referencia a ella.
efectos adversos de las modificaciones, las llamadas recursivas también se pueden producir
cuando se están haciendo modificaciones y muchos efectos secundarios (disparadores, índices de tipo
función, etc.) se producen. Tomemos el siguiente ejemplo:

CREATE TABLE t (x INT) TABLESPACE users;

CREATE TRIGGER t_trigger BEFORE INSERT ON t FOR EACH ROW


BEGIN
FOR x IN (SELECT * FROM DUAL WHERE :new.x > (SELECT COUNT(*) FROM emp))
LOOP
RAISE_APPLICATION_ERROR (-20001, 'check failed' );
END LOOP;
END;
/

INSERT INTO t SELECT 1 FROM all_users;

SET AUTOTRACE TRACEONLY STATISTICS

INSERT INTO t SELECT 1 FROM all_users;

Statistics
----------------------------------------------------------
39 recursive calls

...
38 rows processed

Aquí la activación del disparador y la ejecución de esa consulta para cada fila procesada genera
todas las llamadas recursivas. Generalmente esto no es algo que se pueda evitar (porque necesitarías
eliminar el disparador completamente), pero puedes minimizarlo escribiendo un disparador eficiente
(evitando los SQL recursivos tanto como sea posible y moviendo código SQL fuera del disparador y
hacia un paquete).

peticiones de espacio, podéis ver grandes operaciones SQL recursivas realizadas para satisfacer
peticiones de espacio para realizar ordenaciones en disco o como el resultado de grandes
modificaciones sobre una tabla que las necesita para extenderse. Esto no es un problema
generalmente en los tablespaces manejados localmente, donde el espacio es manejado como
"bitmaps" en el encabezado de los archivos de datos. Puede ser un problema con tablespaces
manejados por diccionario, donde el espacio es manejado en tablas como tus datos mismamente.

Considerad este ejemplo realizado usando un tablespace manejado por diccionario. Empezaremos
creando una tabla con extensiones pequeñas (cada extensión será de 64Kb):

CREATE TABLESPACE testing


DATAFILE 'C:\Oracle\oradata\prod9\testing.dbf' SIZE 1M REUSE
AUTOEXTEND ON NEXT 1M EXTENT MANAGEMENT DICTIONARY;

CREATE TABLE t STORAGE (INITIAL 64K NEXT 64K PCTINCREASE 0)


TABLESPACE testing
AS SELECT * FROM all_objects WHERE 1=0;

Ahora insertaremos un par de filas en esta tabla T. La sentencia INSERT está cuidadosamente
montada usando variables bind para que las posteriores ejecuciones de esa consulta sean parseadas
mínimamente (= soft parse), y así el SQL recursivo que intentamos medir no sea incluido en
sentencias recursivas por parseo. Considerad este paso como el cebado de una bomba, para calentar
el motor:

VARIABLE n NUMBER
exec :n := 5

INSERT INTO t SELECT * FROM all_objects WHERE rownum < :n;

Y ahora estamos listos para realizar una inserción masiva en esta tabla. Estableciendo el valor de
la variable bind a un número grande y simplemente reejecutando el mismo INSERT:

SET AUTOTRACE TRACEONLY STATISTICS

exec :n := 99999

INSERT INTO t SELECT * FROM all_objects WHERE rownum < :n;

Statistics
----------------------------------------------------------
2910 recursive calls
2441 db block gets

...
23698 rows processed

Hay un montón de SQL recursivo para esa operación de INSERT. No están involucrados
disparadores o llamadas a funciones PL/SQL. Esto es debido al manejo del espacio. ¿Cómo podemos
reducirlo fácilmente? Seguramente usando tablespace manejados localmente:

NOTA: El siguiente ejemplo no funcionará en Oracle9i ver.2 si el tablespace SYSTEM


fue creado del tipo manejado localmente.

CREATE TABLESPACE testing_lmt


DATAFILE 'C:\Oracle\oradata\prod9\testing.dbf' SIZE 1M REUSE
AUTOEXTEND ON NEXT 1M EXTENT MANAGEMENT LOCAL UNIFORM SIZE 64K;

DROP TABLE t;

CREATE TABLE t TABLESPACE testing_lmt


AS SELECT * FROM all_objects WHERE 1=0;

Esto emula nuestra tabla de ejemplo manejada por diccionario de forma exacta. La tabla T tendrá
extensiones de 64Kb (observar que se heredan los valores del tablespace). Ahora repetiremos el
mismo test de inserciones:

VARIABLE n NUMBER

exec :n := 5

INSERT INTO t SELECT * FROM all_objects WHERE rownum < :n;

SET AUTOTRACE TRACEONLY STATISTICS

exec :n := 99999

INSERT INTO t SELECT * FROM all_objects WHERE rownum < :n;

Statistics
----------------------------------------------------------
800 recursive calls
2501 db block gets
...
23698 rows processed
Eso está mucho mejor. ¿Qué son las 800 consultas SQL recursivas? Usando SQL_TRACE y
TKPROF para analizarlo (como se explicará en la sección posterior de TKPROF) podríamos ver que
son debidas al manejo de cuotas en el tablespace (una serie de sentencias SELECT y UPDATE para
manejar la información de cuota). Estos SQL recursivos son verdaderamente inevitables. Oracle
siempre chequea nuestras cuotas por si necesitamos más espacio. Todavía podríamos ser capaces de
reducir estos SQL recursivos usando extensiones asignadas por el sistema o usando un tamaño de
extensión uniforme más grande para reducir el número de extensiones necesarias para almacenar los
datos de la tabla.

Db Block Gets y Consistent Gets. Los bloques pueden ser recuperados y usados por Oracle de una de
estas dos maneras: current o consistente. Un current mode get es la recuperación de un bloque así
como existe ahora. Veréis la mayoría de estos durante las sentencias de modificación, las cuales
actualizan sólo la última copia del bloque. Los consisten gets son recuperaciones de bloques desde el
buffer cache en el modo lectura consistente y puede incluir lecturas de UNDO (de los segmentos de
ROLLBACK). Una consulta SQL generalmente realizará consistent gets .

Estas dos son las partes más importantes del informe AUTOTRACE. Representan las operaciones lógicas
I/ O (= el número de veces que se bloquea un buffer mediante un cerrojo para inspeccionarlo). Cuantos
menos cerrojos se activen, mejor, por lo que cuantas menos operaciones I/ O lógicas realicemos, mejor.
¿Qué podemos hacer?

- ajuste de sentencias, ¿cómo reducir las operaciones I/ O lógicas? En muchos casos conseguir esto
requiere dejar atrá antigüos mitos, en particular el mito de que si tu sentencia no está usando índices
el optimizador está haciendo algo mal.

"He creado dos tablas y una tabla de mapeo:


CREATE TABLE i1(n NUMBER PRIMARY KEY, v VARCHAR2(10)) TABLESPACE users;
CREATE TABLE i2(n NUMBER PRIMARY KEY, v VARCHAR2(10)) TABLESPACE users;

CREATE TABLE map (


n NUMBER PRIMARY KEY,
i1 NUMBER REFERENCING i1(n),
i2 NUMBER REFERENCING i2(n))
TABLESPACE users;

CREATE UNIQUE INDEX idx_map ON map(i1, i2) TABLESPACE users;

lanzo la siguiente consulta:

SELECT * FROM i1, map, i2


WHERE i1.n=map.i1
AND i2.n=map.i2
AND i1.v = 'x'
AND i2.v = 'y';

y obtengo este plan de ejecución con EXPLAIN PLAN:

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE
1 0 NESTED LOOPS
2 1 NESTED LOOPS
3 2 TABLE ACCESS (FULL) OF 'MAP'
4 2 TABLE ACCESS (BY INDEX ROWID) OF 'I2'
5 4 INDEX (UNIQUE SCAN) OF 'SYS_C00683648' (UNIQUE)
6 1 TABLE ACCESS (BY INDEX ROWID) OF 'I1'
7 6 INDEX (UNIQUE SCAN) OF 'SYS_C00683647' (UNIQUE)
¿Hay alguna manera de evitar el "full-table scan" sobre la tabla MAP? De cualquier manera como
lo intente una tabla siempre es escaneada por completo. ¿Qué debería hacer para evitar un "full
scan" es este caso?"

Mi respuesta fue simple. Empecé contándole que repitiera conmigo:

Los "full scans" no siempre son el demonio, los índices no siempre son buenos.

Diciendo esto una vez y otra comenzó a creerlo. Después le dije que mirara la consulta y que me
dijera como podía ser evitado un "full scan". Usando las estructuras de datos existentes, ¿qué plan se
acercaría a ese sin implicar un "full-table scan" o un "index scan"? Por mi mismo no veo ninguno.

Adicionalmente dadas las estrucutras existentes, los índices que actualmente son usados son
mortales aquí. Viendo el informe de AUTOTRACE puedo afirmar que está usando el optimizador RBO
(porque no hay información sobre Cost, Card ni Bytes). El plan que el optimizador RBO encontró es
realmente un pésimo plan. El optimizador CBO puede ser más inteligente y dejar de usar los índices.
¡Así que una solución es simplemente analizar las tablas y usar un plan que evite los índices!

Aquí veremos un ejemplo simple. Uniremos dos tablas. Antes de ejecutar la consulta usaremos
AUTOTRACE para ver los planes que serán generados e intentar anticiparnos al optimizador con
"hints" (en la errónea creencia de que si el optimizador obvia un índice es algo malo).

INSERT INTO i1 SELECT rownum, RPAD('*',10,'*') FROM all_objects;


INSERT INTO i2 SELECT rownum, RPAD('*',10,'*') FROM all_objects;
INSERT INTO map SELECT rownum, rownum, rownum FROM all_objects;

SET AUTOTRACE TRACEONLY


EXPLAIN PLAN FOR
SELECT * FROM i1, map, i2
WHERE i1.n = map.i1
AND i2.n = map.i2
AND i1.v = x
AND i2.v = y ;

no rows selected

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE
1 0 NESTED LOOPS
2 1 NESTED LOOPS
3 2 TABLE ACCESS (FULL) OF 'MAP'
4 2 TABLE ACCESS (BY INDEX ROWID) OF 'I2'
5 4 INDEX (UNIQUE SCAN) OF 'SYS_C003755' (UNIQUE)
6 1 TABLE ACCESS (BY INDEX ROWID) OF 'I1'
7 6 INDEX (UNIQUE SCAN) OF 'SYS_C003754' (UNIQUE)

Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
60127 consistent gets
0 physical reads
60 redo size
513 bytes sent via SQL*Net to client
368 bytes received via SQL*Net from client
1 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
0 rows processed

En este punto podemos ver que el rendimiento de esta consulta es pobre, con más de 60.000
operaciones I/ O lógicas. Para una consulta que últimamente no retorna filas esto es mucho. Ahora
vamos a darle una oportunidad al optimizador CBO:

exec dbms_stats.gather_table_stats (user,'I1')


exec dbms_stats.gather_table_stats (user,'I2')
exec dbms_stats.gather_table_stats (user,'MAP')

SET AUTOTRACE TRACEONLY


EXPLAIN PLAN FOR
SELECT * FROM i1, map, i2
WHERE i1.n = map.i1
AND i2.n = map.i2
AND i1.v = x
AND i2.v = y ;

no rows selected

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=21 Card=1 Bytes=40)
1 0 NESTED LOOPS (Cost=21 Card=1 Bytes=40)
2 1 HASH JOIN (Cost=20 Card=1 Bytes=26)
3 2 TABLE ACCESS (FULL) OF 'I1' (Cost=10 Card=1 Bytes=14)
4 2 TABLE ACCESS (FULL) OF 'MAP' (Cost=9 Card=30020 Bytes=360240)
5 1 TABLE ACCESS (BY INDEX ROWID) OF 'I2' (Cost=1 Card=1 Bytes=14)
6 5 INDEX (UNIQUE SCAN) OF 'SYS_C003755' (UNIQUE)

Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
92 consistent gets
0 physical reads
0 redo size
513 bytes sent via SQL*Net to client
368 bytes received via SQL*Net from client
1 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
0 rows processed

Como se puede ver evitando el uso de un índice hemos incrementado el rendimiento y uso de
recursos de esta consulta en varias unidades. Este es un gran comienzo.

Ahora vamos a considerar los predicados i1.v = value y i2.v = value. Tal vez pueda
ayudar el crear un índice en i1.v o en i2.v:

CREATE INDEX i1_idx ON i1(v) TABLESPACE users;


exec dbms_stats.gather_table_stats (user,'I1',cascade=>TRUE)

SET AUTOTRACE TRACEONLY


EXPLAIN PLAN FOR
SELECT * FROM i1, map, i2
WHERE i1.n = map.i1
AND i2.n = map.i2
AND i1.v = x
AND i2.v = y ;
no rows selected

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=13 Card=1 Bytes=40)
1 0 NESTED LOOPS (Cost=13 Card=1 Bytes=40)
2 1 HASH JOIN (Cost=12 Card=1 Bytes=26)
3 2 TABLE ACCESS (BY INDEX ROWID) OF 'I1' (Cost=2 Card=1 Bytes=14
4 3 INDEX (RANGE SCAN) OF 'I1_IDX' (NON-UNIQUE) (Cost=1 Card=1)
5 2 TABLE ACCESS (FULL) OF 'MAP' (Cost=9 Card=30020 Bytes=360240)
6 1 TABLE ACCESS (BY INDEX ROWID) OF 'I2' (Cost=1 Card=1 Bytes=14)
7 6 INDEX (UNIQUE SCAN) OF 'SYS_C003755' (UNIQUE)

Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
2 consistent gets
0 physical reads
0 redo size
513 bytes sent via SQL*Net to client
368 bytes received via SQL*Net from client
1 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
0 rows processed

Esto ayuda a demostrar que los índices no son siempre buenos y que los full scans no siempre
deben ser evitados. Esta solución a este problema es usar el optimizador CBO e indexar debidamente
las estructuras de datos de acuerdo a nuestras necesidades de recuperaciones de datos.

En general la principal manera de reducir los db block gets y los consistent gets es ajustando
las consultas. Sin embargo para tener éxito se debe mantener una mente abierta y tener un buen
conocimiento de las vías de acceso disponibles. Se debe tener un buen dominio del lenguaje SQL,
incluyendo toda la funcionalidad, para poder entender la diferencia entre NOT IN y NOT EXISTS,
cuando WHERE EXISTS será apropiado y cuando WHERE IN será una mejor elección. Una de las
mejores vías para descubrir todo es a través de tests simples como los que estamos haciendo.

- efectos del tamaño de array, es el número de filas recogidas cada vez (o enviadas en el caso de
operaciones INSERT, UPDATE y DELETE) por el servidor en un momento dado. Puede tener un efecto
dramático sobre el rendimiento

Para demostrar los efectos del tamaño del array ejecutaremos la misma sentencia un par de
veces y veremos las diferencias de consistent gets entre ejecuciones:

CREATE TABLE t TABLESPACE users AS SELECT * FROM all_objects;

SET AUTOTRACE TRACEONLY


SET ARRAYSIZE 2
SELECT * FROM t;

29352 rows selected.

Statistics
----------------------------------------------------------
14889 consistent gets

Notad como la mitad de 29.352 (filas recogidas) es muy cercana a 14.889 (el número de
consistent gets ). Cada fila que hemos recogido del servidor ha causado que retornaran dos filas. Así
por cada dos filas de datos hemos necesitado hacer una operación I/ O lógica para recuperar los
datos. Oracle obtuvo un bloque, tomó dos filas de él y las envió por SQL* Plus. Después SQL*Plus
solicitó las siguientes dos filas y Oracle obtuvo ese bloque otra vez (o uno diferente) y retornó las
siguientes dos filas de datos, así sucesivamente. Ahora vamos a incrementar el tamaño del array y
repetimos el test:

SET ARRAYSIZE 5
SELECT * FROM t;
29352 rows selected.

Statistics
----------------------------------------------------------
6173 consistent gets

Ahora 29.352 dividido entre 5 es casi 5.871 y ese será la mínima cantidad de consistent gets
que seremos capaz de conseguir (el número actual observado de consistent gets es ligeramente
superior). (A veces para recuperar dos filas necesitamos leer dos bloques: leer la última fila de un
bloque y leer la otra desde otro bloque). Incrementemos el tamaño del array otra vez:

SET ARRAYSIZE 10
SELECT * FROM t;

29352 rows selected.

Statistics
----------------------------------------------------------
3285 consistent gets

SET ARRAYSIZE 15
SELECT * FROM t;

29352 rows selected.

Statistics
----------------------------------------------------------
2333 consistent gets

SET ARRAYSIZE 100


SELECT * FROM t;

29352 rows selected.

Statistics
----------------------------------------------------------
693 consistent gets

SET ARRAYSIZE 5000


SELECT * FROM t;

29352 rows selected.

Statistics
----------------------------------------------------------
410 consistent gets

SET AUTOTRACE OFF


Como podéis ver así como se incrementa el tamaño del array el número de consistent gets
disminuye. Entonces, ¿significa esto que se debe situar el tamaño del array en 5.000 y olvidarnos?
Absolutamente no.

Si os fijáis la media de consistent gets no ha caído dramáticamente entre los tamaños de array
100 y 5.000. Sin embargo la cantidad de memoria RAM necesitada en nuestro cliente y en el servidor
se ha incrementado con los incrementos de tamaño del array. El cliente debe ser capaz de cachear
5.000 filas. No sólo eso, sino que parece que el rendimiento aumenta engañosamente: el servidor
trabaja de forma dura y rápida para procesar 5.000 filas, el cliente también trabaja igual para
procesar esas 5.000 filas, y el servidor trabaja más aún y luego el cliente, etc. Sería mejor poner un
poco de orden en el flujo de información: pedir 100 filas, obtener 100 filas, procesar 100 filas, y
vuelta a empezar. De esa manera el cliente y el servidor estarían procesando datos de manera fluida
continuamente, en vez de realizar el proceso en pequeños golpes .

NOTA: Una crítica muy común que se hace al PL/ SQL es su lento rendimiento. Sin
embargo esto no es un problema del PL/SQL por si mismo, sino que es debido a que de
hecho mucha gente programa en PL/ SQL orientado hacia una-fila-cada-vez . Dado que
incluso el SQL dinámico nativo puede ser programado como bulk no hay razón para
usar este método de trabajo.

He comprobado empíricamente que un valor entre 100 y 500 es un buen tamaño para el array.
Con tamaños más grandes el rendimiento baja. Virtualmente cada entorno de programación por el
que he pasado (desde Pro* C a OCI, Java/ JDBC e incluso VB/ ODBC) permite especificar el tamaño del
array.

Physical reads. Esta estadística es una medida de cuantas operaciones reales I/O (= operaciones físicas
I/O) está realizando tu consulta. La lectura física de los datos de una tabla o de un índice se producen en
el bloque dentro del buffer cache. Después se realiza una operación I/ O lógica para recuperar el bloque.
Por lo tanto muchas lecturas físicas son seguidas inmediatamente por una lectura lógica. Hay dos tipos
principales de operaciones I/O físicas:

- lectura de los datos desde los archivos de datos, a través de operaciones I/O. Estas
operaciones son seguidas inmediatamente por operaciones I/O lógicas a la cache.

- lecturas directas desde TEMP, como respuesta a un sort area o a una hash area que no son lo
suficientemente grandes para albergar u ordenar los datos en memoria. Oracle se ve forzado a
realizar swap con algunos de los datos hacia el tablespace TEMP y leerlos después. Estas lecturas
físicas se saltan el buffer cache y no provocarán I/O lógico.

No hay mucha cosa que podamos hacer con el primer tipo de I/ O, o sea, la lectura de los datos
desde el disco. Después de todo si no está en el buffer cache debe ser obtenida de algún sitio. Si se
ejecuta una consulta pequeña, una consulta que realiza cientos de I/ O lógicas, y observamos
repetidamente que se realizan operaciones I/ O físicas, puede ser una buena indicación de que el
buffer cache se ha quedado corto (no hay suficiente espacio para cachear todos los resultados de
nuestra pequeña consulta con cientos de operaciones I/ O lógicas). Generalmente el número de
lecturas físicas debe reducirse para la mayoría de consultas después de la primera ejecución.

Mucha gente piensa que deben vaciar el buffer cache antes de ajustar una sentencia para emular
el mundo real. Yo pienso justo lo contrario. Si ajustáis con las operaciones I/ O lógicas (= consistent
gets ) en mente las I/ O físicas se cuidarán ellas mismas con el paso del tiempo (asumiendo que os
aseguréis de que hay una distribución I/ O equitativa entre los discos). Vaciando el buffer cache
durante el ajuste de sentencias es tan artificial como ejecutar la consulta un par de veces y usar los
últimos resultados. Es como echar por la borda toneladas de información que debieran estar allí en el
mundo real cuando se ejecutara la sentencia.

Yo sugiero ejecutar la consulta dos veces. Si las I/ O lógicas son pocas en proporción al buffer
cache y todavía estáis viendo lecturas físicas, es señal de que se están produciendo lecturas directas
desde el tablespace temporal. En esta sección daremos un vistazo a la solución de este problema.
Cuando hablemos después de TKPROF y SQL_TRACE entraremos en más detalles para encontrar en
donde se están produciendo las I/O físicas.

Para detectar las lecturas directas desde TEMP primero hay que desactivar el manejo automático
de la PGA (= Program Global Area ) y establecer físicamente el sort area y el hash size . Al estar
el parámetro WORKAREA_SIZE_POLICY= AUTO los parámetros sort area y hash size son
ignorados. En este ejemplo he declarado el hash size artificialmente bajo para obligar al optimizador
a elegir un sort merge join para ejercitar la sort area . Primero mostraré el plan de la consulta para
la primera ejecución:

ALTER SESSION SET workarea_size_policy = MANUAL;


ALTER SESSION SET hash_area_size = 1024;
ALTER SESSION SET sort_area_retained_size = 65565;
CREATE TABLE t TABLESPACE users AS SELECT * FROM all_objects;

exec dbms_stats.gather_table_stats (user,'T',


method_opt=>'FOR COLUMNS object_id SIZE AUTO',cascade=>TRUE)

Esto prepara nuestro test. Ahora realizaremos una gran ordenación con áreas de 100Kb, 1Mb y
10Mb para ver que sucede en cada caso:

SET AUTOTRACE TRACEONLY


ALTER SESSION SET sort_area_size = 102400;
SELECT * FROM t T1, t T2 WHERE T1.object_id = T2.object_id;

29366 rows selected.

Obvio estos resultados ya que el segundo parseo y el calentamiento de la cache son relevantes:

/
29366 rows selected.

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=4264 Card=29366 Bytes=5638272)
1 0 MERGE JOIN (Cost=4264 Card=29366 Bytes=5638272)
2 1 SORT (JOIN) (Cost=2132 Card=29366 Bytes=2819136)
3 2 TABLE ACCESS (FULL) OF 'T' (Cost=42 Card=29366 Bytes=2819136)
4 1 SORT (JOIN) (Cost=2132 Card=29366 Bytes=2819136)
5 4 TABLE ACCESS (FULL) OF 'T' (Cost=42 Card=29366 Bytes=2819136)

Statistics
----------------------------------------------------------
0 recursive calls
216 db block gets
810 consistent gets
4486 physical reads
0 redo size
2474401 bytes sent via SQL*Net to client
22026 bytes received via SQL*Net from client
1959 SQL*Net roundtrips to/from client
0 sorts (memory)
2 sorts (disk)
29366 rows processed

Podemos ver que las lecturas físicas exceden con mucho las lecturas lógicas. Ese es un indicador
de que se está haciendo swap al disco. Las estadística 2 sorts (disk) ayuda apuntar esto pero
no hay que fijarse en esta estadística para afirmar esto. Otras operaciones que usan espacio temporal
(somo un hash join ) no reportan ordenaciones a disco porque no ordenan.Si las lecturas físicas no
desaparecen en la segunda ejecución, habiendo incluso sólo unos cientos de I/O lógicas (sería normal
esperar que los datos ya estuviesen en la cache), o las lecturas físicas sobrepasan las I/ O lógicas, se
debe sospechar de que la memoria asignada ha sido sobrepasada.

Pero ¿qué sucede cuando incrementamos el área de ordenación para este proceso?

ALTER SESSION SET sort_area_size = 1024000;


SELECT * FROM t T1, t T2 WHERE T1.object_id = T2.object_id;

29366 rows selected.

Statistics
----------------------------------------------------------
0 recursive calls
12 db block gets
810 consistent gets
1222 physical reads
0 redo size
2474401 bytes sent via SQL*Net to client
22026 bytes received via SQL*Net from client
1959 SQL*Net roundtrips to/from client
0 sorts (memory)
2 sorts (disk)
29366 rows processed

Las lecturas físicas se redujeron. Fuimos capaces de almacenar diez veces más los datos en la
RAM. Hicimos mucho menos swap hacia TEMP. Vamos a aumentar el área de ordenación y ver que
pasa:

ALTER SESSION SET sort_area_size = 10240000;


SELECT * FROM t T1, t T2 WHERE T1.object_id = T2.object_id;

29366 rows selected.

Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
810 consistent gets
0 physical reads
0 redo size
2474401 bytes sent via SQL*Net to client
22026 bytes received via SQL*Net from client
1959 SQL*Net roundtrips to/from client
2 sorts (memory)
0 sorts (disk)
29366 rows processed

La necesidad de lecturas físicas ha desaparecido completamente.

Muchos servidores tienen un área de ordenación absurdamente pequeña (y hash area y otras).
Han sido configurados en la creencia errónea de que estableciendo el área de ordenación están
asignando permanentemente esa memoria por cada sesión. El parámetro sort area size controla
cuanta memoria será dinámicamente asignada en tiempo de ejecución para satisfacer una petición de
ordenación. Después de que la ordenación se ha completado esta memoria será reducida al valor
definido por el parámetro sort area retained size . Los parámetros por defecto son muy pequeños e
incluso un aumento modesto puede beneficiar a muchos servidores. Su rendimiento total puede ser
mejorado aumentando este parámetro: sort_area_size. El optimizador reconocerá que el área de
ordenación es más grande y generará planes de ejecución de consultas tomando ventaja de eso.

En Oracle9i y sus versiones superiores, en las que se está usando el work area size policy
automatizado, el área de ordenación no es un problema. Bajo esta política el DBA le está diciendo a
Oracle cuanta memoria dinámica está permitido usar del sistema operativo para el procesamiento
(separado de la memoria SGA). En base a eso Oracle establecerá las áreas de ordenación
automáticamente, usando como máximo el valor del parámetro pga_aggregate_target. De hecho
la cantidad usada puede variar en una sesión de una sentencia a otra así como la carga suba y baje
con el tiempo. La decisión en la cantidad de memoria a usar es hecha dinámicamente para cada única
sentencia que se realiza en una sesión.

Redo Size. La estadística redo size muestra cuanta información redo genera una sentencia cuando se
ejecuta. Esto es muy útil cuando se juzga la eficacia de las grandes operaciones bulk . Se suele producir
mayormente con operaciones INSERT del tipo direct path o con sentencias CTAS. La cantidad de redo
generado por las operaciones MERGE, UPDATE o DELETE estará en muchos casos fuera de nuestro
control. Si se realizan estas operaciones con los mínimos índices activados es posible que se reduzca la
cantidad de redo generado, pero después será necesario reconstruir los índices desactivados ya que
lógicamente los datos habrán cambiado. Esto sólo es útil en entornos especializado cuando se realizan
grandes cargas de datos, como en los warehouse.

- cargas bulk , con frecuencia son la colución a un problema común: ¿cómo cargar un gran número
de filas en una tabla existente de forma eficaz? Si estamos hablando de un par de miles de filas en
una tabla muy grande una inserción sencilla será probablemente la mejor solución. Si estamos
hablando de decenas, centenares, miles o millones de filas de datos en una gran tabla prevalecerán
otros métodos.

En nuestro entorno de desarrollo nuestras inserciones bulk se hacen muy deprisa y generan
muy poca información de redo . Cuando las realizamos en producción nuestras operaciones
INSERT /*+ APPEND */ tardan muchísimo más y generan varios Gbytes de redo log ,
llenando el destino de archivado. ¿Qué hacemos mal?

Realmente nada está yendo mal. Únicamente que mucha gente prueba los desarrollos en servidores
que están en modo NOARCHIVELOG cuando los servidores de producción están en el modo
ARCHIVELOG. Esta simple diferencia puede ser crucial y es otra razón por la que el entorno de desarrollo
debe ser calcado al de producción. ¡Queremos detectar esas diferencias en la fase de desarrollo y
pruebas, no en la de paso a producción!

Vamos a ver un ejemplo de una carga bulk de datos en una tabla. Dado que todo el mundo tiene
acceso a la tabla ALL_OBJECTS la usaremos en nuestra demostración. En el mundo real estaríamos
cargando datos desde una tabla externa o procesando un archivo con SQLLoader. Empezaremos creando
una tabla y cargando datos en ella:

CREATE TABLE big_table TABLESPACE users


AS SELECT * FROM all_objects WHERE 1=0;

Esto sólo ha creado una tabla con la misma estructura que ALL_OBJECTS pero sin datos. Vamos a
llenarla:

SET AUTOTRACE ON STATISTICS


INSERT INTO big_table SELECT * FROM all_objects;

29368 rows created.

Statistics
----------------------------------------------------------
...
3277944 redo size
INSERT /*+ APPEND */ INTO big_table SELECT * FROM all_objects;

29368 rows created.

Statistics
----------------------------------------------------------
...
3328820 redo size

COMMIT;

Esto es algo que confunde a muchos programadores Oracle. Piensan que la segunda operación
INSERT no debería haber generado ningún redo . Hemos usado la sintaxis para el INSERT direct-path
(justo como lo hace una carga direct-path de SQLLoader), así que debería haber evitado la generación
de redo . Pero este no es el caso. Un INSERT direct-path sólo evitará la generación de redo en estos
dos casos:

- cuando se está usando una base de datos que está en modo NOARCHIVELOG (la mía está en
modo ARCHIVELOG);
- cuando se realiza la operación sobre una tabla que está marcada como NOLOGGING;

El INSERT /*+ APPEND */ minimizará la generación de redo en todos los casos ya que minimiza
la cantidad de UNDO generado. El redo que de otra forma será generado por la información UNDO no
es creado ahora, pero en última instancia será el modo logging de la tabla quien decida si se debe
generar redo para la tabla o no. Vamos a cambiar este modo en la tabla y a probar de nuevo:

ALTER TABLE big_table NOLOGGING;


INSERT INTO big_table SELECT * FROM all_objects;
29368 rows created.

Statistics
----------------------------------------------------------
...
3239724 redo size

INSERT /*+ APPEND */ INTO big_table SELECT * FROM all_objects;


29368 rows created.

Statistics
----------------------------------------------------------
...
7536 redo size

COMMIT;

Podemos ver como AUTOTRACE es muy útil mostrando como operaciones similares pueden
ejecutarse de modo muy diferente. Usando un INSERT normal hemos generado 3,2Mb de redo log . El
segundo INSERT por direct-path virtualmente no ha generado redo .

De hecho el redo generado por el INSERT normal puede provocar un poco de confusión.
Declarando una tabla como NOLOGGING no significa que todo el redo de esta tabla sea evitado. Sólo
ciertas operaciones, como las bulk direct-path de este caso, no generarán redo normalmente. El resto
de operaciones (INSERT, UPDATE, DELETE y MERGE) lo seguirán generando.

Así pues, ¿significa esto que debemos declarar todas nuestras tablas como NOLOGGING y usar /*+
APPEND */ en nuestras inserciones? No. Primero hay que observar que esta opción sólo funciona con
operaciones INSERT AS SELECT (= INSERT bulk ), no funciona con operaciones INSERT ... VALUES.
Muchas veces he visto trozos de código con:

INSERT /*+ APPEND */ INTO t VALUES (...)

cuando le pregunté al programador acerca de ello me dijo Bueno, no necesito que genere redo así
que he desactivado su generación . Eso simplemente significa que el programador no entiende del todo
que hace esta opción. Así que usándola en un INSERT con VALUES sólo revela que es un mal
programador (pero no tiene otro efecto).

Una importante razón para evitar el modo NOLOGGING de una tabla en obvio: las operaciones sobre
esa tabla no quedarán reflejadas en los archivelog . Mientras que eso puede sonar excelente para
muchos programadores eso es un problema para los DBA. Hemos cargado datos en esta tabla y la gente
empieza ha hacer modificaciones en ella. Al día siguiente el disco revienta. No hay problema dice el
DBA, acabo de restaurar el backup de hace dos días . Se revisa la tabla y se ve que no contiene los
datos que fueron cargados masivamente el día anterior. ¡Todo el trabajo de un día se ha perdido! ¿Por
qué? Pues porque se evitó que se generase información de redo . Al no estar los datos en los
archivelog no se pueden recuperar.

Aquí hay una pocas razones para no declarar las tablas como NOLOGGING y usar /*+ APPEND */
en todas las inserciones:

- el INSERT direct-path escribe los datos sobre la HWM (= High Water Mark ) de la tabla, ignorando
cualquier espacio libre en las free lists , así como lo hacen las cargas direct-path de SQLLoader. Si
se borran muchas de las filas de la tabla y después se hacen INSERT /*+ APPEND */ en ella no
será posible reusar el espacio, con lo que tabla estará defragmentada;

- se debe realizar un COMMIT después de una carga direct-path exitosa antes de poder leer desde la
tabla en esa transacción;

- sólo una sesión en un mismo momento puede realizar un INSERT direct-path en una misma tabla.
El resto de modificaciones serán bloqueadas. Esta operación es de forma serializada (aunque se
pueden hacer operaciones INSERT direct-path desde una misma sesión);

Nuevamente esta operación debe ser usada con cautela y sincronizarla con los backups por si
hubieran problemas.

- redo y las operaciones con índices, ¿qué sucede si se hace un INSERT / * + APPEND * / con
SELECT con la tabla en NOLOGGING pero se quiere mantener todo el redo log y archivelog
generado? ¿Qué hay de malo en ello? La respuesta es que si hay índices en la tabla y esos índices no
pueden ser añadidos deberán ser unidos. Debido a que necesitamos unir datos en ellos necesitamos
redo para poder recuperar la instancia en caso de fallo (si el sistema revienta en medio de
operaciones con índices su estructura quedará corrupta). De nuevo el redo es generado para las
operaciones con índices. Podemos verlo con este ejemplo:

CREATE INDEX big_table_idx ON big_table(owner,object_type,object_name)


TABLESPACE users;

INSERT /*+ APPEND */ INTO big_table SELECT * FROM all_objects;


29369 rows created.

Statistics
----------------------------------------------------------
...
18020324 redo size

COMMIT;
¡Vaya, que diferencia puede hacer un índice! Los índices son estructuras complejas de datos y su
mantenimiento puede ser muy caro. Aquí sabemos que los datos de la tabla deben generar alrededor
de 3,2Mb de "redo" ¡pero un solo índice como éste generó 18Mb de "redo"!

Si esto hubiera sido una cada de datos "bulk" debería haber hecho algo así:

1. Declarar los índices en estado UNUSABLE (sin borrarlos)


2. Modificar la sesión para que obvie los índices en estado UNUSABLE y realizar la carga "bulk".
3. Reactivar los índices.

Aquí muestro los pasos que se harían en SQL* Plus para ello (SQLLoader sería similar pero se
debe usar el parámetro skip_index_maintenance en vez de los dos primeros comandos):

ALTER INDEX big_table_idx UNUSABLE;


ALTER SESSION SET skip_unusable_indexes=TRUE;

INSERT /*+ APPEND */ INTO big_table SELECT * FROM all_objects;


29369 rows created.

Statistics
----------------------------------------------------------
...
7588 redo size

ALTER INDEX big_table_idx REBUILD NOLOGGING;

Ahora todo lo que necesitamos es programar un backup de los archivos de datos afectados y
habremos acabado con nuestra carga. ¿Por qué simplemente no borramos los índices y los recreamos
después? Pues porque he visto muchos sistemas donde el comando DROP INDEX funcionó pero el
posterior CREATE INDEX falló por alguna razón.

Incluso si no borramos el índice y lo dejamos en estado UNUSABLE nunca lo perderemos. Si el


comando para reactivarlo falla, cuando los usuarios ejecuten consultas que precisen del índice se
encontrarán con un mensaje de error, el cual reportarán inmediatamente. Así que en vez de tener el
sistema sufriendo pérdidas de rendimiento misteriosas durante horas o día podremos detectar
rápidamente la pérdida del índice.

SQL*Net Statistics. Hay tres partes en las estadísticas SQL*Net:

- bytes recibidos vía SQL*Net desde el cliente (cuanta información enviaste al servidor)
- bytes enviados vía SQL*Net al cliente (cuanta información el servidor te envió)
- trayectos SQL*Net completos a o al cliente (cuantos viajes de ida y vuelta fueron hechos)

Sobre los que se tiene más control es sobre los dos últimos. Podemos minimizar los datos que
recibimos. El modo de controlar esto es seleccionando sólo las columnas que son relevante para nosotros.
Frecuentemente veo gente que programa consultas como SELECT * FROM table y sólo usa una o dos
columnas. Esto no sólo llena la red con toneladas de información innecesaria y consume RAM adicional,
sino que puede afectar radicalmente la eficacia de los planes de ejecución. Para evitar este problema hay
que seleccionar sólo aquellas columnas que realmente son necesitadas para solventar nuestro problema,
ni más ni menos.

El número de trayectos completos (ida y vuelta) también es ajustable cuando se está procesando una
sentencia SELECT. Para verlo vamos a reusar el anterior ejemplo que mostró los efectos del tamaño del
array en las estadísticas de lecturas consistentes. Simplemente proporcionando el tamaño del array
podemos medir el impacto en las estadísticas de los trayectos SQL*Net hacia o desde el cliente:

SET AUTOTRACE TRACEONLY STATISTICS


set arraysize 2
SELECT * FROM t;
29369 rows selected.
...
14686 SQL*Net roundtrips to/from client

set arraysize 5
/
...
5875 SQL*Net roundtrips to/from client

set arraysize 10
/
...
2938 SQL*Net roundtrips to/from client

set arraysize 15
/
...
1959 SQL*Net roundtrips to/from client

set arraysize 100


/
...
295 SQL*Net roundtrips to/from client

set arraysize 5000


/
...
7 SQL*Net roundtrips to/from client

Por lo general cuantos menos trayectos completos hayan será mejor. Un array de recuperación de
entre 100 y 500 resultados proporcionará el mejor rendimiento y uso de memoria.

Sorts and Row s Processed. Las últimas tres estadísticas están relacionados con las ordenaciones y el
procesamiento de filas:

- sorts (memory), muestra cuantas ordenaciones se hicieron enteramente en memoria (sin swap)
- sorts (disk), muestra cuantas de las ordenaciones hechas necesitaron espacio temporal en disco
- rows processed, muestra cuantas filas fueron afectadas (incluye las retornadas por un SELECT o las
modificadas por un INSERT, UPDATE, DELETE o MERGE).

Nuestro objetivo es reducir el número de ordenaciones en disco. Como se demostró antes con las
estadísticas de lecturas físicas, un método para conseguir esto es establecer un área de ordenación que
sea apropiada para lo que estamos haciendo; esto significa que muchos parámetros "sort area size" son
demasiado pequeños. Otro método es "liso", del viejo estilo del ajuste de sentencias. Determinar si hay
una alternativa equivalente al método que queremos usar para reducir el tamaño de la ordenación o para
eliminarla por completo incluso.

Notad sin embargo que una estadística "sorts to disk" a cero no significa que no estemos haciendo
uso del espacio temporal TEMP. Es completamente posible que la ordenación sea hecha en memoria pero
que sus resultados sean escritos en disco. Esto sucederá cuando el parámetro
sort_area_retained_size (= la cantidad de datos que podemos almacenar en memoria después de
una ordenación) sea más pequeño que el parámetro sort_area_size.

Bueno, esto completa la discusión sobre las estadísticas de AUTOTRACE. Como habéis visto
AUTOTRACE es una herramienta muy potente. Es un gran avance sobre EXPLAIN PLAN, el cual sólo
puede mostrar que plan será usado pero no que plan se está usando actualmente. Sin embargo todavía
está un nivel por debajo de TKPROF, la siguiente herramienta que veremos.
TKPROF.-
Oracle tiene la capacidad de activar (generalmente por medio del comando ALTER SESSION) una capacidad
para tracear a bajo nivel. Una vez que el traceado está activo Oracle grabará todas las sentencias SQL y llamadas
PL/ SQL que nuestra aplicación haga en un archivo de traza del servidor (en la máquina del servidor de base de
datos, nunca en la máquina del cliente). Este archivo de traza no sólo contendrá nuestras llamadas SQL y PL/SQL,
sino que además contendrá información sobre tiempos, posiblemente información sobre eventos de espera (lo
que está rezagando el sistema), cuantas operaciones I/ O lógicas y físicas hemos realizado, los tiempos de uso de
CPU y de tiempo transcurrido, el número de filas procesadas, los planes de ejecución y mucho más. Este archivo
de traza en difícil de leer en si mismo. Lo que queremos es generar un informe a partir de este archivo de traza
en un formato fácilmente entendible por nosotros. Ese es el único objetivo de TKPROF: convertir un archivo de
traza en algo que podemos usar fácilmente.

Como he mencionado muchas veces TKPROF es mi herramienta de ajuste favorita. Además su uso parece
estar muy extendido. No obstante algunas veces la gente no la usa por ignorancia: no sabe que existe. Otras no
la usa por prevención: el DBA impide el acceso de los programadores a los necesarios archivos de traza. Aquí
eliminaremos la ignorancia sobre esta herramienta y proporcionaremos un método para acceder a los archivos de
traza en un modo que echará por tierra los principios del DBA. Primero vmos a ver como se activa y ejecuta
TKPROF.

Activar TKPROF
Antes de poder ejecutar TKPROF necesitamos entender como activarlo (= métodos para activar SQL_TRACE).
Para ello tengo un pequeño script llamado trace.sql . Simplements contiene dos comandos:

ALTER SESSION SET timed_statistics = TRUE;


ALTER SESSION SET events '10046 trace name context forever, level 12';

En el caso de un sistema en el que las estadísticas de tiempo no estén activadas las activaremos a nivel de
sesión con ALTER SESSION SET timed_statistics=TRUE. Sin estas estadísticas de tiempo la utilidad de un
informe TKPROF se reduce. No veremos tiempos de CPU ni tiempos transcurridos. En una palabra: ¡no seremos
capaces de ver donde están los cuellos de botella!

Después de haber activado las estadísticas podemos iniciar la generación de trazas usando ALTER SESSION
SET SQL_TRACE=TRUE o bien ALTER SESSION SET EVENTS 10046 TRACE NAME CONTEXT FOREVER,
LEVEL <n> , donde n puede ser:

- 1, para activar SQL_TRACE (= a SQL_TRACE=TRUE);


- 4, para activar SQL_TRACE y capturar los valores de las variables bind ;
- 8, para activar SQL_TRACE y capturar eventos de espera;
- 12, para activar SQL_TRACE, variables bind y eventos de espera.

Como podéis ver en el script, uso ALTER SESSION SET EVENTS 10046 TRACE NAME CONTEXT
FOREVER, LEVEL 12 . Esto es especialmente preciado en Oracle9i donde la utilidad TKPROF incluye eventos
de espera en el informe (las versiones anteriores no lo hacían). TKPROF no mostrará los valores de las variables
bind : para eso necesitaremos el archivo de traza en si. Notad que la inclusión de eventos de espera en el
archivo de traza incrementará significativamente su tamaño. Por eso debemos comprobar el parámetro
max_dump_file_size para asegurarnos de tiene un valor suficiente para acomodar la información adicional.

Ejecutando TKPROF
Para empezar usaremos un rápido test para demostrar lo que TKPROF proporciona y como acceder a los
archivos de traza en el servidor. Ejecutaremos el script para activar el traceado y después ejecutar algunas
consultas.

Generando el archivo de traza


En este ejemplo usaré BIG_TABLE, una tabla creada a partir de ALL_OBJECTS, duplicada una vez y otra para
tener bastantes filas:
ALTER SESSION SET timed_statistics = TRUE;
ALTER SESSION SET events '10046 trace name context forever, level 12';

SELECT COUNT(*) FROM big_table;

COUNT(*)
----------
176210

SELECT * FROM big_table


WHERE owner = 'SYS'
AND object_type = 'PACKAGE'
AND object_name like 'F%';

...
6 rows selected.

Ahora que hemos generado un poco de actividad y se ha creado un archivo de traza estamos listos para
ejecutar algunas consultas que nos ayudarán a ejecutar TKPROF.

Obtener el nombre del archivo de traza


Para ejecutar TKPROF necesitamos conocer el nombre del archivo de traza. La siguiente consulta es útil para
Oracle9i (y versiones superiores) en Linux y Windows:

SELECT RTRIM(c.value,'/')||'/'||d.instance_name||
'_ora_'||LTRIM(TO_CHAR(a.spid))||'.trc'
FROM v$process a, v$session b, v$parameter c, v$instance d
WHERE a.addr = b.paddr
AND b.audsid = sys_context( 'userenv', 'sessionid')
AND c.name = 'user_dump_dest';

En un servidor Linux por ejemplo SQL*Plus retorna este resultado:

RTRIM(C.VALUE,'/')||'/'||D.INSTANCE_NAME||'_ORA_'||LTRIM(TO_CHAR
---------------------------------------------------------
/usr/oracle/ora920/admin/ora920/udump/ora920_ora_14246.trc

Después de ejecutar estas consultas debemos salir de SQL* Plus (o cerrar de la sesión de la herramienta que
usemos). Esto es debido a que el archivo de traza se debe cerrar completamente para que podamos tener toda la
información disponible en el archivo de traza.

Creando el informe TKPROF


Ahora que sabemos como se llama nuestro archivo de traza estamos listos para ejecutar TKPROF. Lanzamos
el siguiente comando:

$ tkprof /usr/oracle/.../ora920/udump/ora920_ora_14246.trc tk.prf SYS=no

Este comando crea el archivo de texto tk.prf en nuestro directorio actual de trabajo.

Leyendo un informe TKPROF


Al abrir el archivo de texto tk.prf encontramos unos resultados como estos:

select count(*) from big_table

call count cpu elapsed disk query current rows


------- ------ ---- -------- ------- ------- --------- ----------
Parse 1 0.00 0.00 0 0 0 0
Execute 1 0.00 0.00 0 0 0 0
Fetch 2 0.22 0.52 2433 2442 0 1
------- ------ ----- -------- ------- ------- -------- ----------
total 4 0.22 0.52 2433 2442 0 1

Misses in library cache during parse: 0


Optimizer goal: CHOOSE
Parsing user id: 147

Rows Row Source Operation


------- ---------------------------------------------------
1 SORT AGGREGATE
176210 TABLE ACCESS FULL BIG_TABLE

Elapsed times include waiting on following events:


Event waited on Times Max. Wait Total Waited
---------------------------- Waited ---------- ------------
SQL*Net message to client 2 0.00 0.00
db file sequential read 1 0.00 0.00
db file scattered read 163 0.07 0.40
SQL*Net message from client 2 0.00 0.00
*******************************************************************

Revisemos este informe pieza por pieza, empezando desde arriba.

estadísticas de consulta y ejecución, el informe empieza con el texto original de la consulta que fue
procesada por la base de datos en nuestra sesión desde que activamos el traceado:

select count(*) from big_table

Después hay un informe tabulado que muestra estadísticas vitales sobre cada fase de la consulta. Podemos
ver tres fases de la consulta:

- parse ( = parseado). Esta fase es donde Oracle halla la consulta en la Shared Pool (= soft parse ) o
crea un nuevo plan para la consulta (= hard parse );

- execute (= ejecución). Esta fase es el trabajo hecho por Oracle en la sentencia OPEN o EXECUTE de la
consulta. Para una sentencia SELECT estará vacío en la mayoría de los casos. Para una sentencia UPDATE
aquí aparecerá todo el trabajo realizado por Oracle.

- fetch ( = recuperación de datos) . Para una sentencia SELECT esta fase será en donde la mayor parte
del trabajo se haga visible. Para una sentencia UPDATE no mostrará trabajo alguno, ya que
evidentemente no se recogen los datos de filas para una operación de actualización.

Cada sentencia procesada tendrá estas tres fases. También tenemos las siguientes columnas:

- count. Es la cantidad de veces que la fase actual de la consulta ha sido ejecutada. En una aplicación
programada debidamente todas las sentencias SQL tendrán un parse de 1 y un execute de 1 ó más.
Si es posible no queremos parsear una misma sentencia más de una vez;

- CPU. Tiempo que se ha empleado en esta fase de la ejecución en milésimas de segundo;

- elapsed. Cantidad de tiempo real ( wall clock time ) tardado en esta fase. Cuando el tiempo transcurrido
es mucho mayor que el tiempo de CPU significa que se ha esperado un tiempo por algo. En Oracle9i y
con TKPROF es fácil ver de que espera se trataba. Al final del informe podemos ver que la espera fue por
db file scattered read , lo que significa que sufrimos retrasos por operaciones I/ O físicas;

- disk. Cantidad de operaciones I/ O físicas realizadas durante esta fase de la consulta. En este ejemplo
cuando recuperamos las filas de la tabla realizamos 2.433 lecturas físicas de disco;
- query. Cantidad de operaciones I/ O lógicas realizadas para recuperar los bloques en modo consistente.
Esos bloques pueden haber sido reconstruidos desde los segmentos de rollback por si queríamos ver
como estaban antes de que nuestra consulta se ejecutara. Generalmente todas las operaciones I/O físicas
resultan en operaciones I/ O lógicas. En muchos casos encontraremos que nuestras I/ O lógicas
sobrepasan las I/O físicas. Sin embargo como se dijo antes en la seción AUTOTRACE, las lecturas y
escrituras directas en el espacio temporal violan esta regla, y podemos tener I/ O físicas que no se
traducen en I/O lógicas;

- current. Cantidad de operaciones I/ O lógicas que fueron realizadas para recuperar los bloques así como
están ahora. Se produciran frecuentemente durante las operaciones DML, como UPDATE y DELETE. Aquí
un bloque debe ser recuperado en su estado normal para procesar la modificación, en contraposición a
cuando se consulta una tabla y Oracle recupera el bloque en el momento en que la consulta empieza;

- rows. El número de filas procesadas o afectadas por esta fase. Durante una modificación veremos
valores en la fase de execute (= ejecución). Durante una consulta SELECT este valor aparece en la fase
fetch (= recuperación);

Una pregunta que frecuentemente se produce en relación a TKPROF y su informe es como sería posible
obtener una salida como esta:

call count cpu elapsed disk query current rows


------- ------ -------- ---------- ----- -------- ---------- ----------
Parse 1 0.00 0.00 0 0 0 0
Execute 14755 12.77 12.60 4 29511 856828 14755
Fetch 0 0.00 0.00 0 0 0 0
------- ------ -------- ---------- ----- -------- ---------- ----------
total 14756 12.77 12.60 4 29511 856828 14755

¿Cómo puede ser que el tiempo de CPU sea más largo que tiempo transcurrido? Esta discrepancia es
debido al modo en que los tiempos son recogidos y a como se miden operaciones rapidísimas o muy
numerosas. Supongamos por ejemplo que estamos usando un cronómetro que sólo puede medir con una
precisión de un segundo. Medimos 50 eventos. Cada evento parece haber tardado dos segundos de acuerdo
al cronómetro. ¿Significa eso que si tenemos 100 segundos transcurridos será una medida correcta?
Probablemente no. Supongamos que realmente cada evento tardó 2,99 segundos, entonces realmente
deberíamos tener 150 segundos transcurridos.

Yendo un paso más lejos supongamos que el cronómetro estaba en marcha contínuamente. Cuando el
evento empezó deberíamos haberlo mirado al comienzo y al fin del evento, restando los dos números. Esto es
muy parecido a lo que sucede con las medidas de tiempo en el servidor: mira el reloj del sistema, hace algo y
vuelve a mirarlo. Delta representa el tiempo del evento. Ahora realicemos el mismo test de tiempo que antes.
Ahora cada evento parece tardar 2 segundos cuando en realidad pueden estar tardando sólo 1,01 segundos.
¿Cómo es posible? Cuando el evento comenzó el reloj estaba realmente en 2,00 segundos pero solo dijimos 2
(la precisión del cronómetro). Cuando el evento fue completado el cronómetro marcaba 4 (y el tiempo real
fue 4,00). El delta que vemos es de 2 pero el delta real es actualmente de 1,01.

Con el tiempo estas discrepancias se acumulan en el resultado total. Esa es la causa de la discrepancia: el
tiempo transcurrido es menor que el tiempo de CPU. En el nivel más bajo Oracle está recogiendo estadísticas
de tiempo con una precisión que va desde el milisegundo al microsegundo. Además puede suceder que
algunos eventos se midan usando un reloj y otros eventos con otro reloj (esto es inevitable ya que las
informaciones de tiempo provienen del sistema operativo a través de las APIs). En este ejemplo ejecutamos
una sentencia 14.755 veces, dando como resultado que cada sentencia tardó 0.00865469 segundos. Si
ejecutáramos este test una vez y otra podríamos encontrarnos con tiempos de CPU y transcurrido más
cercanos.

entorno de la consulta, la siguiente sección del informe TKPROF muestra datos sobre el entorno en el que
la sentencia se ejecutó. En este caso podemos ver:

Misses in library cache during parse: 0


Optimizer goal: CHOOSE
Parsing user id: 147

Los fallos en la library cache que están a cero nos están diciendo que la consulta fue soft parsed (=
Oracle encontró que la consulta ya estaba en la Shared Pool) debido a que yo ya la había ejecutado al menos
una vez antes. El optimizer goal (= objetivo del optimizador) simplemente muestra que objetivo tenía el
optimizador cuando esta consulta fue parseada. En este caso es CHOOSE, lo que significa que podía haberse
utilizado el optimizador RBO (= Rule Based Optimizer ) o el CBO (= Cost Based Optimizer ). El parsing
user ID muestra el identificador del esquema que ejecutó la consulta. Si hago esta consulta:

USERNAME USER_ID CREATED


------------------------------ ---------- ---------
OPS$TKYTE 147 24-DEC-02

Puedo ver que usé el usuario de base de datos OPS$TKYTE .

el plan de ejecución, la siguiente sección del informe TKPROF es el plan de ejecución que la consulta ha
usado cuando esta consulta fue ejecutada. Este plan no es generado por EXPLAIN PLAN o por AUTOTRACE,
sino que es escrito en el archivo de traza en tiempo de ejecución. Es el plan de ejecución verdadero.

Para mostrar porque esta información es relevante ejecutaré estos comandos SQL antes:

TRUNCATE TABLE big_table;


ALTER TABLE big_table ADD CONSTRAINT big_table_pk PRIMARY KEY(object_id);
exec dbms_stats.gather_table_stats (user,'BIG_TABLE')

Después ejecutaré TKPROF con otro argumento: EXPLAIN PLAN:

$ tkprof ora920_ora_14246.trc /tmp/tk.prf explain=/

...
Rows Row Source Operation
------- ---------------------------------------------------
1 SORT AGGREGATE
176210 TABLE ACCESS FULL BIG_TABLE

Rows Execution Plan


------- ---------------------------------------------------
0 SELECT STATEMENT GOAL: CHOOSE
1 SORT (AGGREGATE)
176210 INDEX GOAL: ANALYZED (FULL SCAN) OF 'BIG_TABLE_IDX' (NON-UNIQUE)

¡Qué extraño, ahora hay dos planes de ejecución! El primero es el verdadero, el que es usado
actualmente. El segundo es el plan que sería empleado si ejecutáramos la consulta ahora mismo. Es diferente
porque añadí la clave primaria y recogí las estadísticas de la tabla. Esta es una razón por la que es posible
que no queráis usar la opción EXPLAIN= de TKPROF: puede no mostrar el plan de ejecución actual que fue
usado en tiempo de ejecución. Otros parámetros que afectan al optimizador, como sort_area_size y
db_file_multiblock_read_count pueden tener el mismo efecto. Si la sesión que ejecutó la consulta
tiene valores diferentes de los que están por defecto EXPLAIN puede mostrar información imprecisa, como
sucedió en este ejemplo.

TKPROF muestra, además del plan de ejecución actual, el número de filas tocadas en cada paso. Aquí
podemos decir que 176.210 filas fluyeron desde el paso TABLE ACCESS FULL BIG_TABLE hacia el SORT
AGGREGATE, y que sólo una fila resultó de ese paso. Cuando se ajusta una sentencia esta información puede
ser muy útil. Ayuda a identificar las partes problemáticas de una sentencia SQL que podemos optimizar.

Como un extra en esta sección si estamos usando Oracle9i ver.2 podemos tener una salida como esta:
Rows Row Source Operation
------- ---------------------------------------------------
1 SORT AGGREGATE (cr=2300 r=2209 w=0 time=1062862 us)
176210 TABLE ACCESS FULL OBJ#(30635) (cr=2300 r=2209 w=0 time=361127 us)

Ahora vemos que contiene incluso más detalles. No sólo tenemos el plan y el número de filas "tocadas"
en cada paso, sino que además vemos I/ O lógico, físico e información de tiempos (estos datos no se
corresponden al ejemplo anterior). Estos datos están representados como:

- cr, son las lecturas en modo consistente, mostrando los "consistent gets" (= I/O lógico);
- r, son las lecturas físicas;
- w , son las escrituras físicas;
- time, es el tiempo pasado en millónesimas de segundo (us se refiere a microsegundos)

eventos de espera, la última parte de informe TKPROF muestra los eventos de espera. Esta información
está disponible desde Oracle9i ver.1 y se muestra cuando se usa SET EVENT para activar el traceado. En este
caso el informe de nuestra consulta fue:

Event waited on Times Max. Wait Total Waited


---------------------------- Waited ---------- ------------
SQL*Net message to client 2 0.00 0.00
db file sequential read 1 0.00 0.00
db file scattered read 163 0.07 0.40
SQL*Net message from client 2 0.00 0.00

El informe identificó claramente el gran evento de espera nuestro: 4/ 10 de segundo se emplearon


esperando por operaciones I/O (hemos esperado 163 veces por eso).

Podemos usar esa información para ayudar en las optimizaciones de nuestras consultas. Por ejemplo si
esta consulta tuviera un rendimiento pobre podríamos concentrarno en reducir o acelerar el I/ O aquí. Una
aproximación en este caso sería el añadir una clave primaria (como hice antes) y recoger las estadísticas de
la tabla. El plan de ejecución (que fue mostrado antes) se convertirá en un rápido "index full scan" (será más
rápido porque el índice es mucho más pequeño que la tabla, así que hay menos I/O).

TKPROF para las masas


Ahora que hemos visto que puede hcaer TKPROF, ¿cómo podemos hacer que esté disponible para todo el
mundo? Sabéis que para que funcione TKPROF necesitáis acceso a los archivos de traza del servidor. Este es el
punto delicado en muchos casos. Los archivos de traza pueden contener información sensible, por lo que es
posible que el programador 1 no deba ver el archivo de traza del programador 2. En un sistema de producción no
es deseable que todo el mundo vea los archivos de traza, a excepción del staf del DBA.

Quiero defender que en un sistema de producción la generación de archivos de traza sólo se producirá para
diagnosticar un problema y será hecho con ayuda del DBA. Sin embargo en un entorno de desarrollo y pruebas es
necesaria una solución más escalable. En otras palabras, los programadores deben poder acceder a esos archivos
de traza sin necesidad de pedir permiso al DBA para conseguirlos. Ellos necesitan las trazas ahora, las necesitan
rápido y las necesitan con frecuencia. Para acceder al archivo de traza también necesitan acceder al servidor, en
concreto al directorio definido por el parámetro user_dump_dest_directory.

Métodos de acceso típicos


Hay unas pocas vías tradicionales para permitir acceder a los archivos de traza:

Una de ellas es usar el parámetro indocumentado _TRACE_FILES_PUBLIC=TRUE.

Sin embargo no se debe usar este método en un sistema en producción porque hará accesible públicamente
los archivos de traza, lo cual puede ser un agujero de seguridad. En cambio en un servidor de desarrollo esto es
generalmente aceptable. Tenemos un universo limitado de desarrolladores que acceden a este servidor y por lo
general es normal que vean sus trazas. Esto hace los archivos de traza leíbles por todo el mundo (normalmente
sólo la cuenta Oracle y el grupo DBA podrían ver esas trazas). Sin embargo esto no resulve el problema del
acceso físico a los archivos. Los programadores todavía necesitan acceder al sistema de archivos, lo que será un
éxito clamoroso. Las soluciones incluyen exportar el directorio user_dump_dest como un sistema de archivos de
sólo lectura y permitir a los programadores montarlo, o permitir el acceso vía telnet o ssh al servidor en si.

Programar una tarea del sistema ("cron job") que se ejecute cada N minutos para mover los archivos de
traza a un directorio público.

He visto esta solución frecuentemente pero nunca he sabido porque es popular. Comparada con el primer
método es más difícil ya que es necesario programar el script y permitir el acceso al sistema de archivos en donde
está el directorio público, pero no añade ninguna seguridad o algo útil. Perjudica el acceso a los archivos de traza
ya que los programadores deben esperar N minutos a que estén disponibles. Aunque funcione es un método que
no recomiendo.

Dejar que los programadores usen un programa setuid (Unix) para copiar sus trazas.

Una vez más y por las mismas razones que con el método anterior es una solución demasiado complicada.
Tampoco la recomiendo.

Una solución alternativa para proporcionar acceso sin problemas


Voy a ofrecer otra alternativa para permitir el acceso a los archivos de traza que funcionará bastante bien y
que he usado con mucho éxito. No es necesario establecer ningún parámetro indocumentado de Oracle. Permite
que cada programador acceda sólo a sus archivos de traza. Se elimina la necesidad de dar acceso físico al
sistema de archivos del servidor. Cumple el objetivo de proporcionar un acceso inmediato a las trazas. Todo esto
se puede hacer simplemente con un conjunto de rutinas PL/SQL.

Lo primero que haremos implica varios pasos, como es crear una "aplicación". El primer paso es crear un
esquema con los mínimos privilegios necesario. Este esquema será usado para permitir que los usuarios vean los
archivos de traza como si fueran tablas de la base de datos (de hecho podrán hacer SELECT * FROM "trace_file").
Para hacer esto escribiremos un pequeño PL/ SQL para leer un archivo de traza y haremos que ese PL/ SQL pueda
ser ejecutado desde SQL. También usaremos un disparador LOGOFF para capturar los archivos de traza que son
generados en una tabla de la base de datos, además de capturar el "propietario" de esa traza para que cada
programador pueda ver sólo sus trazas. Por último desarrollaremos un script SQL* Plus que llamará a TKPROF
contra esas "tablas de la base de datos que son archivos de traza" de la forma más sencilla.

- crear un esquema. Primero crearemos un esquema en la base de datos que será usado para
proporcionar acceso a los archivos de traza de ese usuario como "como si cada archivo de traza fuera
una tabla de la base de datos". El usuario final será capaz de ejecutar SELECT * FROM
their_trace_file. Usando SPOOL en SQL* Plus el usuario final grabará ese archivo de traza
localmente y así no necesitaremos tener acceso al sistema de archivos del servidor. El usuario que
necesitamos será creado con al menos estos privilegios:

conn sys as sysdba

CREATE USER trace_files IDENTIFIED BY trace_files


DEFAULT TABLESPACE users QUOTA UNLIMITED ON users;

GRANT create any directory, create session, create table, create view,
create procedure, create trigger, administer database trigger
TO trace_files;

GRANT SELECT ON v_$process TO trace_files;


GRANT SELECT ON v_$session TO trace_files;
GRANT SELECT ON v_$instance TO trace_files;

- crear una vista y una tabla. Ahora crearemos una vista que devuelve el nombre del archivo de traza
de la sesión actual. Como se discutió en la sección "Obtener el nombre del archivo de traza" durante este
capítulo, esta vista puede ser necesitada para ajustarla a nuestro sistema operativo:
conn trace_files/trace_files

CREATE VIEW session_trace_file_name AS


SELECT d.instance_name||'_ora_'||LTRIM(TO_CHAR(a.spid))||'.trc' filename
FROM v$process a, v$session b, v$instance d
WHERE a.addr = b.paddr
AND b.audsid = userenv('sessionid');

NOTA: Existe el parámetro TRACEFILE_IDENTIFIER que se puede cambiar a nivel de sesión y que es
muy útil para identificar el nombre de un archivo de traza. Cuando de establece con un valor de
cadena, ese valor será añadido al nombre del archivo de traza. Por eso también se podría considerar
unirlo a V$PARAMETER en la vista anterior para que formara parte del nombre.

Y luego creamos una tabla para mapear nombres de usuario con nombres de archivos. También
mantendremos un TIMESTAMP para ver cuando ha finalizado la sesión del archivo de traza. La vista es lo
que usarán los usuarios finales para ver sus trazas cuando estén disponibles:

CREATE TABLE avail_trace_files (


username VARCHAR2(30) DEFAULT USER,
filename VARCHAR2(512),
dt DATE DEFAULT SYSDATE,
CONSTRAINT avail_trace_files_pk PRIMARY KEY(username,filename))
ORGANIZATION INDEX;

CREATE VIEW user_avail_trace_files AS


SELECT * FROM avail_trace_files WHERE username = user;

GRANT SELECT ON user_avail_trace_files TO public;

CREATE GLOBAL TEMPORARY TABLE trace_file_text (


id NUMBER PRIMARY KEY,
text VARCHAR(4000));

GRANT SELECT ON trace_file_text TO public;

- crear un disparador. Ahora usaremos un disparador LOGOFF para capturar el nombre del archivo de
traza y el nombre de usuario actual si existe una traza actualmente. Usaremos BFILE para conseguir esto.
(Debemos establecer el nombre correcto para directorio udump_dir, este es sólo un ejemplo):

CREATE OR REPLACE DIRECTORY udump_dir


AS 'C:\Oracle\admin\hotel\udump';

CREATE OR REPLACE TRIGGER capture_trace_files


BEFORE LOGOFF ON DATABASE
BEGIN
FOR x IN (SELECT * FROM session_trace_file_name ) LOOP
IF (dbms_lob.fileexists(bfilename('UDUMP_DIR',x.filename))=1) THEN
INSERT INTO avail_trace_files (filename)
VALUES (x.filename);
END IF;
END LOOP;
END;
/

- añadir la función. Ahora necesitamos la función que leerá los datos de traza de vuelta al usuario.
Empieza lanzando un SELECT contra USER_AVAIL_TRACE_FILES para asegurarse de que el archivo
solicitado está disponible para el usuario conectado actualmente. Ese SELECT INTO alcanzará un
NO_DATA_FOUND, lo cual cuando sea devuelto al SELECT llamante simplemente aparecerá como "No
data found". Si una consulta encuentra datos se leerá el archivo de traza línea a línea, volviendo al
finalizar:

CREATE OR REPLACE PROCEDURE trace_file_contents( p_filename IN VARCHAR2 )


AS
l_bfile BFILE := bfilename('UDUMP_DIR',p_filename);
l_last NUMBER := 1;
l_current NUMBER;
BEGIN
SELECT ROWNUM INTO l_current FROM user_avail_trace_files
WHERE filename = p_filename;
DELETE FROM trace_file_text;
dbms_lob.fileopen( l_bfile );

LOOP
l_current := dbms_lob.instr( l_bfile, '0A', l_last, 1 );
EXIT WHEN (NVL(l_current,0) = 0);
INSERT INTO trace_file_text (id,text)
VALUES (l_last,utl_raw.cast_to_varchar2(dbms_lob.substr(
l_bfile,l_current-l_last+1,l_last)));
l_last := l_current+1;
END LOOP;
dbms_lob.fileclose(l_bfile);
END;
/

GRANT EXECUTE ON trace_file_contents TO public;

- probar el acceso. En este punto usaremos cualquier cuenta de usuario mínimamente privilegiada de un
programador y ver si funciona:

ALTER SESSION SET sql_trace = TRUE;


connect scott/tiger

Probemos esto:

SELECT * FROM trace_files.user_avail_trace_files;

USERNAME FILENAME DT
-------- --------------------- ----------------------------
DEVELOPER ora920_ora_14973.trc 29-DEC-02 04.31.05.241607 PM

Simplemente generando un archivo de traza ya tenemos una fila aquí. Vamos a continuar:

SELECT * FROM table( trace_files.trace_file_contents( '&f' ) );

COLUMN_VALUE
------------------------------------------------------------
/usr/oracle/OraHome1/admin/ora920/udump/ora920_ora_14973.trc

Oracle9i Enterprise Edition Release 9.2.0.1.0 - Production


With the Partitioning, OLAP and Oracle Data Mining options

JServer Release 9.2.0.1.0 - Production


ORACLE_HOME = /usr/oracle/ora920/OraHome1
System name: Linux
Node name: tkyte-pc-isdn.us.oracle.com
Release: 2.4.18-14
Version: #1 Wed Sep 4 13:35:50 EDT 2002
Machine: i686
Instance name: ora920
Redo thread mounted by this instance: 1
Oracle process number: 14
Unix process pid: 14973, image: oracle@tkyte-pc-isdn.us.oracle.com

*** SESSION ID:(9.389) 2002-12-29 16:31:05.154


APPNAME mod='SQL*Plus' mh=3669949024 act='' ah=4029777240
=====================
PARSING IN CURSOR #1 len=32 dep=0 uid=237 oct=42 lid=237 tim=1016794399565448
hv=1197935484 ad='5399abdc'
alter session set sql_trace=true
END OF STMT
EXEC #1:c=0,e=127,p=0,cr=0,cu=0,mis=0,r=0,dep=0,og=4,tim=1016794399564994
=====================
... <acortado por brevedad> ...
XCTEND rlbk=0, rd_only=1
STAT #12 id=1 cnt=0 pid=0 pos=1 obj=0 op='UPDATE '
STAT #12 id=2 cnt=0 pid=1 pos=1 obj=5992 op='TABLE ACCESS FULL
WM$WORKSPACES_TABLE '
STAT #11 id=1 cnt=4 pid=0 pos=1 obj=222 op='TABLE ACCESS FULL DUAL '

128 rows selected.

Ahora, sólo para demostrar que la seguridad funciona, vamos a conectarnos como otro usuario y a probar
de consultar el mismo archivo de traza:

@connect /
SELECT * FROM table(trace_files.trace_file_contents(' ora920_ora_14973.trc '));
no rows selected

- automatizando el acceso. Ahora que tenemos esta capacidad para acceder a los archivos de traza,
¿cómo podemos usarla? Usaremos un script simple como este, llamado 'tklast.sql':

REM tklast.sql
COLUMN filename new_val f
SELECT filename FROM trace_files.user_avail_trace_files
WHERE dt = ( SELECT MAX(dt) FROM trace_files.user_avail_trace_files);

exec trace_files.trace_file_contents('&f')
set termout off heading off feedback off embedded on linesize 4000 trimspool
on verify off
spool &f
select text from trace_files.trace_file_text order by id;
spool off
set verify on feedback on heading on termout on
host tkprof &f tk.prf SYS=no
edit tk.prf

Y esto es todo lo que necesitamos para automatizar este proceso. Yo hallé mi último archivo de traza
para mi usuario, lo recuperé y ejecuté TKPROF contra él para formaterlo y usarlo.
DBMS_PROFILER.-
Un perfilador de código fuente como DBMS_PROFILER puede señalar en cuestión de minutos que sección del
código ha estado trabajando de forma incorrecta durante una tarde de ajustes. Sin él nos podríamos pasar una
semana intentando imaginar donde empezar a buscar el fallo. Lo que hace es mostrar cuanto tarda en ejecutarse
cada línea de un proceso PL/SQL.

¿Cómo funciona?
La sesión de perfilado se inicia mediante DBMS_PROFILER.START_PROFILER. Se ejecuta el objeto PL/ SQL
que sea y al finalizar se ejecuta DBMS_PROFILER.STOP_PROFILING. Durante la ejecución se introducen datos
sólo en las tablas PLSQL_PROFILER_RUNS, PLSQL_PROFILER_UNITS y PLSQL_PROFILER_DATA. La tabla
PLSQL_PROFILER_RUNS tiene información sobre cada vez que DBMS_PROFILER es iniciado, incluyendo el
comentario que se asignó cuando la sesión se inició. La tabla PLSQL_PROFILE_UNITS contiene información sobre
el código PL/SQL ejecutado durante la ejecución. Cada procedimiento, función y paquete tendrá su propia línea
en esta tabla. La tabla PLSQL_PROFILE_DATA contiene las líneas ejecutadas de código, el tiempo de ejecución y
más. Uniendo esta tabla a USER_OBJECT obtendremos el texto del código actual asociado con la información del
perfilador. También existen los procedimientos PAUSE_PROFILER, RESUME_PROFILER (para pausar
momentáneamente la recogida de tiempos) y FLUSH_DATA (para forzar la escritura de la información de debug a
las 3 tablas).

¿Por qué debemos usar el perfilador?


Dada mi experiencia como programador de C he visto muchas veces que una herramienta de este tipo es un
valiosísima para dos tareas principales:

- testear mi código para asegurarme de que tengo cubierto el 100% de los posibles funcionamientos
- optimizar mis algoritmos para hallar las zonas que generan más carga para el sistema.

¡No se como alguien puede hacer pruebas y optimizaciones de código sin usar un perfilador! TOAD y muchos
otros programas también pueden funcionar con DBMS_PROFILER.

Instalando DBMS_PROFILER
DBMS_PROFILER simplemente es un paquete y unas tablas que por lo general no están instaladas por
defecto. Instalarlo para toda la base de datos es tan fácil como:

- posicionarse en el directorio $ORACLE_HOME/rdbms/admin


- conectarse a la base de datos como SYS "as sysdba" y ejecutar el script profload.sql
- conectarse a la base de datos como usuario normal y ejecutar el script proftab.sql

En un gran bucle en el que queremos grabar los cambios cada 1.000 registros (o cada millón o billón de
registros), ¿qué será más efectivo: usar mod() y luego COMMIT, o declarar un contador (como counter :=
counter+1, if count ...), grabar y resetear el contador (counter := 0) como por ejemplo en el siguiente caso?

1).....
START LOOP
....
cnt := cnt + 1;
IF ( cnt%1000 ) = 0 THEN <= using mod() function
commit;
END IF;
....
END LOOP;
....

2)....
START LOOP
....
cnt := cnt + 1;
IF cnt = 1000 THEN <= no mod() function
commit;
cnt := 0;
END IF;
....
END LOOP;
....

¿Cuál será el mejor planteamiento, el 1 ó el 2?

Lo más rápido sería configurar suficiente espacio de rollback y hacer el UPDATE en una única pasada. La
segunda opción más rápida sería configurar suficiente espacio de rollback y realizar el UPDATE en un bucle único
sin COMMIT intermedios. Estos COMMIT son lo que nos retrasan. También deberíamos considerar como reiniciar
este proceso si se producen errores ORA-01555. La respuesta final queda en el aire ...

DBMS_PROFILER es un modo fácil de determinar el mejor algoritmo a usar. Como un ejemplo he creado y
ejecutado los dos siguientes procedimientos:

CREATE OR REPLACE PROCEDURE do_mod


AS
cnt number := 0;
BEGIN
dbms_profiler.start_profiler( 'mod' );
FOR i IN 1 .. 500000 LOOP
cnt := cnt + 1;
IF ( mod(cnt,1000) = 0 ) THEN
COMMIT;
END IF;
END LOOP;

dbms_profiler.stop_profiler;
END;
/

CREATE OR REPLACE PROCEDURE no_mod


AS
cnt number := 0;
BEGIN
dbms_profiler.start_profiler( 'no_mod' );
FOR i IN 1 .. 500000 LOOP
cnt := cnt + 1;
IF ( cnt = 1000 ) THEN
COMMIT;
cnt := 0;
END IF;
END LOOP;

dbms_profiler.stop_profiler;
end;
/

exec do_mod
exec no_mod

ó bien, si las llamadas al perfilador no están en el código de los objetos PL/SQL:

exec DBMS_PROFILER.START_PROFILER('Ejemplo Do_Mod');


exec do_mod
execute DBMS_PROFILER.STOP_PROFILER;
exec DBMS_PROFILER.START_PROFILER('Ejemplo No_Mod');
exec no_mod
execute DBMS_PROFILER.STOP_PROFILER;

Ahora debemos consultar los datos a través de las tres tablas creadas. Primero debemos seleccionar que
ejecución (RunId) queremos analizar:
set linesize 200 trimout on
column runid format 99999
column run_owner format a15
column run_comment format a50
SELECT runid, run_owner, TO_CHAR(run_date, 'DD/MM/YY HH24:MI:SS') executed,
run_comment FROM plsql_profiler_runs ORDER BY runid;

RUNID RUN_OWNER EXECUTED RUN_COMMENT


------ --------------- ----------------- --------------------------
1 SCOTT 11/04/08 20:40:59 mod
2 SCOTT 11/04/08 20:41:03 no_mod

Seleccionaremos la 1. Ahora debemos seleccionar que unidad de programa perteneciente a la 1 queremos ver:

set define on scan on


COLUMN unit_number FORMAT 9999
COLUMN unit_type FORMAT A15
COLUMN unit_owner FORMAT A15
SELECT runid, unit_number, unit_type, unit_owner,
TO_CHAR(unit_timestamp, 'DD/MM/YY HH24:MI:SS'), unit_name
FROM plsql_profiler_units
WHERE unit_owner <> '<anonymous>'
AND runid = &rpt_runid;

RUNID UNIT_NUMBER UNIT_TYPE UNIT_OWNER TO_CHAR(UNIT_TIME UNIT_NAME


------ ----------- --------------- ---------- ----------------- --------------------
1 1 PROCEDURE SCOTT 11/04/08 19:37:31 DO_MOD

Y finalmente veremos el desglose de tiempos para la unidad 1 (es la típica):

COLUMN unit_name FORMAT A20


COLUMN line# FORMAT 9999
COLUMN passes FORMAT 99999999
COLUMN total_time FORMAT 999999.99999
SELECT pu.unit_name, pd.line#, pd.total_occur passes, ROUND(pd.total_time /
1000000000,3) total_time, us.text text
FROM plsql_profiler_data pd, plsql_profiler_units pu, user_source us
WHERE pd.runid = &rpt_runid
AND pd.unit_number = &rpt_unitid
AND pd.runid = pu.runid
AND pd.unit_number = pu.unit_number
AND us.name = pu.unit_name
AND us.line = pd.line#
ORDER BY line#;

UNIT_NAME LINE# PASSES TOTAL_TIME TEXT


------------- ----- --------- ------------- ---------------------------------------
DO_MOD 3 0 .00000 cnt number := 0;
DO_MOD 5 0 .00000 dbms_profiler.start_profiler( 'mod' );
DO_MOD 6 500001 80.12651 FOR i IN 1 .. 500000 LOOP
DO_MOD 7 500000 95.77795 cnt := cnt + 1;
DO_MOD 8 500000 491.84364 IF ( mod(cnt,1000) = 0 ) THEN
DO_MOD 9 500 1.44672 COMMIT;
DO_MOD 13 1 .00161 dbms_profiler.stop_profiler;

Si sumo los tiempos parciales veo que retorna el siguiente tiempo total:

SELECT SUM(ROUND(pd.total_time / 1000000000,3)) "Total time"


FROM plsql_profiler_data pd, plsql_profiler_units pu, user_source us
WHERE pd.runid = &rpt_runid
AND pd.unit_number = &rpt_unitid
AND pd.runid = pu.runid
AND pd.unit_number = pu.unit_number
AND us.name = pu.unit_name
AND us.line = pd.line#;

Total time
----------
669,198

Usar la función MOD() tomó 6,69 segundos. Hacer el if(cnt=1000) tomó 0,89 segundos más el tiempo
para hacer cnt := 0 más el tiempo para hacer el IF ( cnt = 1000 ) THEN lo que hacen 2,58 segundos. Así que
estamos obteniendo 6,69 segundos para mod y 2,58 segundos para cnt=1000; cnt:=0.

(Ver el DBMS_PROFILER.log )

Por último vaciaremos las tablas en este orden porque tienen relaciones entre ellas:

DELETE FROM PLSQL_PROFILER_DATA;


DELETE FROM PLSQL_PROFILER_UNITS;
DELETE FROM PLSQL_PROFILER_RUNS;
COMMIT;

Potrebbero piacerti anche