Está en la página 1de 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;

el comando: GRANT select_catalog_role,plustrace TO SCOTT; Repaso a algunas herramientas.- En este tema veremos las
el comando: GRANT select_catalog_role,plustrace TO SCOTT; Repaso a algunas herramientas.- En este tema veremos las
el comando: GRANT select_catalog_role,plustrace TO SCOTT; Repaso a algunas herramientas.- En este tema veremos las

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 .

se verán son de modo texto, como los buenos programadores . Para el ajuste de aplicaciones

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.como sea posible. Estas herramientas para programadores son: Explain Plan. AutoTrace. TkProf. DBMS_PROFILER. Describiré

Explain Plan.Estas herramientas para programadores son: SQL*Plus. AutoTrace. TkProf. DBMS_PROFILER. Describiré el principal

AutoTrace.herramientas para programadores son: SQL*Plus. Explain Plan. TkProf. DBMS_PROFILER. Describiré el principal uso de cada

TkProf.para programadores son: SQL*Plus. Explain Plan. AutoTrace. DBMS_PROFILER. Describiré el principal uso de cada

DBMS_PROFILER.son: SQL*Plus. Explain Plan. AutoTrace. TkProf. Describiré el principal uso de cada herramienta, explicaré

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

recrear si hemos metido la pata) con el script %ORACLE_HOME%\ rdbms\admin\ utlsampl.sql como usuario con privilegios
recrear si hemos metido la pata) con el script %ORACLE_HOME%\ rdbms\admin\ utlsampl.sql como usuario con privilegios
recrear si hemos metido la pata) con el script %ORACLE_HOME%\ rdbms\admin\ utlsampl.sql como usuario con privilegios

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:

y para que sirve cada parámetro de los más usados: REM Desactivar la salida de SQL*Plus:
y para que sirve cada parámetro de los más usados: REM Desactivar la salida de SQL*Plus:

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

Además cuando estoy aburrido del tipo de letra tipo Terminal de sqlplusw.exe o necesito ver más

Terminal

Además cuando estoy aburrido del tipo de letra tipo Terminal de sqlplusw.exe o necesito ver más

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

set SQLPLUS_FONT_CHARSET=WestEurope set SQLPLUS_FONT_SIZE=16 Obviamente existe un amplio manual en el que se muestran en

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.)

de ejecución usado por una sentencia cuando fue ejecutada.) Preparando Explain Plan Para ejecutar Explain Plan
de ejecución usado por una sentencia cuando fue ejecutada.) Preparando Explain Plan Para ejecutar Explain Plan
de ejecución usado por una sentencia cuando fue ejecutada.) Preparando Explain Plan Para ejecutar Explain Plan
de ejecución usado por una sentencia cuando fue ejecutada.) Preparando Explain Plan Para ejecutar Explain Plan

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

antes una serie de scripts de $ORACLE_HOME/ rdbms/ admin : utlxplan.sql (UTiLity eXplain PLAN table), que

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 =

SET statement_id =

texto

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.

almacenar el plan de ejecución en otra tabla si se desea. Ahora crearemos una tabla de
almacenar el plan de ejecución en otra tabla si se desea. Ahora crearemos una tabla de

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.

12 filas seleccionadas. Esto muestra que Oracle evaluó el plan. En pocas palabras
12 filas seleccionadas. Esto muestra que Oracle evaluó el plan. En pocas palabras

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.

que se aplican en cada paso del plan de ejecución. Una comparación con AutoTrace Aunque no
que se aplican en cada paso del plan de ejecución. Una comparación con AutoTrace Aunque no

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:

les añadiré una clave primaria para que estén indexadas: ALTER SESSION SET optimizer_mode=RULE; CREATE TABLE emp2

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.

y las descripciones de la guía son muy comprensibles. Para construir un árbol debemos empezar desde
y las descripciones de la guía son muy comprensibles. Para construir un árbol debemos empezar desde
y las descripciones de la guía son muy comprensibles. Para construir un árbol debemos empezar desde

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

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

* FROM salgrade2) LOOP FOR emp2 IN (SELECT * FROM emp2) LOOP El full scan de
* FROM salgrade2) LOOP FOR emp2 IN (SELECT * FROM emp2) LOOP El full scan de
* FROM salgrade2) LOOP FOR emp2 IN (SELECT * FROM emp2) LOOP El full scan de
* FROM salgrade2) LOOP FOR emp2 IN (SELECT * FROM emp2) LOOP El full scan de

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.

ROWID. Eso es el resultado del paso 1, nuestro resultado. Para hacerlo más interesante ejecutaremos una
ROWID. Eso es el resultado del paso 1, nuestro resultado. Para hacerlo más interesante ejecutaremos una
ROWID. Eso es el resultado del paso 1, nuestro resultado. Para hacerlo más interesante ejecutaremos una
ROWID. Eso es el resultado del paso 1, nuestro resultado. Para hacerlo más interesante ejecutaremos una

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:

del plan de ejecución y evaluar como procesa la consulta: EXPLAIN PLAN FOR SELECT ename, dname,
del plan de ejecución y evaluar como procesa la consulta: EXPLAIN PLAN FOR SELECT ename, dname,
del plan de ejecución y evaluar como procesa la consulta: EXPLAIN PLAN FOR SELECT ename, dname,
del plan de ejecución y evaluar como procesa la consulta: EXPLAIN PLAN FOR SELECT ename, dname,

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:

del paso 4. El árbol de evaluación será como éste: Empezando con los pasos 3 y

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);

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

Name

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

| Id

| Operation

|

| 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);

FROM t T1 WHERE object_id > 32000 AND child_number = 0); @?\rdbms\admin\utlxpls
FROM t T1 WHERE object_id > 32000 AND child_number = 0); @?\rdbms\admin\utlxpls

@?\rdbms\admin\utlxpls

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

| Id

| Operation

|

Name

| Rows

| Bytes | Cost

|

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

| 0 | SELECT STATEMENT

| TABLE ACCESS BY INDEX ROWID

1

|

|

|

T

|

|

| 235| 22560 | 21 (5)|

|

|

| INDEX RANGE SCAN

2

|

| 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 VIEW (Cost=12 Card=384 Bytes=44928)

0

2 UNION-ALL

3 HASH JOIN (OUTER) (Cost=6 Card=383 Bytes=25661)

4 3

5 3

6 HASH JOIN (ANTI) (Cost=6 Card=1 Bytes=33)

7 6

8 6

1

2

2

TABLE ACCESS (FULL) OF 'EMP' (Cost=3 Card=14 Bytes=5

TABLE ACCESS (FULL) OF 'DEPT' (Cost=3 Card=82 Bytes=

TABLE ACCESS (FULL) OF 'DEPT' (Cost=3 Card=82 Bytes=

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 VIEW (Cost=12 Card=384 Bytes=44928)

0

2 UNION-ALL

3 HASH JOIN (OUTER) (Cost=6 Card=383 Bytes=25661)

4 3

5 3

6 HASH JOIN (ANTI) (Cost=6 Card=1 Bytes=33)

7 6

8 6

1

2

2

TABLE ACCESS (FULL) OF 'EMP' (Cost=3 Card=14 Bytes=518)

TABLE ACCESS (FULL) OF 'DEPT' (Cost=3 Card=82 Bytes=2460)

TABLE ACCESS (FULL) OF 'DEPT' (Cost=3 Card=82 Bytes=2460)

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

Recursive calls

Número de sentencias SQL ejecutadas para completar la sentencia.

Db block gets

Número total de bloques leídos desde el buffer cache.

Consistent gets

Número de veces que una lectura consistente solicitó un bloque 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.

SQL*Net roundtrips to/from client

Número total de mensajes SQL*Net enviados a y recibidos por el 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.

Sorts (disk)

Ordenaciones que usan el tablespace temporal (en disco) porque 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 .

Una consulta SQL generalmente realizará consistent gets . Estas dos son las partes más importantes del
Una consulta SQL generalmente realizará consistent gets . Estas dos son las partes más importantes del
Una consulta SQL generalmente realizará consistent gets . Estas dos son las partes más importantes del
Una consulta SQL generalmente realizará consistent gets . Estas dos son las partes más importantes del
Una consulta SQL generalmente realizará consistent gets . Estas dos son las partes más importantes del
Una consulta SQL generalmente realizará consistent gets . Estas dos son las partes más importantes del
Una consulta SQL generalmente realizará consistent gets . Estas dos son las partes más importantes del

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 NESTED LOOPS

0

2 NESTED LOOPS

1

3 TABLE ACCESS (FULL) OF 'MAP'

2

4 TABLE ACCESS (BY INDEX ROWID) OF 'I2'

5 4

2

INDEX (UNIQUE SCAN) OF 'SYS_C00683648' (UNIQUE)

6 TABLE ACCESS (BY INDEX ROWID) OF 'I1'

1

¿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 =

xAND i1.v =

x

AND i2.v =

;y

y

; y

no rows selected

Execution Plan

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

0 SELECT STATEMENT Optimizer=CHOOSE

1 NESTED LOOPS

0

2 NESTED LOOPS

1

3 TABLE ACCESS (FULL) OF 'MAP'

2

4 TABLE ACCESS (BY INDEX ROWID) OF 'I2'

5 4

2

INDEX (UNIQUE SCAN) OF 'SYS_C003755' (UNIQUE)

6 TABLE ACCESS (BY INDEX ROWID) OF 'I1'

1

7 INDEX (UNIQUE SCAN) OF 'SYS_C003754' (UNIQUE)

6

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 =

xAND i1.v =

x

AND i2.v =

;y

y

; y

no rows selected

Execution Plan

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

0 SELECT STATEMENT Optimizer=CHOOSE (Cost=21 Card=1 Bytes=40)

1 NESTED LOOPS (Cost=21 Card=1 Bytes=40)

2 HASH JOIN (Cost=20 Card=1 Bytes=26)

3 TABLE ACCESS (FULL) OF 'I1' (Cost=10 Card=1 Bytes=14)

4 TABLE ACCESS (FULL) OF 'MAP' (Cost=9 Card=30020 Bytes=360240)

5 TABLE ACCESS (BY INDEX ROWID) OF 'I2' (Cost=1 Card=1 Bytes=14)

6 INDEX (UNIQUE SCAN) OF 'SYS_C003755' (UNIQUE)

0

1

2

2

1

5

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 ayudar el crear un índice en i1.v o en i2.v:

= value y i2.v = value. Tal vez pueda

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 =

xAND i1.v =

x

AND i2.v =

;y

y

; y

no rows selected

Execution Plan

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

0 SELECT STATEMENT Optimizer=CHOOSE (Cost=13 Card=1 Bytes=40)

1 NESTED LOOPS (Cost=13 Card=1 Bytes=40)

2 HASH JOIN (Cost=12 Card=1 Bytes=26)

3 TABLE ACCESS (BY INDEX ROWID) OF 'I1' (Cost=2 Card=1 Bytes=14

4 3

5 TABLE ACCESS (FULL) OF 'MAP' (Cost=9 Card=30020 Bytes=360240)

6 TABLE ACCESS (BY INDEX ROWID) OF 'I2' (Cost=1 Card=1 Bytes=14)

INDEX (UNIQUE SCAN) OF 'SYS_C003755' (UNIQUE)

7 6

0

1

2

2

1

INDEX (RANGE SCAN) OF 'I1_IDX' (NON-UNIQUE) (Cost=1 Card=1)

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

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.

Esto ayuda a demostrar que los índices no son siempre buenos y que los

a demostrar que los índices no son siempre buenos y que los full scans En general

full scans

que los índices no son siempre buenos y que los full scans En general la principal
que los índices no son siempre buenos y que los full scans En general la principal
que los índices no son siempre buenos y que los full scans En general la principal
que los índices no son siempre buenos y que los full scans En general la principal
que los índices no son siempre buenos y que los full scans En general la principal

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:

las diferencias de consistent gets entre ejecuciones: CREATE TABLE t TABLESPACE users AS SELECT * FROM
las diferencias de consistent gets entre ejecuciones: CREATE TABLE t TABLESPACE users AS SELECT * FROM

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

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í

(el número de consistent gets ). Cada fila que hemos recogido del servidor ha causado que

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
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

así como se incrementa el tamaño del array el número de consistent gets disminuye. Entonces, ¿significa

consistent gets

el tamaño del array el número de consistent gets disminuye. Entonces, ¿significa esto que se debe

disminuye. Entonces, ¿significa esto que se debe situar el tamaño del array en 5.000 y olvidarnos? Absolutamente no.

tamaño del array en 5.000 y olvidarnos? Absolutamente no. Si os fijáis la media de consistent
tamaño del array en 5.000 y olvidarnos? Absolutamente no. Si os fijáis la media de consistent

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 .

en vez de realizar el proceso en pequeños golpes . NOTA: Una crítica muy común que
en vez de realizar el proceso en pequeños golpes . NOTA: Una crítica muy común que

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.

como bulk no hay razón para usar este método de trabajo. He comprobado empíricamente que un
como bulk no hay razón para usar este método de trabajo. He comprobado empíricamente que un

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.

inmediatamente por operaciones I/O lógicas a la cache. - lecturas directas desde TEMP, como respuesta a
inmediatamente por operaciones I/O lógicas a la cache. - lecturas directas desde TEMP, como respuesta a
inmediatamente por operaciones I/O lógicas a la cache. - lecturas directas desde TEMP, como respuesta a
inmediatamente por operaciones I/O lógicas a la cache. - lecturas directas desde TEMP, como respuesta a

- 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.

allí en el mundo real cuando se ejecutara la sentencia. Yo sugiero ejecutar la consulta dos
allí en el mundo real cuando se ejecutara la sentencia. Yo sugiero ejecutar la consulta dos

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:

mostraré el plan de la consulta para la primera ejecución: ALTER SESSION SET workarea_size_policy = MANUAL;
mostraré el plan de la consulta para la primera ejecución: ALTER SESSION SET workarea_size_policy = MANUAL;
mostraré el plan de la consulta para la primera ejecución: ALTER SESSION SET workarea_size_policy = MANUAL;
mostraré el plan de la consulta para la primera ejecución: ALTER SESSION SET workarea_size_policy = MANUAL;
mostraré el plan de la consulta para la primera ejecución: ALTER SESSION SET workarea_size_policy = MANUAL;
mostraré el plan de la consulta para la primera ejecución: ALTER SESSION SET workarea_size_policy = MANUAL;
mostraré el plan de la consulta para la primera ejecución: ALTER SESSION SET workarea_size_policy = MANUAL;
mostraré el plan de la consulta para la primera ejecución: ALTER SESSION SET workarea_size_policy = MANUAL;
mostraré el plan de la consulta para la primera ejecución: ALTER SESSION SET workarea_size_policy = MANUAL;
mostraré el plan de la consulta para la primera ejecución: ALTER SESSION SET workarea_size_policy = MANUAL;
mostraré el plan de la consulta para la primera ejecución: ALTER SESSION SET workarea_size_policy = MANUAL;
mostraré el plan de la consulta para la primera ejecución: ALTER SESSION SET workarea_size_policy = MANUAL;
mostraré el plan de la consulta para la primera ejecución: ALTER SESSION SET workarea_size_policy = MANUAL;
mostraré el plan de la consulta para la primera ejecución: ALTER SESSION SET workarea_size_policy = MANUAL;
mostraré el plan de la consulta para la primera ejecución: ALTER SESSION SET workarea_size_policy = MANUAL;
mostraré el plan de la consulta para la primera ejecución: ALTER SESSION SET workarea_size_policy = MANUAL;

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

/

29366

rows selected.

calentamiento de la cache son relevantes:

Execution Plan

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

0 SELECT STATEMENT Optimizer=CHOOSE (Cost=4264 Card=29366 Bytes=5638272)

1 MERGE JOIN (Cost=4264 Card=29366 Bytes=5638272)

2 SORT (JOIN) (Cost=2132 Card=29366 Bytes=2819136)

3 TABLE ACCESS (FULL) OF 'T' (Cost=42 Card=29366 Bytes=2819136)

4 SORT (JOIN) (Cost=2132 Card=29366 Bytes=2819136)

5 TABLE ACCESS (FULL) OF 'T' (Cost=42 Card=29366 Bytes=2819136)

0

1

2

1

4

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

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.

hash join ) no reportan ordenaciones a disco porque no ordenan.Si las lecturas físicas no

a disco porque no ordenan.Si las lecturas físicas no Pero ¿qué sucede cuando incrementamos el área
a disco porque no ordenan.Si las lecturas físicas no Pero ¿qué sucede cuando incrementamos el área

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

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

incluso un aumento modesto puede beneficiar a muchos servidores. Su rendimiento total puede ser

sort area retained size . Los parámetros por defecto son muy pequeños e

total puede ser sort area retained size . Los parámetros por defecto son muy pequeños e
total puede ser sort area retained size . Los parámetros por defecto son muy pequeños e
total puede ser sort area retained size . Los parámetros por defecto son muy pequeños e

sort area size

controlaSu rendimiento total puede ser sort area retained size . Los parámetros por defecto son muy

total puede ser sort area retained size . Los parámetros por defecto son muy pequeños e
total puede ser sort area retained size . Los parámetros por defecto son muy pequeños e

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

y sus versiones superiores, en las que se está usando el work area size policy automatizado,

work area size policy

en las que se está usando el work area size policy automatizado, el área de ordenación

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.

para cada única sentencia que se realiza en una sesión. Redo Size. La estadística redo size
para cada única sentencia que se realiza en una sesión. Redo Size. La estadística redo size
para cada única sentencia que se realiza en una sesión. Redo Size. La estadística redo size
para cada única sentencia que se realiza en una sesión. Redo Size. La estadística redo size

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.

se realizan grandes cargas de datos, como en los warehouse. - cargas bulk , con frecuencia
se realizan grandes cargas de datos, como en los warehouse. - cargas bulk , con frecuencia
se realizan grandes cargas de datos, como en los warehouse. - cargas bulk , con frecuencia
se realizan grandes cargas de datos, como en los warehouse. - cargas bulk , con frecuencia
se realizan grandes cargas de datos, como en los warehouse. - cargas bulk , con frecuencia
se realizan grandes cargas de datos, como en los warehouse. - cargas bulk , con frecuencia
se realizan grandes cargas de datos, como en los warehouse. - cargas bulk , con frecuencia
se realizan grandes cargas de datos, como en los warehouse. - cargas bulk , con frecuencia

-

realizan grandes cargas de datos, como en los warehouse. - cargas bulk , con frecuencia son
realizan grandes cargas de datos, como en los warehouse. - cargas bulk , con frecuencia son

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.

de datos en una gran tabla prevalecerán otros métodos. En nuestro entorno de desarrollo nuestras inserciones
de datos en una gran tabla prevalecerán otros métodos. En nuestro entorno de desarrollo nuestras inserciones
de datos en una gran tabla prevalecerán otros métodos. En nuestro entorno de desarrollo nuestras inserciones

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?

log , llenando el destino de archivado. ¿Qué hacemos mal? Realmente nada está yendo mal. Únicamente
log , llenando el destino de archivado. ¿Qué hacemos mal? Realmente nada está yendo mal. Únicamente
log , llenando el destino de archivado. ¿Qué hacemos mal? Realmente nada está yendo mal. Únicamente
log , llenando el destino de archivado. ¿Qué hacemos mal? Realmente nada está yendo mal. Únicamente
log , llenando el destino de archivado. ¿Qué hacemos mal? Realmente nada está yendo mal. Únicamente

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!

de desarrollo y pruebas, no en la de paso a producción! Vamos a ver un ejemplo
de desarrollo y pruebas, no en la de paso a producción! Vamos a ver un ejemplo

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

(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:

redo . Hemos usado la sintaxis para el INSERT

dos casos: redo . Hemos usado la sintaxis para el INSERT direct-path - cuando se está
dos casos: redo . Hemos usado la sintaxis para el INSERT direct-path - cuando se está
dos casos: redo . Hemos usado la sintaxis para el INSERT direct-path - cuando se está
dos casos: redo . Hemos usado la sintaxis para el INSERT direct-path - cuando se está
dos casos: redo . Hemos usado la sintaxis para el INSERT direct-path - cuando se está

direct-path

redo . Hemos usado la sintaxis para el INSERT direct-path - cuando se está usando una
redo . Hemos usado la sintaxis para el INSERT direct-path - cuando se está usando una
redo . Hemos usado la sintaxis para el INSERT direct-path - cuando se está usando una
redo . Hemos usado la sintaxis para el INSERT direct-path - cuando se está usando una

- 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;

operación sobre una tabla que está marcada como NOLOGGING; El INSERT /*+ APPEND */ minimizará la
operación sobre una tabla que está marcada como NOLOGGING; El INSERT /*+ APPEND */ minimizará la

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:

Vamos a cambiar este modo en la tabla y a probar de nuevo: ALTER TABLE big_table
Vamos a cambiar este modo en la tabla y a probar de nuevo: ALTER TABLE big_table
Vamos a cambiar este modo en la tabla y a probar de nuevo: ALTER TABLE big_table
Vamos a cambiar este modo en la tabla y a probar de nuevo: ALTER TABLE big_table
Vamos a cambiar este modo en la tabla y a probar de nuevo: ALTER TABLE big_table
Vamos a cambiar este modo en la tabla y a probar de nuevo: ALTER TABLE big_table

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 .

INSERT por direct-path virtualmente no ha generado redo . De hecho el redo generado por el
INSERT por direct-path virtualmente no ha generado redo . De hecho el redo generado por el
INSERT por direct-path virtualmente no ha generado redo . De hecho el redo generado por el
INSERT por direct-path virtualmente no ha generado redo . De hecho el redo generado por el
INSERT por direct-path virtualmente no ha generado redo . De hecho el redo generado por el
INSERT por direct-path virtualmente no ha generado redo . De hecho el redo generado por el
INSERT por direct-path virtualmente no ha generado redo . De hecho el redo generado por el

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.

(INSERT, UPDATE, DELETE y MERGE) lo seguirán generando. Así pues, ¿significa esto que debemos declarar todas
(INSERT, UPDATE, DELETE y MERGE) lo seguirán generando. Así pues, ¿significa esto que debemos declarar todas
(INSERT, UPDATE, DELETE y MERGE) lo seguirán generando. Así pues, ¿significa esto que debemos declarar todas
(INSERT, UPDATE, DELETE y MERGE) lo seguirán generando. Así pues, ¿significa esto que debemos declarar todas
(INSERT, UPDATE, DELETE y MERGE) lo seguirán generando. Así pues, ¿significa esto que debemos declarar todas
(INSERT, UPDATE, DELETE y MERGE) lo seguirán generando. Así pues, ¿significa esto que debemos declarar todas

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 Muchas veces
operaciones INSERT AS SELECT (= INSERT bulk ), no funciona con operaciones INSERT Muchas veces

operaciones INSERT AS SELECT (= INSERT bulk ), no funciona con operaciones INSERT Muchas veces he visto trozos de código con:

INSERT /*+ APPEND */ INTO t VALUES (

)

VALUES.

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).

cuando le pregunté al programador acerca de ello me dijo

cuando le pregunté al programador acerca de ello me dijo Una importante razón para evitar el
cuando le pregunté al programador acerca de ello me dijo Una importante razón para evitar el

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.

no estar los datos en los archivelog no se pueden recuperar. Aquí hay una pocas razones
no estar los datos en los archivelog no se pueden recuperar. Aquí hay una pocas razones
no estar los datos en los archivelog no se pueden recuperar. Aquí hay una pocas razones
no estar los datos en los archivelog no se pueden recuperar. Aquí hay una pocas razones
no estar los datos en los archivelog no se pueden recuperar. Aquí hay una pocas razones
no estar los datos en los archivelog no se pueden recuperar. Aquí hay una pocas razones
no estar los datos en los archivelog no se pueden recuperar. Aquí hay una pocas razones
no estar los datos en los archivelog no se pueden recuperar. Aquí hay una pocas razones
no estar los datos en los archivelog no se pueden recuperar. Aquí hay una pocas razones
no estar los datos en los archivelog no se pueden recuperar. Aquí hay una pocas razones

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

- el INSERT direct-path

High Water Mark ) de la tabla, ignorando

de SQLLoader. Si

se borran muchas de las filas de la tabla y después se hacen INSERT /*+ APPEND */ en ella no

cualquier espacio libre en las free lists , así como lo hacen las cargas direct-path

las free lists , así como lo hacen las cargas direct-path escribe los datos sobre la

escribe los datos sobre la HWM (=

las cargas direct-path escribe los datos sobre la HWM (= será posible reusar el espacio, con
las cargas direct-path escribe los datos sobre la HWM (= será posible reusar el espacio, con
las cargas direct-path escribe los datos sobre la HWM (= será posible reusar el espacio, con
las cargas direct-path escribe los datos sobre la HWM (= será posible reusar el espacio, con
las cargas direct-path escribe los datos sobre la HWM (= será posible reusar el espacio, con

será posible reusar el espacio, con lo que tabla estará defragmentada;

- se debe realizar un COMMIT después de una carga direct-path tabla en esa transacción;

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

exitosa antes de poder leer desde la

- 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

Esta operación es de forma serializada (aunque se pueden hacer operaciones INSERT direct-path desde una misma

pueden hacer operaciones INSERT direct-path

desde una misma sesión);(aunque se pueden hacer operaciones INSERT direct-path Nuevamente esta operación debe ser usada con cautela y

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:

operaciones con índices. Podemos verlo con este ejemplo: CREATE INDEX big_table_idx ON
operaciones con índices. Podemos verlo con este ejemplo: CREATE INDEX big_table_idx ON
operaciones con índices. Podemos verlo con este ejemplo: CREATE INDEX big_table_idx ON
operaciones con índices. Podemos verlo con este ejemplo: CREATE INDEX big_table_idx ON
operaciones con índices. Podemos verlo con este ejemplo: CREATE INDEX big_table_idx ON
operaciones con índices. Podemos verlo con este ejemplo: CREATE INDEX big_table_idx ON
operaciones con índices. Podemos verlo con este ejemplo: CREATE INDEX big_table_idx ON
operaciones con índices. Podemos verlo con este ejemplo: CREATE INDEX big_table_idx ON

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 Rows 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:

CONTEXT FOREVER, LEVEL <n> , donde n puede ser: - 1, para activar SQL_TRACE (= a
CONTEXT FOREVER, LEVEL <n> , donde n puede ser: - 1, para activar SQL_TRACE (= a

- 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;

; - 8, para activar SQL_TRACE y capturar eventos de espera; - 12, para activar SQL_TRACE,
; - 8, para activar SQL_TRACE y capturar eventos de espera; - 12, para activar SQL_TRACE,
; - 8, para activar SQL_TRACE y capturar eventos de espera; - 12, para activar SQL_TRACE,

- 12, para activar SQL_TRACE, variables bind

de espera; - 12, para activar SQL_TRACE, variables bind y eventos de espera. Como podéis ver

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.

un valor suficiente para acomodar la información adicional. Ejecutando TKPROF Para empezar usaremos un rápido test
un valor suficiente para acomodar la información adicional. Ejecutando TKPROF Para empezar usaremos un rápido test
un valor suficiente para acomodar la información adicional. Ejecutando TKPROF Para empezar usaremos un rápido test
un valor suficiente para acomodar la información adicional. Ejecutando TKPROF Para empezar usaremos un rápido test

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

/ora920/udump/ora920_ora_14246.trc tk.prf SYS=no Este comando crea el archivo de texto tk.prf en nuestro
/ora920/udump/ora920_ora_14246.trc tk.prf SYS=no Este comando crea el archivo de texto tk.prf en nuestro

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:

de texto tk.prf encontramos unos resultados como estos: select count(*) from big_table rows ------- ------ ----
de texto tk.prf encontramos unos resultados como estos: select count(*) from big_table rows ------- ------ ----

select count(*) from big_table

rows

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

call

count cpu elapsed

disk

query

current

Parse

1 0.00

0.00

0

0

0

0

Execute

1 0.00

0.00

0

0

0

0

Fetch

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

total

1

2 0.22

4

0.22

0.52

0.52

2433

2433

2442

2442

0

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

---------------------------- Waited ---------- ------------

Times Max. Wait Total 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:

fase de la consulta. Podemos ver tres fases de la consulta: - parse (= parseado). Esta
fase de la consulta. Podemos ver tres fases de la consulta: - parse (= parseado). Esta

- 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 );

) o crea un nuevo plan para la consulta (= hard parse ); - execute (=
) o crea un nuevo plan para la consulta (= hard parse ); - execute (=

- 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;

no queremos parsear una misma sentencia más de una vez; - CPU. Tiempo que se ha
no queremos parsear una misma sentencia más de una vez; - CPU. Tiempo que se ha
no queremos parsear una misma sentencia más de una vez; - CPU. Tiempo que se ha
no queremos parsear una misma sentencia más de una vez; - CPU. Tiempo que se ha

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

en esta fase de la ejecución en milésimas de segundo; - elapsed. Cantidad de tiempo real
en esta fase de la ejecución en milésimas de segundo; - elapsed. Cantidad de tiempo real

- 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;

que sufrimos retrasos por operaciones I/O físicas; - disk. Cantidad de operaciones I/ O físicas realizadas
que sufrimos retrasos por operaciones I/O físicas; - disk. Cantidad de operaciones I/ O físicas realizadas

- 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

Durante una consulta SELECT este valor aparece en la fase fetch (= recuperación); Una pregunta que
Durante una consulta SELECT este valor aparece en la fase fetch (= recuperación); Una pregunta que
Durante una consulta SELECT este valor aparece en la fase fetch (= recuperación); Una pregunta que

fetch

una consulta SELECT este valor aparece en la fase fetch (= recuperación); Una pregunta que frecuentemente

(= 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:

count

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

call

rows

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

cpu

elapsed disk

query

current

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

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

14755

12.77

12.60

4

29511

856828

¿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.

que vemos es de 2 pero el delta real es actualmente de 1,01. Con el tiempo
que vemos es de 2 pero el delta real es actualmente de 1,01. Con el tiempo

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:

Optimizer goal: CHOOSE Parsing user id: 147

Optimizer goal: CHOOSE Parsing user id: 147 Los fallos en la library cache que están a
Optimizer goal: CHOOSE Parsing user id: 147 Los fallos en la library cache que están a

Los fallos en la library cache que están a cero nos están diciendo que la consulta fue

que están a cero nos están diciendo que la consulta fue soft parsed (= Oracle encontró

soft parsed

a cero nos están diciendo que la consulta fue soft parsed (= Oracle encontró que la

(=

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

En este caso es CHOOSE, lo que significa que podía haberse utilizado el optimizador RBO (=
En este caso es CHOOSE, lo que significa que podía haberse utilizado el optimizador RBO (=
En este caso es CHOOSE, lo que significa que podía haberse utilizado el optimizador RBO (=
En este caso es CHOOSE, lo que significa que podía haberse utilizado el optimizador RBO (=
En este caso es CHOOSE, lo que significa que podía haberse utilizado el optimizador RBO (=
En este caso es CHOOSE, lo que significa que podía haberse utilizado el optimizador RBO (=
En este caso es CHOOSE, lo que significa que podía haberse utilizado el optimizador RBO (=

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:

del esquema que ejecutó la consulta. Si hago esta consulta: USERNAME ------------------------------ ---------- ---------

USERNAME

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

OPS$TKYTE

147 24-DEC-02

USER_ID CREATED

---------- --------- OPS$TKYTE 147 24-DEC-02 USER_ID CREATED Puedo ver que usé el usuario de base de
---------- --------- OPS$TKYTE 147 24-DEC-02 USER_ID CREATED Puedo ver que usé el usuario de base de

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.

información imprecisa, como sucedió en este ejemplo. TKPROF muestra, además del plan de ejecución actual, el
información imprecisa, como sucedió en este ejemplo. TKPROF muestra, además del plan de ejecución actual, el

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.

problemáticas de una sentencia SQL que podemos optimizar. Como un extra en esta sección si estamos
problemáticas de una sentencia SQL que podemos optimizar. Como un extra en esta sección si estamos

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

---------------------------- Waited

Times

Max. Wait

----------

Total 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 (

VARCHAR2(30) DEFAULT USER, filename VARCHAR2(512),

dt

username

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

text VARCHAR(4000));

NUMBER PRIMARY KEY,

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;

/

END IF;

END LOOP;

- 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

BEGIN

l_bfile

l_last

l_current

BFILE := bfilename('UDUMP_DIR',p_filename); NUMBER := 1;

NUMBER;

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

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

DEVELOPER ora920_ora_14973.trc 29-DEC-02 04.31.05.241607 PM

DT

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:

Machine: i686 Instance name: ora920

#1 Wed Sep 4 13:35:50 EDT 2002

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

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

- conectarse a la base de datos como usuario normal y ejecutar el script proftab.sql

datos como usuario normal y ejecutar el script proftab.sql En un gran bucle en el que
datos como usuario normal y ejecutar el script proftab.sql En un gran bucle en el que
datos como usuario normal y ejecutar el script proftab.sql En un gran bucle en el que

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?

LOOP; ¿Cuál será el mejor planteamiento, el 1 ó el 2? Lo más rápido sería configurar

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

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

11/04/08 20:40:59 mod

1 SCOTT

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

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