lunes, 10 de septiembre de 2007

Cantidad de CPU utilizada en un procedimiento

Todos conocemos la función DBMS_UTILITY.GET_TIME() que se utiliza para tomar el tiempo entre dos puntos de un determinado proceso. Esta función es muy utilizada por los desarrolladores. La verdad es que no conozco un sólo desarrollador que jamás haya usado ésta función.
En 10g, Oracle agrega una nueva función, DBMS_UTILITY.GET_CPU_TIME() que sirve para saber la cantidad de CPU que es utilizada entre dos puntos de un determinado proceso.

En nuestro primer ejemplo vamos a ver la utilización de éstas 2 funciones sin acceder a disco, y luego, vamos a acceder a disco y vamos a ver la diferencia que se genera tanto en tiempo de CPU como de procesamiento:

Ejemplo SIN I/0 a disco:

SQL_10gR1> DECLARE
2
3 l_inicio_1 PLS_INTEGER ;
4 l_inicio_2 PLS_INTEGER ;
5 l_fin_1 PLS_INTEGER ;
6 l_fin_2 PLS_INTEGER ;
7
8 BEGIN
9
10 l_inicio_1 := DBMS_UTILITY.GET_TIME() ;
11 l_inicio_2 := DBMS_UTILITY.GET_CPU_TIME() ;
12
13 FOR i IN 1 .. 99999999 LOOP
14 NULL ;
15 END LOOP ;

16
17 l_fin_1 := DBMS_UTILITY.GET_TIME() - l_inicio_1 ;
18 l_fin_2 := DBMS_UTILITY.GET_CPU_TIME() - l_inicio_2 ;
19
20 DBMS_OUTPUT.PUT_LINE( 'GET_TIME = ' || l_fin_1 || ' hsecs.' ) ;
21 DBMS_OUTPUT.PUT_LINE( 'GET_CPU_TIME = ' || l_fin_2 || ' hsecs.' ) ;
22
23 END ;
24 /

GET_TIME = 1820 hsecs.
GET_CPU_TIME = 1692 hsecs.


PL/SQL procedure successfully completed.

Ejemplo CON I/0 a disco:

SQL_10gR1> CREATE TABLE test AS
2 SELECT level id
3 FROM dual
4 CONNECT BY level <= 10000000 ;

Table created.

SQL_10gR1> EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'TEST') ;

PL/SQL procedure successfully completed.


SQL_10gR1> DECLARE
2
3 l_inicio_1 PLS_INTEGER ;
4 l_inicio_2 PLS_INTEGER ;
5 l_fin_1 PLS_INTEGER ;
6 l_fin_2 PLS_INTEGER ;
7
8 BEGIN
9
10 l_inicio_1 := DBMS_UTILITY.GET_TIME() ;
11 l_inicio_2 := DBMS_UTILITY.GET_CPU_TIME() ;
12
13 FOR i IN ( SELECT * FROM test ) LOOP
14 NULL ;
15 END LOOP ;

16
17 l_fin_1 := DBMS_UTILITY.GET_TIME() - l_inicio_1 ;
18 l_fin_2 := DBMS_UTILITY.GET_CPU_TIME() - l_inicio_2 ;
19
20 DBMS_OUTPUT.PUT_LINE( 'GET_TIME = ' || l_fin_1 || ' hsecs.' ) ;
21 DBMS_OUTPUT.PUT_LINE( 'GET_CPU_TIME = ' || l_fin_2 || ' hsecs.' ) ;
22
23 END ;
24 /

GET_TIME = 7884 hsecs.
GET_CPU_TIME = 7741 hsecs.


PL/SQL procedure successfully completed.

Comillas simples dentro de cadenas de texto en 10g

En Oracle 10g se agrega una nueva funcionalidad que permite escribir literales en una cadena de texto con comillas simples, sin la necesidad de escribir los literales con dobles, triples, ... comillas. Esta funcionalidad es muy útil cuando los desarrolladores trabajan con sql dinámico, ya que muchas consultas escritas en sql dinámico pueden ser bastante complejas debido a las comillas.
En PL/SQL llamamos a esta funcionalidad de ésta manera q'[.....]'. Podemos reemplazar los [] con cualquiera de éstos caracteres:
[ ]
{ }
( )
< >

Creamos una tabla de ejemplo:

SQL_9iR2> CREATE TABLE emp AS
2 SELECT level id, 'nom_'||level nom
3 FROM dual
4 CONNECT BY level <= 100 ;

Table created.

En Oracle 9iR2 escribiríamos éste código PL/SQL:

SQL_9iR2> DECLARE
2 l_query VARCHAR2(100) ;
3 BEGIN
4 l_query := 'SELECT id FROM emp WHERE nom = ''nom_10''' ;
5 EXECUTE IMMEDIATE l_query ;
6 DBMS_OUTPUT.PUT_LINE(l_query) ;
7 END ;
8 /

SELECT id FROM emp WHERE nom = 'nom_10'

PL/SQL procedure successfully completed.

En 10gR1 podríamos transcribir ese mismo código de la siguiente manera:

SQL_10gR1> DECLARE
2 l_query VARCHAR2(100) ;
3 BEGIN
4 l_query := q'[SELECT id FROM emp WHERE nom = 'nom_10']' ;
5 EXECUTE IMMEDIATE l_query ;
6 DBMS_OUTPUT.PUT_LINE(l_query) ;
7 END ;
8 /

SELECT id FROM emp WHERE nom = 'nom_10'

PL/SQL procedure successfully completed.


SQL_10gR1> DECLARE
2 l_query VARCHAR2(100) ;
3 BEGIN
4 l_query := q'(SELECT id FROM emp WHERE nom = 'nom_10')' ;
5 EXECUTE IMMEDIATE l_query ;
6 DBMS_OUTPUT.PUT_LINE(l_query) ;
7 END ;
8 /

SELECT id FROM emp WHERE nom = 'nom_10'

PL/SQL procedure successfully completed.


SQL_10gR1> DECLARE
2 l_query VARCHAR2(100) ;
3 BEGIN
4 l_query := q'{SELECT id FROM emp WHERE nom = 'nom_10'}' ;
5 EXECUTE IMMEDIATE l_query ;
6 DBMS_OUTPUT.PUT_LINE(l_query) ;
7 END ;
8 /

SELECT id FROM emp WHERE nom = 'nom_10'

PL/SQL procedure successfully completed.

Pueden observar, que con ésta nueva funcionalidad, ya no hace falta preocuparnos por las comillas en las cadenas de texto.

Buscar el valor MAX o MIN de un tabla de forma eficiente

Muchas veces tenemos que buscar el valor máximo o mínimo actual de cierta columna en una tabla, pero suele ser una consulta ineficiente. Generalmente se leen muchos bloques de datos para encontrar el valor MAX/MIN y ésto hace que cuando se ejecute la consulta en nuestro ambiente OLTP bajo gran cantidad de usuarios concurrentes, tengamos graves problemas de performance.
Como es sabido, más del 90% del trabajo de tuning es sobre la aplicación y no necesariamente sobre la base de datos. Este caso es un problema de la aplicación, particularmente cómo se escribe la consulta para obtener el MAX/MIN.

Supongamos el siguiente ejemplo:

SQL_9iR2> CREATE TABLE test AS
2 SELECT level id, 'nom_'||level nombre
3 FROM dual
4 CONNECT BY level <= 1000000 ;

Table created.

SQL_9iR2> EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'TEST') ;

PL/SQL procedure successfully completed.

Tenemos una tabla cargada con 1 millón de registros. Supongamos que queremos obtener el MAX de la columna ID de esta tabla. Escribiríamos la siguiente consulta...

SQL_9iR2> explain plan for
2 SELECT MAX(id)
3 FROM test ;

Explained.

SQL_9iR2> @explains

--------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
--------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 5 | 279 |
| 1 | SORT AGGREGATE | | 1 | 5 | |
| 2 | TABLE ACCESS FULL | TEST | 1000K| 4882K| 279 |
--------------------------------------------------------------------

SQL_9iR2> SET AUTOTRACE TRACEONLY STATISTICS

Statistics
---------------------------------------------------
0 recursive calls
0 db block gets
2889 consistent gets
773 physical reads

0 redo size
329 bytes sent via SQL*Net to client
495 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed

En el Explain Plan vemos que se está realizando un Full Scan de la tabla TEST y en las estadísticas del Autotrace observamos que se leen 2889 bloques de datos desde memoria y 773 bloques desde disco.
Lo ideal es reducir lo mayor posible la cantidad de lecturas lógicas. Cómo podemos optimizar ésta consulta? Qué sucede si creamos un índice único sobre la columna ID?

SQL_9iR2> CREATE UNIQUE INDEX test_id_idx ON test(id) ;

Index created.

SQL_9iR2> EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'TEST', cascade => true) ;

PL/SQL procedure successfully completed.

SQL_9iR2> explain plan for
2 SELECT MAX(id)
3 FROM test ;

Explained.

SQL_9iR2> @explains

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 5 | 279 |
| 1 | SORT AGGREGATE | | 1 | 5 | |
| 2 | INDEX FULL SCAN (MIN/MAX)| TEST_ID_IDX | 1000K| 4882K| 279 |
---------------------------------------------------------------------------

SQL_9iR2> SET AUTOTRACE TRACEONLY STATISTICS

Statistics
---------------------------------------------------
0 recursive calls
0 db block gets
3 consistent gets
0 physical reads

0 redo size
329 bytes sent via SQL*Net to client
495 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed

SQL_9iR2> explain plan for
2 SELECT MIN(id)
3 FROM test ;

Explained.

SQL_9iR2> @explains

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 5 | 279 |
| 1 | SORT AGGREGATE | | 1 | 5 | |
| 2 | INDEX FULL SCAN (MIN/MAX)| TEST_ID_IDX | 1000K| 4882K| 279 |
---------------------------------------------------------------------------

SQL_9iR2> SET AUTOTRACE TRACEONLY STATISTICS

Statistics
---------------------------------------------------
0 recursive calls
0 db block gets
3 consistent gets
0 physical reads

0 redo size
329 bytes sent via SQL*Net to client
495 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed

Vemos que tanto para sacar el MAX o el MIN, se están leyendo solamente 3 bloques de datos desde memoria y ninguno desde disco! Esto no es ninguna magia. Se debe a que se está utilizando el método de acceso INDEX FULL SCAN (MIN/MAX).
Cuando se utiliza el acceso INDEX FULL SCAN, Oracle no lee todos los bloques del índice (no lee todos los bloques que conforman la estructura del árbol del índice), sino que lee solamente los Leaf Blocks del índice (son los bloques del índice que contiene los datos. Son los bloques que se encuentran en la raíz del árbol del índice). Cuando se accede a través del INDEX FULL SCAN (MIN/MAX), Oracle solamente accede al primer Leaf Block en caso de que busquemos el MIN, o al último Leaf Block en caso de que busquemos el MAX. Es una manera muy eficiente de obtener el valor máximo o mínimo de un índice.

Veamos lo siguiente. El ejemplo que acabos de realizar fue creando un índice de forma ascendente (es el default). Qué sucede si realizamos el mismo ejemplo creando un índice de forma descendente?

SQL_9iR2> DROP INDEX test_id_idx ;

Index dropped.

Eliminamos el índice que habíamos creado y ejecutamos la consulta buscando el MAX:

SQL_9iR2> explain plan for
2 SELECT MAX(id)
3 FROM test ;

Explained.

SQL_9iR2> @explains

--------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
--------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 5 | 279 |
| 1 | SORT AGGREGATE | | 1 | 5 | |
| 2 | TABLE ACCESS FULL | TEST | 1000K| 4882K| 279 |
--------------------------------------------------------------------

SQL_9iR2> SET AUTOTRACE TRACEONLY STATISTICS

Statistics
---------------------------------------------------
0 recursive calls
0 db block gets
2889 consistent gets
773 physical reads

0 redo size
329 bytes sent via SQL*Net to client
495 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed

Ahora creamos un índice descendiente por la columna ID:

SQL_9iR2> CREATE UNIQUE INDEX test_id_idx ON test(id DESC) ;

Index created.

SQL_9iR2> EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'TEST', cascade => true) ;

PL/SQL procedure successfully completed.

Ejecutamos la consulta buscando el MAX y luego el MIN:

SQL_9iR2> explain plan for
2 SELECT MAX(id)
3 FROM test ;

Explained.

SQL_9iR2> @explains

--------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
--------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 5 | 279 |
| 1 | SORT AGGREGATE | | 1 | 5 | |
| 2 | TABLE ACCESS FULL | TEST | 1000K| 4882K| 279 |
--------------------------------------------------------------------

SQL_9iR2> SET AUTOTRACE TRACEONLY STATISTICS

Statistics
---------------------------------------------------
0 recursive calls
0 db block gets
2889 consistent gets
773 physical reads

0 redo size
329 bytes sent via SQL*Net to client
495 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed

SQL_9iR2> explain plan for
2 SELECT MIN(id)
3 FROM test ;

Explained.

SQL_9iR2> @explains

--------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
--------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 5 | 279 |
| 1 | SORT AGGREGATE | | 1 | 5 | |
| 2 | TABLE ACCESS FULL | TEST | 1000K| 4882K| 279 |
--------------------------------------------------------------------

SQL_9iR2> SET AUTOTRACE TRACEONLY STATISTICS

Statistics
---------------------------------------------------
0 recursive calls
0 db block gets
2889 consistent gets
773 physical reads

0 redo size
329 bytes sent via SQL*Net to client
495 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed

Ouch!!! Que pasó??? El CBO no está utilizando nuestro índice! Pero porqué? Bueno, ésto es debido al Bug 1389479. EL CBO no puede utilizar el tipo de acceso INDEX FULL SCAN (MIN/MAX) cuando creamos un índice descendiente.

Utilizando Hint's sobre vistas

Hay 2 preguntas que me hacen muy a menudo: Cuando consulto una vista ¿Cómo puedo hacer para influenciar al CBO con un Hint sin modificar el contenido de la vista? ¿Porqué el CBO no toma los Hint's que le coloco cuando consulto la vista?

Bien, generalmente veo que cuando se utilizan Hint's sobre las vistas... se hace de forma incorrecta. Lo importante es saber cómo escribir el Hint para que el CBO lo utilice. Para ésto, vamos a realizar un ejemplo.
La primer parte del ejemplo va a consistir en crear una vista y colocar un Hint para que utilice un índice. En la segunda parte vamos a crear otra vista más sobre la vista anterior, y vamos a colocar un Hint para que utilice un índice de la primer vista.

Comencemos creando las tablas, índice y estadísticas...

SQL_9iR2> CREATE TABLE a AS
2 SELECT level id, 'desc_a_'||level desc_a
3 FROM dual
4 CONNECT BY level <= 10000 ;

Table created.

SQL_9iR2> CREATE TABLE b AS
2 SELECT level id, 'desc_b_'||level desc_b
3 FROM dual
4 CONNECT BY level <= 10000 ;

Table created.

SQL_9iR2> CREATE UNIQUE INDEX a_id_idx ON a(id) ;

Index created.

SQL_9iR2> EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'A', cascade => true) ;

PL/SQL procedure successfully completed.

SQL_9iR2> EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'B') ;

PL/SQL procedure successfully completed.

Creamos la primer vista:

SQL_9iR2> CREATE OR REPLACE VIEW view_a_b AS
2 SELECT a.id, a.desc_a, b.desc_b
3 FROM a, b
4 WHERE a.id = b.id ;

View created.

Veamos el Explain Plan de la vista...

SQL_9iR2> explain plan for
2 SELECT *
3 FROM view_a_b ;

Explained.

SQL_9iR2> @explains

--------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
--------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10000 | 292K| 12 |
|* 1 | HASH JOIN | | 10000 | 292K| 12 |
| 2 | TABLE ACCESS FULL | A | 10000 | 146K| 4 |
| 3 | TABLE ACCESS FULL | B | 10000 | 146K| 4 |
--------------------------------------------------------------------

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

1 - access("A"."ID"="B"."ID")

Lo que notamos es que se está realizando un Full Scan de las tabla A y B. Que sucede si a modo de ejemplo queremos hacer que la tabla A acceda por el índice que creamos antes? El CBO eligió el explain plan que vemos como el más óptimo (de hecho lo es), pero supongamos que para nosotros es más conveniente acceder por índice... tendríamos que utilizar un Hint para decirle al CBO que acceda por índice en vez de realizar un Full Scan. Pero como hacemos ésto a través de una vista??? Veamos...

SQL_9iR2> explain plan for
2 SELECT /*+ INDEX(view_a_b.a a_id_idx) */ *
3 FROM view_a_b ;

Explained.

SQL_9iR2> @explains

----------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
----------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10000 | 292K| 58 |
|* 1 | HASH JOIN | | 10000 | 292K| 58 |
| 2 | TABLE ACCESS BY INDEX ROWID| A | 10000 | 146K| 50 |
| 3 | INDEX FULL SCAN | A_ID_IDX | 10000 | | 21 |
| 4 | TABLE ACCESS FULL | B | 10000 | 146K| 4 |
----------------------------------------------------------------------------

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

1 - access("A"."ID"="B"."ID")

Fijense que en el Hint estoy anteponiendo al nombre de la tabla A, el nombre de la vista. Porqué? Porque si coloco solamente en nombre de la tabla A en el Hint, el CBO no lo toma ya que la tabla A no existe en en nuestra consulta, si existe dentro de la vista VIEW_A_B, es por eso que tenemos que anteponer el nombre de la vista y luego hacer referencia a la tabla A.

Qué suecede si tenemos una vista de una vista y queremos hacer lo mismo que hicimos antes?

Creamos la segunda vista:

SQL_9iR2> CREATE OR REPLACE VIEW view_a_b_2 AS
2 SELECT *
3 FROM view_a_b
4 WHERE id BETWEEN 1 AND 500 ;

View created.

Veamos el Explain Plan de la vista...

SQL_9iR2> explain plan for
2 SELECT *
3 FROM view_a_b_2 ;

Explained.

SQL_9iR2> @explains

--------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
--------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 499 | 14970 | 9 |
|* 1 | HASH JOIN | | 499 | 14970 | 9 |
|* 2 | TABLE ACCESS FULL | A | 500 | 7500 | 4 |
|* 3 | TABLE ACCESS FULL | B | 500 | 7500 | 4 |
--------------------------------------------------------------------

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

1 - access("A"."ID"="B"."ID")
2 - filter("A"."ID">=1 AND "A"."ID"<=500)
3 - filter("B"."ID">=1 AND "B"."ID"<=500)

Sucedió exactamente lo mismo que en el ejemplo anterior utilizando una única vista. El CBO está accediendo por Full Scan. Pero nosotros somos caprichosos y pensamos que con un índice la consulta sería más eficiente. Cómo le decimos al CBO a través de un Hint que acceda por índice?

SQL_9iR2> explain plan for
2 SELECT /*+ INDEX(view_a_b_2.view_a_b.a a_id_idx) */ *
3 FROM view_a_b_2 ;

Explained.

SQL_9iR2> @explains

----------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
----------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 499 | 14970 | 10 |
|* 1 | HASH JOIN | | 499 | 14970 | 10 |
| 2 | TABLE ACCESS BY INDEX ROWID| A | 500 | 7500 | 5 |
|* 3 | INDEX RANGE SCAN | A_ID_IDX | 500 | | 3 |
|* 4 | TABLE ACCESS FULL | B | 500 | 7500 | 4 |
----------------------------------------------------------------------------

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

1 - access("A"."ID"="B"."ID")
3 - access("A"."ID">=1 AND "A"."ID"<=500)
4 - filter("B"."ID">=1 AND "B"."ID"<=500)

Ahora no sólo estoy anteponiendo en el Hint el nombre de la vista, sino que también estoy anteponiendo el nombre de la segunda vista que creamos. Porqué? Por la misma razón que en el ejemplo anterior. Si hacemos referencia en el Hint sólo a la tabla A, el CBO no utiliza el Hint ya que la tabla A no existe en nuestra consulta, tampoco existe en la vista VIEW_A_B_2, pero si existe dentro de la vista VIEW_A_B; es por eso que tenemos que anteponer el nombre de las 2 vistas y luego hacer referencia a la tabla A.

sábado, 8 de septiembre de 2007

Monitoreando el progreso de largas ejecuciones

Para monitorear las largas ejecuciones que se producen en la base de datos, contamos con la vista del diccionario de datos llamada V$SESSION_LONGOPS. Esta vista nos permite monitorear tanto las sentencias DML como así también las sentencias DDL.
Pero cuáles son las sentencias que aparecen en esa vista?
Una ejecución es considerada una operación 'larga' y aparece en la vista V$SESSION_LONGOPS si la ejecución demora más de 6 segundos. Este no es el único criterio por el cual una sentencia aparece en la vista. También aparecen las operaciones en la vista si Oracle tiene que leer más de 10.000 bloques de la tabla.
Pero no todas las operaciones que cumplen estos criterios aparecen en esa vista.

Cada versión de Oracle agrega más tipos de operaciones a la vista V$SESSION_LONGOPS.
Algunas de esas operaciones son:
- Table Scan
- Index Fast Full Scan
- Hash Join
- Sort/Merge
- Sort Output
- Estadísticas
- Rollback

Veamos un ejemplo:

SQL_9iR2> CREATE TABLE test AS
2 SELECT level a, level b, level c
3 FROM dual
4 CONNECT BY level <= 1000000 ;

Table created.

SQL_9iR2> EXEC dbms_stats.GATHER_TABLE_STATS(USER,'TEST') ;

PL/SQL procedure successfully completed.

Bien, ya tenemos nuestra tabla creada, cargada con 1 millón de registros y analizada.
Lo que vamos a hacer ahora es ejecutar una consulta en una sesión, y en otra sesión vamos a consultar a la vista V$SESSION_LONGOPS mientras la consulta se ejecuta.

En la SESION_1 ejecuto la consulta (Esta consulta realiza un Full Scan sobre la tabla):

SQL_9iR2> SELECT a, b, c
2 FROM test
3 ORDER BY a ASC, b DESC, c ASC ;

En la SESION_2 ejecuto la consulta sobre la vista:

SQL_9iR2> SELECT sid, serial#, username, opname, sql_hash_value hash_value,
2 TO_CHAR(start_time,'HH24:MI:SS') "INICIO",
3 (sofar/totalwork)*100 "%"
4 FROM v$session_longops
5 WHERE (sofar/totalwork)*100 < 100 ;

no rows selected

Fijense, que mientras estoy corriendo la consulta de la SESION_1, cuando ejecuto la consulta de la SESION_2, no me devuelve ningún registro. Esto sucede porque aún Oracle no leyó 10.000 bloques de datos.
Qué sucede si intento ejecutar la consulta nuevamente?

SQL_9iR2> SELECT sid, serial#, username, opname, sql_hash_value hash_value,
2 TO_CHAR(start_time,'HH24:MI:SS') "INICIO",
3 (sofar/totalwork)*100 "%"
4 FROM v$session_longops
5 WHERE (sofar/totalwork)*100 < 100 ;

SID SERIAL# USERNAME OPNAME HASH_VALUE INICIO %
---------- ---------- ---------- --------------- ---------- -------- ----------
87 6852 LEO Sort Output 3570479103 22:27:58 4.24448217

1 row selected.

Vemos que el porcentaje está en 4.24 y tenemos que llegar al 100 porciento para que la consulta termine de ejecutarse.
Ejecuto nuevamente la consulta:

SQL_9iR2> SELECT sid, serial#, username, opname, sql_hash_value hash_value,
2 TO_CHAR(start_time,'HH24:MI:SS') "INICIO",
3 (sofar/totalwork)*100 "%"
4 FROM v$session_longops
5 WHERE (sofar/totalwork)*100 < 100 ;

SID SERIAL# USERNAME OPNAME HASH_VALUE INICIO %
---------- ---------- ---------- --------------- ---------- -------- ----------
87 6852 LEO Sort Output 3570479103 22:27:58 7.53820034

1 row selected.

El porcentaje se incrementó. Ahora se encuentra en 7.5 y aún falta mucho para que la consulta de la SESION_1 termine de ejecutarse.

Algo interesante para ver de la vista, es que tenemos una columna llamada SQL_HASH_VALUE en donde nos muestra el valor hash (recordemos que cuando Oracle tiene que buscar una consulta en la Shared Pool, busca esa consulta mediante un valor hash. Cuando ejecutamos una consulta, Oracle aplica una función sobre la consulta obteniendo el valor hash que vemos en ésta tabla en la columna HASH_VALUE) de la consulta que estamos ejecutando. Si tomamos ese valor y buscamos la consulta en la V$SQLTEXT...

SQL_9iR2> SELECT sql_text
2 FROM v$sqltext
3 WHERE hash_value = 3570479103
4 ORDER BY piece ;

SQL_TEXT
------------------------------------------------------------
SELECT a, b, c FROM test ORDER BY a ASC, b DESC, c ASC

1 row selected.

Bien, ahora veamos lo siguiente. El paquete DBMS_APPLICATION_INFO, contiene un procedimiento llamado SET_SESSION_LONGOPS, que se utiliza para popular la vista V$SESSION_LONGOPS desde una aplicación.

Veamos un ejemplo:

SQL_9iR2> DECLARE
2 l_rindex BINARY_INTEGER := DBMS_APPLICATION_INFO.SET_SESSION_LONGOPS_NOHINT ;
3 l_sofar NUMBER := 0 ;
4 l_slno BINARY_INTEGER ;
5 BEGIN
6 FOR i IN ( SELECT a, b, c
7 FROM test
8 WHERE rownum <= 1000
9 ORDER BY a ASC, b DESC, c ASC ) LOOP
10
11 l_sofar := l_sofar + 1 ;
12
13 DBMS_APPLICATION_INFO.SET_SESSION_LONGOPS
14 (rindex => l_rindex ,
15 slno => l_slno ,
16 op_name => 'TEST LEO' ,
17 target => NULL ,
18 context => 0 ,
19 sofar => l_sofar ,
20 totalwork => 1000 ,
21 target_desc => 'Completado') ;
22
23 END LOOP ;
24 END ;
25 /

PL/SQL procedure successfully completed.

Ese bloque PL/SQL anónimo, iteró 1000 veces y logueo la operación en la vista.
Si ahora queremos ver esa información, lo único que tenemos que hacer es consultar la V$SESSION_LONGOPS.

SQL_9iR2> SELECT sid, serial#, username, opname, sql_hash_value hash_value,
2 TO_CHAR(start_time,'HH24:MI:SS') "INICIO",
3 (sofar/totalwork)*100 "%"
4 FROM v$session_longops ;

SID SERIAL# USERNAME OPNAME HASH_VALUE INICIO %
---------- ---------- ---------- --------------- ---------- -------- ----------
58 19110 LEO TEST LEO 2061745150 23:13:00 100

Como pueden ver, este procedimiento es muy útil cuando queremos, por ejemplo, monitorear una consulta larga y poder identificarla en la vista de manera fácil como en éste último ejemplo.

viernes, 7 de septiembre de 2007

Búsquedas Case-Insensitive

Los tipos de búsquedas Case-Insensitive (nueva funcionalidad en Oracle 10gR2) nos permiten tratar por igual las letras mayúsculas y las letras minúsculas cuando realizamos una búsqueda.

Veamos...

SQL_10gR2> SELECT * FROM test WHERE texto = 'oracle' ;

TEXTO
----------
Oracle
ORACLE
oraCLE
oracle

4 rows selected.

Fijense que cuando busco las palabra 'oracle' en minúsculas, gracias a ésta nueva funcionalidad, la consulta me devuelve todas las palabras 'oracle' que encuentra.
Pero cómo logramos realizar ésto? Necesitamos setear algún parámetro?

Realicemos el ejemplo completo:

SQL_10gR2> CREATE TABLE test ( texto VARCHAR2(10) ) ;

Table created.

SQL_10gR2> INSERT INTO test VALUES ( 'Oracle' ) ;

1 row created.

SQL_10gR2> INSERT INTO test VALUES ( 'ORACLE' ) ;

1 row created.

SQL_10gR2> INSERT INTO test VALUES ( 'oraCLE' ) ;

1 row created.

SQL_10gR2> INSERT INTO test VALUES ( 'oracle' ) ;

1 row created.

Ya tenemos nuestra tabla creada con sus respectivos datos cargados.
Ahora vamos a prestar atención a 2 parámetros: NLS_COMP y NLS_SORT.

En versiones anteriores a la 10gR2, si teníamos la tabla TEST (la tabla de nuestro ejemplo) con estos mismos datos cargados y buscabamos la palabra 'oracle' (con el parámetro NLS_SORT = BINARY que es el valor por default), veríamos lo siguiente:

SQL_10gR2> SELECT * FROM test WHERE texto = 'oracle' ;

TEXTO
----------
oracle

1 row selected.

Qué sucede si nuestra aplicación necesita realizar búsquedas que devuelvan tanto los valores que se encontraron en mayúsculas como los valores encontrados en minúsculas?

SQL_10gR2> ALTER SESSION SET NLS_COMP = ANSI ;

Session altered.

SQL_10gR2> ALTER SESSION SET NLS_SORT = BINARY_CI ;

Session altered.

SQL_10gR2> SELECT * FROM test WHERE texto = 'oracle' ;

TEXTO
----------
Oracle
ORACLE
oraCLE
oracle

4 rows selected.

Al setear el parámetro NLS_SORT = BINARY_CI, Oracle convierte nuestras búsquedas en case-insensitive.

Pero qué sucede si nuestra aplicación necesita hacer búsquedas del tipo LIKE '%ora%' porque no conoce la palabra completa que quiere buscar?

SQL_10gR2> SELECT * FROM test WHERE texto LIKE '%ora%' ;

TEXTO
----------
oraCLE
oracle

2 rows selected.

Al parecer con los parámetros que seteamos anteriormente no fue suficiente para satisfacer el resultado de ésta consulta.
Bien, es hora de setear correctamente el parámetro NLS_COMP. Si lo seteamos con el valor LINGUISTIC, le dice a Oracle que cada sort y comparación, use el orden lingüístico del parámetro NLS_SORT. Esto quiere decir, que si seteamos NLS_SORT = BINARY_CI, todos los sorts y comparaciones van a ser case-insensitive en esa sesión.

SQL_10gR2> ALTER SESSION SET NLS_COMP = LINGUISTIC ;

Session altered.

SQL_10gR2> ALTER SESSION SET NLS_SORT = BINARY_CI ;

Session altered.

SQL_10gR2> SELECT * FROM test WHERE texto LIKE '%ora%' ;

TEXTO
----------
Oracle
ORACLE
oraCLE
oracle

4 rows selected.

SQL_10gR2> SELECT * FROM test WHERE texto = 'oracle' ;

TEXTO
----------
Oracle
ORACLE
oraCLE
oracle

4 rows selected.

Fijense, que éste último seteo de parámetros me sirvió para satisfacer las 2 consultas que estuvimos realizando!

Veamos el explain plan de ésta última consulta:

SQL_10gR2> explain plan for
2 SELECT * FROM test WHERE texto = 'oracle' ;

Explained.

SQL_10gR2> @explains
Plan hash value: 1357081020

--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 7 | 3 (0)| 00:00:01 |
|* 1 | TABLE ACCESS FULL| TEST | 1 | 7 | 3 (0)| 00:00:01 |
--------------------------------------------------------------------------

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

1 - filter(NLSSORT("TEXTO",'nls_sort=''BINARY_CI''')=HEXTORAW('6F7261636C6500') )

Note
-----
- dynamic sampling used for this statement
- star transformation used for this statement

Vemos que el filter del explain plan nos muestra que se está utilizando case-insensitive.
Bien, ahora vamos a crear un índice con el contenido del filter que acabamos de ver:

SQL_10gR2> CREATE INDEX test_texto_idx ON test (NLSSORT(texto,'NLS_SORT=BINARY_CI')) ;

Index created.

Ejecutamos nuevamente la consulta:

SQL_10gR2> explain plan for
2 SELECT * FROM test WHERE texto = 'oracle' ;

Explained.

SQL_10gR2> @explains
Plan hash value: 2134909805

----------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 4 | 28 | 2 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID| TEST | 4 | 28 | 2 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | TEST_TEXTO_IDX | 4 | | 1 (0)| 00:00:01 |
----------------------------------------------------------------------------------------------

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

2 - access(NLSSORT("TEXTO",'nls_sort=''BINARY_CI''')=HEXTORAW('6F7261636C6500') )

Note
-----
- dynamic sampling used for this statement
- star transformation used for this statement

jueves, 6 de septiembre de 2007

ANYDATA

En Oracle 9i se agregó un nuevo tipo de dato: ANYDATA. Normalmente, creamos un campo DATE para guardar fechas, un campo VARCHAR2 para guardar texto, un campo NUMBER para guardar números... pero qué sucede si mi aplicación guarda datos genéricos? Es decir, qué sucede si mi aplicación guarda en un mismo campo valores de distinto tipo de dato? En éste tipo de casos nos convendría usar el tipo de dato ANYDATA.

Veamos un ejemplo:

SQL_9iR2> CREATE TABLE test ( algo sys.anyData ) ;

Table created.

SQL_9iR2> INSERT INTO test VALUES ( sys.anyData.ConvertVarchar2('esto es
un ejemplo') ) ;

1 row created.

SQL_9iR2> INSERT INTO test VALUES ( sys.anyData.ConvertDate(sysdate) ) ;

1 row created.

SQL_9iR2> INSERT INTO test VALUES ( sys.anyData.ConvertNumber(1000) ) ;

1 row created.

Si consultamos la tabla (como lo hariamos de forma tradicional), no vemos los datos... simplemente vemos el tipo ANYDATA...

SQL_9iR2> SELECT *
2 FROM test ;

ALGO()
-----------------
ANYDATA()
ANYDATA()
ANYDATA()

3 rows selected.

SQL_9iR2> DESC test
Name Null? Type
------------------------- -------- --------------
ALGO SYS.ANYDATA

Antes de mostrarles cómo ver los datos reales de la tabla... Veamos los tipos de cada dato...

SQL_9iR2> SELECT anyData.gettypeName(algo) TipoDeDato
2 FROM test ;

TIPODEDATO
-----------------
SYS.VARCHAR2
SYS.DATE
SYS.NUMBER

3 rows selected.

Lamentablemente, no es tan fácil ver los datos de la tabla. Pero porqué no es fácil? Porque está pensando para ser utilizado dentro de un procedimiento en donde se hace el fetch de los datos y luego se los convierte y procesa. Entonces veamos... Para ver los datos reales de la tabla, vamos a construir un package con 3 funciones. Cada una de esas funciones se va a encargar de convertir un tipo de dato y devolvernos el valor de la conversión:

SQL_9iR2> CREATE OR REPLACE PACKAGE pkg_convertir_anydata AS
2
3 FUNCTION convertir_varchar2 ( p_dato IN sys.anydata ) RETURN VARCHAR2 ;
4 FUNCTION convertir_date ( p_dato IN sys.anydata ) RETURN DATE ;
5 FUNCTION convertir_number ( p_dato IN sys.anydata ) RETURN NUMBER ;
6
7 END pkg_convertir_anydata ;
8 /

Package created.

SQL_9iR2> CREATE OR REPLACE PACKAGE BODY pkg_convertir_anydata AS
2
3 FUNCTION convertir_varchar2 ( p_dato IN sys.anydata ) RETURN VARCHAR2 IS
4 nada NUMBER ;
5 l_varchar2 VARCHAR2(4000) ;
6 BEGIN
7 nada := p_dato.GetVarchar2( l_varchar2 ) ;
8 RETURN ( l_varchar2 ) ;
9 END ;
10
11 FUNCTION convertir_date ( p_dato IN sys.anydata ) RETURN DATE IS
12 nada NUMBER ;
13 l_date DATE ;
14 BEGIN
15 nada := p_dato.GetDate( l_date ) ;
16 RETURN ( l_date ) ;
17 END ;
18
19 FUNCTION convertir_number ( p_dato IN sys.anydata ) RETURN NUMBER IS
20 nada NUMBER ;
21 l_number NUMBER ;
22 BEGIN
23 nada := p_dato.GetNumber ( l_number ) ;
24 RETURN ( l_number ) ;
25 END ;
26
27 END pkg_convertir_anydata ;
28 /

Package body created.

Lo único que nos resta por hacer es construir la consulta para convertir y mostrar nuestros datos:

SQL_9iR2> WITH Datos AS
2 ( SELECT algo , sys.anydata.gettypename(algo) TipoDeDato FROM test )
3 SELECT CASE
4 WHEN TipoDeDato = 'SYS.VARCHAR2' THEN
5 TO_CHAR(pkg_convertir_anydata.convertir_Varchar2(algo))
6 WHEN TipoDeDato = 'SYS.DATE' THEN
7 TO_CHAR(pkg_convertir_anydata.convertir_Date(algo),'dd-mon-
rrrr hh24:mi:ss')
8 WHEN TipoDeDato = 'SYS.NUMBER' THEN
9 TO_CHAR(pkg_convertir_anydata.convertir_Number(algo))
10 END algo
11 FROM Datos ;

ALGO
--------------------------
esto es un ejemplo
06-sep-2007 21:49:37
1000


3 rows selected.

miércoles, 5 de septiembre de 2007

Foreign Keys no indexadas

Suelo encontrarme en los diseños de las aplicaciones, foreign keys que no se encuentran indexadas. No indexar las foreign keys puede ser un gran problema de performance.

Podemos encontrar 2 tipos de problemas:
1.- Se produce un loqueo de la tabla si modificamos (muy inusual) o eliminamos algún registro de la primary key (tabla padre) y las foreign keys (tabla hija) no se encuentran indexadas.
2.- Si tenemos la cláusula ON DELETE CASCADE y no tenemos índices creados en la foreign keys, si eliminamos algún registro de la primary key, como no tenemos indexadas las foreign keys, se producirá un full scan de la tabla que contiene las foreign keys. Esto puede ser el factor de un grave problema de performance ya que si eliminamos varios registros de la tabla padre, se realizará un full scan de la tabla hija por cada registro eliminado de la tabla padre.

Veamos un ejemplo:

SQL_9iR2> CREATE TABLE tabla_padre AS
2 SELECT level id
3 FROM dual
4 CONNECT BY level <= 9 ;

Table created.

SQL_9iR2> CREATE TABLE tabla_hija AS
2 SELECT decode(mod(level,9),0,9,mod(level,9)) id , 'detalle_'||level detalle
3 FROM dual
4 CONNECT BY level <= 1000000 ;

Table created.

SQL_9iR2> ALTER TABLE tabla_padre
2 ADD CONSTRAINT tabla_padre_pk
3 PRIMARY KEY (id) ;

Table altered.

SQL_9iR2> ALTER TABLE tabla_hija
2 ADD CONSTRAINT tabla_padre_hija_fk
3 FOREIGN KEY (id) REFERENCES tabla_padre(id)
4 ON DELETE CASCADE ;

Table altered.

SQL_9iR2> EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'TABLA_PADRE', cascade => true) ;

PL/SQL procedure successfully completed.

SQL_9iR2> EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'TABLA_HIJA', cascade => true) ;

PL/SQL procedure successfully completed.

Bien, ya tenemos nuestras tablas de ejemplo con sus respectivos datos, constraints y estadísticas.

Ejecutamos un insert en la tabla hija:

SQL_9iR2> INSERT INTO tabla_hija VALUES(5,'detalle'||5) ;

1 row created.

Ahora ejecutamos un delete en la tabla padre:

SQL_9iR2> DECLARE
2 PRAGMA AUTONOMOUS_TRANSACTION ;
3 BEGIN
4 DELETE FROM tabla_padre
5 WHERE id = 2 ;
6
7 COMMIT ;
8 END ;
9 /
DECLARE
*
ERROR at line 1:
ORA-00060: deadlock detected while waiting for resource
ORA-06512: at line 4

Como podemos ver, al no tener un índice en la foreign key se produce un Deadlock.
Veamos qué sucede si creamos un índice sobre la foreign key y repetimos el procedimiento.

SQL_9iR2> CREATE INDEX tabla_hija_id_idx ON tabla_hija(id) ;

Index created.

SQL_9iR2> EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'TABLA_HIJA', cascade => true) ;

PL/SQL procedure successfully completed.

SQL_9iR2> INSERT INTO tabla_hija VALUES(5,'detalle'||5) ;

1 row created.

SQL_9iR2> DECLARE
2 PRAGMA AUTONOMOUS_TRANSACTION ;
3 BEGIN
4 DELETE FROM tabla_padre
5 WHERE id = 2 ;
6
7 COMMIT ;
8 END ;
9 /

PL/SQL procedure successfully completed.

Mi consejo es el siguiente: Si realizamos deletes en la tabla padre y/o updates en la primary key de la tabla padre, entonces debemos crear índices en las foreign keys de la tabla hija.

Deadlocks --> ORA-00060

Qué es un Deadlock? Un Deadlock (abrazo mortal) es cuando 2 o más usuarios están esperando algún dato que está siendo loqueado por alguna sesión. Si ésto sucede, los usuarios involucrados en el Deadlock deben esperar y no pueden continuar con el procesamiento.

Cuando Oracle detecta que se produjo un Deadlock, lo que hace es cortar la ejecución del procedimiento y mostrar el siguiente mensaje de error: ORA-00060: deadlock detected while waiting for resource. Tengamos en cuenta que cuando se produce éste error, Oracle genera un archivo de trace en el directorio UDUMP con información acerca del error.

Generalmente éste problema se produce por un mal diseño de la aplicación. Veamos un ejemplo...

SQL_9iR2> CREATE TABLE sesion_1 AS
2 SELECT level id, 'nom_'||level nombre
3 FROM dual
4 CONNECT BY level <= 10 ;

Table created.

SQL_9iR2> CREATE TABLE sesion_2 AS
2 SELECT level id, 'nom_'||level nombre
3 FROM dual
4 CONNECT BY level <= 10 ;

Table created.

En la SESION 1 loqueo un registro de la tabla SESION_1 correspondiente al ID 1.

SQL_9iR2> UPDATE sesion_1
2 SET nombre = 'nom_'||id*2
3 WHERE id = 1 ;

1 row updated.

En la SESION 2 loqueo un registro de la tabla SESION_2 correspondiente al ID 1.

SQL_9iR2> UPDATE sesion_2
2 SET id = id+10
3 WHERE id = 1 ;

1 row updated.

En la SESION 1 modifico un registro de la tabla SESION_2 correspondiente al ID 1. Vemos que esta sesión se 'colgó' debido al loqueo y no nos devuelve el control. Todavía no se produjo el Deadlock... sólo se produjo un loqueo.

SQL_9iR2> UPDATE sesion_2
2 SET id = id+10
3 WHERE id = 1 ;

En la SESION 2 modifico un registro de la tabla SESION_1 correspondiente al ID 1.
Vemos que esta sesión se 'colgó' debido al loqueo y no nos devuelve el control.

SQL_9iR2> UPDATE sesion_1
2 SET nombre = 'nom_'||id*2
3 WHERE id = 1 ;

Esto va a producir un Deadlock y luego de unos segundos aparece un mensaje de error en la SESION 1:

SQL_9iR2> UPDATE sesion_2
2 SET id = id+10
3 WHERE id = 1 ;
UPDATE sesion_2
*
ERROR at line 1:
ORA-00060: deadlock detected while waiting for resource

La SESION 2 sigue colgada esperando que la SESION 1 termine la transacción que comenzó. Entonces en la SESION 1 ejecutamos...

SQL_9iR2> rollback ;

Rollback complete.

En la SESION 2 se libera automáticamente el loqueo...

SQL_9iR2> UPDATE sesion_1
2 SET nombre = 'nom_'||id*2
3 WHERE id = 1 ;

1 row updated.

martes, 4 de septiembre de 2007

Estadísticas en paralelo

Cuando necesitamos analizar objetos de muy grandes dimensiones, podemos obtener estadísticas en paralelo con el paquete DBMS_STATS (por esto, y por muchas cosas más, es el porque Oracle recomienda en la documentación utilizar éste paquete para obtener estadísticas en vez del comando ANALYZE).

Veamos un ejemplo:

SQL_9iR2> CREATE TABLE t
2 NOLOGGING
3 AS
4 SELECT level a, mod(level,3) b, level c
5 FROM dual
6 CONNECT BY level <= 10000000 ;

Table created.

Elapsed: 00:01:00.08

SQL_9iR2> CREATE UNIQUE INDEX t_abc_ASC_uq ON t ( a ASC, b ASC, c ASC ) NOLOGGING ;

Index created.

Elapsed: 00:00:31.05

SQL_9iR2> CREATE UNIQUE INDEX t_abc_DESC_uq ON t ( a DESC, b DESC, c DESC ) NOLOGGING ;

Index created.

SQL_9iR2> EXEC dbms_stats.gather_table_stats( ownname => USER,
tabname => 'T', cascade => TRUE ) ;

PL/SQL procedure successfully completed.

Elapsed: 00:02:59.03

Vemos que si obtenemos las estadísticas computadas en serial, tardamos 3 minutos. Veamos que sucede si realizamos lo mismo pero en paralelo...

SQL_9iR2> EXEC dbms_stats.gather_table_stats( ownname => USER,
tabname => 'T', cascade => TRUE , degree => 4 ) ;

Mientras se ejecuta éste procedimiento, podemos consultar la vista V$PX_PROCESS (en otra sesión) para ver si efectivamente estamos realizando una ejecución en paralelo...

SQL_9iR2> select * from v$px_process ;

SERV STATUS PID SPID SID SERIAL#
---- --------- ---------- ------------ ---------- ----------
P000 IN USE 48 19360 74 1093
P001 IN USE 60 19362 64 773
P002 IN USE 62 19364 12 4904

P003 AVAILABLE 70 19366
P004 IN USE 74 19368 86 447

5 rows selected.

Observamos que hay 4 procesos que se están utilizando (IN USE) para obtener las estadísticas en paralelo. Veamos que sucede cuando termina de ejecutarse el procedimiento y volvemos a consultar la vista.

PL/SQL procedure successfully completed.

Elapsed: 00:01:21.04

SQL_9iR2> select * from v$px_process ;

SERV STATUS PID SPID SID SERIAL#
---- --------- ---------- ------------ ---------- ----------
P000 AVAILABLE 48 19360
P001 AVAILABLE 60 19362
P002 AVAILABLE 62 19364
P003 AVAILABLE 70 19366
P004 AVAILABLE 74 19368

5 rows selected.

Los procesos que estaban en estado IN USE pasaron a estar AVAILABLE nuevamente y obtuvimos las estadísticas en 1 minuto 20 segundos!!!

lunes, 3 de septiembre de 2007

Explain Plan vs. Autotrace Explain Plan

Hace unos meses me hicieron una pregunta que hoy en día veo que muchas personas no saben la respuesta. La pregunta era: ¿ Cuál es la diferencia de ejecutar el Explain Plan en SQL*Plus de ejecutar el Explain Plan desde la herramienta Autotrace ? Bueno, para contestar esta pregunta vamos a crear un muy sencillo ejemplo:

SQL_9iR2> CREATE TABLE estudiantes
2 (
3 legajo NUMBER(5),
4 nombre VARCHAR2(30),
5 ingreso DATE
6 )
7 PARTITION BY RANGE(ingreso)
8 (
9 PARTITION ingreso_2006 VALUES LESS THAN(TO_DATE('01/01/2007','DD/MM/YYYY'))
,
10 PARTITION ingreso_2007 VALUES LESS THAN(TO_DATE('01/01/2008','DD/MM/YYYY'))

11 ) ;

Table created.

SQL_9iR2> INSERT INTO estudiantes
2 SELECT level, 'nom_'||level, DECODE(MOD(level,2),0,sysdate,1,sysdate-500)
3 FROM dual
4 CONNECT BY level <= 1000 ;

1000 rows created.

SQL_9iR2> EXEC dbms_stats.GATHER_TABLE_STATS(user,'ESTUDIANTES') ;

PL/SQL procedure successfully completed.

Ya creamos nuestro ambiente de prueba. Veamos que sucede si vemos el Explain Plan desde el Autotrace:

SQL_9iR2> SET AUTOTRACE TRACEONLY EXPLAIN

SQL_9iR2> SELECT legajo, nombre
2 FROM estudiantes
3 WHERE ingreso = to_date('03/09/2007 12:11:27','dd/mm/yyyy hh24:mi:ss') ;

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=2 Card=500 Bytes=9500)
1 0 TABLE ACCESS (FULL) OF 'ESTUDIANTES' (Cost=2 Card=500 Bytes=9500)

Vemos que estamos haciendo un full scan de la tabla, pero si observamos bien, podemos notar que en realidad no estamos leyendo la tabla entera (que tiene 1.000 registros), sino que sólo estamos leyendo 500 registros. Esto nos esta indicando que no estamos leyendo las 2 particiones que creamos, sólo estamos leyendo 1 de ellas... pero cuál?

Veamos que sucede si ejecutamos el Explain Plan:

SQL_9iR2> EXPLAIN PLAN FOR
2 SELECT legajo, nombre
3 FROM estudiantes
4 WHERE ingreso = to_date('03/09/2007 12:11:27','dd/mm/yyyy hh24:mi:ss') ;

Explained.

SQL_9iR2> @explains

-------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost | Pstart| Pstop |
-------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 500 | 9500 | 2 | | |
|* 1 | TABLE ACCESS FULL | ESTUDIANTES | 500 | 9500 | 2 | 2 | 2 |
-------------------------------------------------------------------------------------

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

1 - filter("ESTUDIANTES"."INGRESO"=TO_DATE(' 2007-09-03 12:11:27', 'syyyy-mm-ddhh24:mi:ss'))

Claramente vemos que en el Explain Plan tenemos 2 columnas nuevas: Pstart y Pstop. Estas columnas nos están indicando que sólo estamos haciendo un full scan de la partición 2, no de la tabla completa.

Moraleja: Utilicen el Explain Plan cuando estén interesados en el plan de ejecución de la consulta. Utilicen el Autotrace cuando estén interesados en las estadísticas de la consulta.

viernes, 31 de agosto de 2007

Estado de los cursores

A menudo veo que los desarrolladores y DBA's consultan la vista V$OPEN_CURSOR para identificar todos los cursores que se encuentran abiertos en la sesión. Pero esta vista muestra realmente los cursores actualmente abiertos en la sesión? La respuesta es NO.

Veamos un ejemplo:

Consultamos la vista V$OPEN_CURSOR:

SQL_9iR2> SELECT count(*)
2 FROM v$open_cursor ;

COUNT(*)
----------
169

1 row selected.

La vista nos muestra que tenemos 169 cursores abiertos. Pero en realidad lo que nos está mostrando es la cantidad de cursores actualmente abiertos en la sesión y los cursores que se encuentran actualmente cerrados (pero abiertos). Estos cursores que se encuentran cerrados pero cacheados, son los cursores que Oracle mantiene silenciosamente abiertos con la esperanza de que los volvamos a utilizar en la sesión.
Pero porque Oracle cachea los cursores? Principalmente para reducir la cantidad de parseos. Este cacheo de cursores es muy importante en aplicaciones como Oracle Forms, que suelen cerrar todos los cursores abiertos por el form principal cuando se cambia de un form a otro. Entonces, cacheando los cursores reducimos en gran medida los parseos que se realizan. Por otro lado, como reducimos la cantidad de parseos, reducimos también los loqueos (latches) que se realizan el la Library Cache y en la Shared Pool.

Suelo crear en mis bases de datos la vista MY_STATS para que los desarrolladores puedan consultar fácilmente las estadísticas de sus respectivas sesiones:

SQL_9iR2> CREATE VIEW my_stats AS
2 SELECT s.*, n.name
3 FROM v$mystat s, v$statname n
4 WHERE s.statistic# = n.statistic# ;

View created.

Veamos que suecede si consultamos el valor del parámetro 'opened cursors current' de la vista MY_STATS:

SQL_9iR2> SELECT *
2 FROM my_stats
3 WHERE name = 'opened cursors current' ;

SID STATISTIC# VALUE NAME
---------- ---------- ---------- -----------------------------------------------
43 3 4 opened cursors current

1 row selected.

Vemos que nos devuelve el valor 4. Esa es la cantidad real de cursores actualmente abiertos en nuestra sesión.

jueves, 30 de agosto de 2007

Index-Skip Scans

El "Index-Skip Scans" está disponible a partir de Oracle 9i. Cuando tenemos un índice compuesto, permite en algunas circunstancias, que el CBO no tome en cuenta la primer columna del índice, sino que lea las restantes. Esto es útil cuando en una consulta hacemos referencia a alguna de las columnas que no están en la cabecera del índice pero que sin embargo queremos utilizar ese índice. Obviamente, el CBO utiliza Index-Skip Scans solamente cuando se cumplen 2 condiciones:

1- La columna cabecera del índice debe contener muy pocos valores distintos. Osea, tiene que ser una columna no selectiva. Tipicamente son las columnas apropiadas para la utilización de un Bitmap Index.
2- En la consulta debemos hacer referencia, por lo menos, a alguna de las columnas restantes del índice.

Supongamos que tenemos 3 consultas:

SELECT d FROM test WHERE a = :a ;

SELECT d FROM test WHERE a = :a AND b = :b ;

SELECT d FROM test WHERE a = :a AND b = :b AND c = :c ;

Qué índice nos conviene crear para que sea utilizado en las 3 consultas?
Claramente crearíamos un índice B*Tree compuesto por las columnas A,B,C.

Qué sucede si tenemos ésta consulta?

SELECT d FROM test WHERE b = :b AND c = :c ;

El índice es utilizado?

Veamos un ejemplo:

SQL_9iR2> CREATE TABLE test AS
2 SELECT mod(level,3) a, level b, level c, 'nom_'||level d
3 FROM dual
4 CONNECT BY level <= 20000 ;

Table created.

SQL_9iR2> CREATE INDEX t_abc_idx ON test(a,b,c) ;

Index created.

SQL_9iR2> EXEC DBMS_STATS.GATHER_TABLE_STATS(USER,'TEST',cascade=>true) ;

PL/SQL procedure successfully completed.

Ya tenemos nuestra tabla creada y un índice por las columnas A,B,C. Observen que inserté en la columna A sólo 3 valores distintos (0,1 y 2).

Bien, veamos el explain plan de la consulta que vimos anteriormente:

SQL_9iR2> explain plan for
2 SELECT d
3 FROM test
4 WHERE a = 2 AND b = 182 AND c = 182 ;

Explained.

SQL_9iR2> @explains

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 21 | 2 |
| 1 | TABLE ACCESS BY INDEX ROWID| TEST | 1 | 21 | 2 |
|* 2 | INDEX RANGE SCAN | T_ABC_IDX | 1 | | 1 |
---------------------------------------------------------------------------

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

2 - access("TEST"."A"=2 AND "TEST"."B"=182 AND "TEST"."C"=182)

Como podemes ver, el índice es utilizado porque estamos accediendo a las 3 columnas del índice. Que sucede si accedemos sólo a las columnas B y C ?

SQL_9iR2> explain plan for
2 SELECT d
3 FROM test
4 WHERE b = 182 AND c = 182 ;

Explained.

SQL_9iR2> @explains

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 20 | 5 |
| 1 | TABLE ACCESS BY INDEX ROWID| TEST | 1 | 20 | 5 |
|* 2 | INDEX SKIP SCAN | T_ABC_IDX | 1 | | 4 |
---------------------------------------------------------------------------

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

2 - access("TEST"."B"=182 AND "TEST"."C"=182)
filter("TEST"."B"=182 AND "TEST"."C"=182)

Observamos que el índice es utilizado porque cumplimos con las 2 condiciones necesarios para que el CBO utilice Index-Skip Scans.

La utilización de ésta clase de acceso es más costosa que realizar un accedo directo a través del índice, pero en general es menos costosa que utilizar un full-scan.

martes, 28 de agosto de 2007

APPEND hint

El hint APPEND permite que Oracle comience a escribir bloques nuevos luego de la HWM (high water mark) de la tabla.

Veo a menudo a desarrolladores y DBA's usar este hint, y en la mayoría de los casos, el hint no es usado por Oracle. Pero porque?

Veamos un ejemplo:

SQL_9iR2> CREATE TABLE test ( a number, b number ) ;

Table created.

SQL_9iR2> SET AUTOTRACE TRACEONLY STATISTICS

SQL_9iR2> INSERT INTO test
2 SELECT level, level
3 FROM dual
4 CONNECT BY level <= 1000000 ;

1000000 rows created.

Statistics
---------------------------------------------------
9 recursive calls
10713 db block gets
4128 consistent gets
2 physical reads
20461776 redo size

626 bytes sent via SQL*Net to client
835 bytes received via SQL*Net from client
4 SQL*Net roundtrips to/from client
2 sorts (memory)
0 sorts (disk)

1000000 rows processed

Como podemos ver, se está generando aprox. 19 MB de Redo para insertar en la tabla 1.000.000 de registros. Qué sucedería si necesitamos realizar una carga masiva de datos? Estaríamos consumiendo gran cantidad de Redo.
Para reducir la cantidad de Redo que se genera, podemos usar el hint APPEND.

SQL_9iR2> INSERT /*+ APPEND */ INTO test
2 SELECT level, level
3 FROM dual
4 CONNECT BY level <= 1000000 ;

1000000 rows created.

Statistics
---------------------------------------------------
688 recursive calls
242 db block gets
242 consistent gets
0 physical reads
17120808 redo size

612 bytes sent via SQL*Net to client
849 bytes received via SQL*Net from client
4 SQL*Net roundtrips to/from client
2 sorts (memory)
0 sorts (disk)

1000000 rows processed

Con el hint APPEND estamos utilizando aprox. 16 MB de Redo. Pero realmente se está utilizando el hint? No hubo una gran diferencia en la generación del Redo. Porque? Esto es debido a que existen 2 condiciones para que se utilice el hint, y debemos cumplir alguna de ellas:

- La base de datos debe estar en modo NOARCHIVELOG.
- La tabla que estamos utilizando debe estar en modo NOLOGGING.

SQL_9iR2> SELECT log_mode from v$database ;

LOG_MODE
------------
ARCHIVELOG

1 row selected.

En la base de datos que estoy utilizando para éste ejemplo estoy utilizando ARCHIVELOG, por lo cual vamos a colocar la tabla en modo NOLOGGING para ver el efecto de utilizar el hint.

SQL_9iR2> ALTER TABLE test NOLOGGING ;

Table altered.

SQL_9iR2> INSERT /*+ APPEND */ INTO test
2 SELECT level, level
3 FROM dual
4 CONNECT BY level <= 1000000 ;

1000000 rows created.

Statistics
---------------------------------------------------
153 recursive calls
18 db block gets
23 consistent gets
1 physical reads
5944 redo size

614 bytes sent via SQL*Net to client
849 bytes received via SQL*Net from client
4 SQL*Net roundtrips to/from client
7 sorts (memory)
0 sorts (disk)

1000000 rows processed

Claramente podemos ver que ahora estamos utilizando sólo 5 KB de Redo. Esto nos permite optimizar las operaciones de carga masiva de datos.

NOTA: El APPEND sólo podemos utilizarlo en sentencias del tipo INSERT AS SELECT.

jueves, 23 de agosto de 2007

La Clave del Tuning (CDT)

Cuando comencé con Tuning, ya hace unos años, hubo una pregunta a la cual no le encontraba respuesta. La pregunta era: Cuál es la clave del Tuning?
A medida que iba pasando el tiempo, hubo una sola respuesta que me vino a la cabeza. Los días seguían pasando y a medida que iba adquiriendo experiencia en el Tuning, esa respuesta cada vez me iba convenciendo más... hasta que un día terminó por convencerme.
La respuesta a mi pregunta era muy fácil y podemos resumirla en una simple fórmula:


El tiempo de respuesta es inversamente proporcional al tiempo de pensar. Veamos... El tiempo de respuesta es el tiempo total que demanda cierta ejecución (ej: una consulta). El tiempo de pensar es el tiempo total que dedico para la resolución de cierto problema (ese tiempo total incluye el análisis del problema, la generación de sus posibles soluciones, las pruebas de esas soluciones y la elección de la solución más performante).

La fórmula se resume en lo siguiente: Si le dedicamos a cada problema el "tiempo de pensar" que se merece... estaremos más cerca de lograr un "tiempo de respuesta" más óptimo.

Recuerden: Cada problema es un mundo.

miércoles, 1 de agosto de 2007

El hermoso Teorema de Pitágoras

Pueden ver AQUI una de las demostraciones más hermosas que jamas vi del Teorema de Pitágoras. Lean el artículo... seguramente les fascinará como a mi.

viernes, 27 de julio de 2007

WHEN OTHERS THEN NULL ;

Veo muchas veces a los desarrolladores poniendo en sus códigos...


EXCEPTION
WHEN OTHERS THEN
NULL ;



Las programadores que hacen ésto no estan concientes de sus graves consecuencias.
Un desarrollador JAMAS debe poner código de ese tipo. Porque? Por la simple razón que poniendolo, es como si jamas lo hubieramos puesto. Se entiende? No tiene ningún sentido ponerlo porque no cumple ninguna función. Ahh! Si! Cumple con la función de que si tenemos un error durante la ejecución de nuestro código... jamas nos enteramos; y si nos enteramos de que hubo un error es demasiado difícil encontrarlo.

Veamos un muy sencillo ejemplo:


SQL_9iR2> CREATE OR REPLACE PROCEDURE pr_log_errores
2 (
3 p_cod_error IN VARCHAR2 ,
4 p_msj_error IN VARCHAR2
5 )
6 IS
7 BEGIN
8 INSERT INTO log_errores
9 (
10 fecha ,
11 cod_error,
12 msj_error
13 )
14 VALUES
15 (
16 SYSDATE ,
17 p_cod_error,
18 p_msj_error
19 ) ;
20 COMMIT ;
21 EXCEPTION
22 WHEN OTHERS THEN
23 NULL ;
24 END ;
25 /

Procedure created.

SQL_9iR2> BEGIN
2 pr_log_errores('ORA-00001','unique constraint violated.') ;
3 END ;
4 /

PL/SQL procedure successfully completed.


Como podemos ver, ejecutamos nuestro procedimiento de prueba y terminó perfectamente... pero, realmente terminó bien? Veamos...


SQL_9iR2> SELECT *
2 FROM log_errores ;

no rows selected


Aha! El procedimiento no insertó en la tabla de logueo el error que le pasamos como parámetro. Pero como puede ser esto posible?


SQL_9iR2> DESC log_errores
Name Null? Type
------------------ -------- -------------
FECHA DATE
COD_ERROR VARCHAR2(100)
MSJ_ERROR VARCHAR2(10)


El campo MSJ_ERROR tiene una longitud de 10 caracteres y nosotros queremos introducir una cadena más larga. Nosotros nunca nos enteramos de que hubo un error.
Imaginense si es un proceso Batch crítico que se ejecuta todos los días de forma automática durante la madrugada. Si ocurre algún error... nadie se podría dar cuenta que el proceso en algún punto falló.

Corregimos nuestro procedimiento...


SQL_9iR2> CREATE OR REPLACE PROCEDURE pr_log_errores
2 (
3 p_cod_error IN VARCHAR2 ,
4 p_msj_error IN VARCHAR2
5 )
6 IS
7 BEGIN
8 INSERT INTO log_errores
9 (
10 fecha ,
11 cod_error,
12 msj_error
13 )
14 VALUES
15 (
16 SYSDATE ,
17 p_cod_error,
18 p_msj_error
19 ) ;
20 COMMIT ;
21 EXCEPTION
22 WHEN OTHERS THEN
23 RAISE ;
24 END ;
25 /

Procedure created.

SQL_9iR2> BEGIN
2 pr_log_errores('ORA-00001','unique constraint violated.') ;
3 END ;
4 /
BEGIN
*
ERROR at line 1:
ORA-01401: inserted value too large for column
ORA-06512: at "IP_UTILS.PR_LOG_ERRORES", line 24
ORA-06512: at line 2

jueves, 26 de julio de 2007

Compulsive Tuning Disorder

Descubrí AQUI si tenés los síntomas del CTD...

Geeks...

Muy interesante ARTÍCULO que explica cómo tratar en un ambiente laboral a los Geeks y cuales son sus necesidades.
Darles trabajos desafiantes, importantes y significativos a ésta clase de personas es esencial.

Si sos un Geek o crees que lo sos y estas en un trabajo en el cual sentis que estas desperdiciando mucho de vos, quizás sea hora de pensar en un cambio...
The views expressed on this blog are my own and do not necessarily reflect the views of Oracle.