martes, 15 de diciembre de 2009

Como encontrar los "agujeros" (gaps) en las columnas que se llenan con valores de secuencias

Voy a mostrar un ejemplo de uso de las funciones analiticas "Analytical Functions" para mostrarles de que forma sencilla y elegante se pueden encontrar los gaps de valores en las columnas alimentadas por secuencias. Es sabido que el objeto secuencia no es transaccional, por lo tanto si una transaccion se descarta (rollback) el próximo valor de secuencia obtenido para la transacción se pierde. Tambien es común que las secuencias manejen un cache y por lo tanto ante una bajada de la base de datos tambien se pierden los valores cacheados que no habian sido consumidos. A veces se necesita saber cuales son los intervalos no cosecutivos por algún tema de negocio. Para hacer eso se podría usar un bloque procedureal con pl/sql, pero con funciones analiticas lo resolvemos de una manera mas simple y performante, veamos con un ejemplo desde cero como hacerlo:


rop@DESA11G> create table t (x int);

Tabla creada.

rop@DESA11G> create sequence t_seq;

Secuencia creada.



Una vez creada la tabla y la secuencia voy a llenar la tabla T con 1000 valores consecutivos:


rop@DESA11G> insert into t
2 select t_seq.nextval
3 from dual
4 connect by rownum <= 1000;
1000 filas creadas.



Ahora voy a eliminar filas para generar gaps y poder mostrar como funciona la senntencia para listar agujeros.


rop@DESA11G> delete from t
2 where (x between 100 and 150) or (x between 900 and 910);
62 filas suprimidas.

Ya tengo armado el ambiente asi que ahora simplemente ejecuto la sentencia
con FA para detectar los gaps:


rop@DESA11G> select *
2 from (select unique x,
3 lead(x) over (order by x) x_next
4 from t)
5 where x+1 != x_next;

X X_NEXT
---------- ----------
99 151
899 911

Se puede observar que se listaron los intervalos no cosecutivos que corresponden a las filas que se eliminaron en el ejemplo.

viernes, 11 de diciembre de 2009

Reporte para analizar planes que referencien a una tabla dada

Cada tanto me consultan desde el area de desarrollo sobre si agregando tal o cual indice a una tabla mejoraria el rendimiento de la aplicación. Como en muchas ocasiones yo no conozco el negocio me resulta dificil saber como se usa la tabla, es decir, que sentencias la referencian, si se hacen updates, deletes, inserts o solo se consulta. Para poder analizar esto a veces uso el script que copio abajo, que da el plan de ejecución y la ultima vez que se ejecutaron las sentencias que referencian a una cierta tabla. A esto se le puede sumar la generación de sugerencias usando advisor tales como SQL Tuning y SQL Access Advisors.


set serverout on
set line 120
set pagesize 9999
set verify off
set feed off

ACCEPT tabla PROMPT "Ingrese Tabla a Analizar: "
PROMPT
PROMPT

begin
for i in (select sp.sql_id,max(sh.begin_interval_time) ufecha
from dba_hist_sql_plan sp,
dba_hist_sqlstat ss,
dba_hist_snapshot sh
where sp.sql_id = ss.sql_id
and ss.snap_id = sh.snap_id
and sp.object_name = upper('&tabla')
group by sp.sql_id)
loop
dbms_output.put_line('Ultima Ejecución: '||to_char(i.ufecha,'DD/MM/YYYY HH24:MI'));
for j in (select * from table(dbms_xplan.display_awr(i.sql_id)))
loop
dbms_output.put_line (j.plan_table_output);
end loop;
dbms_output.put_line(chr(10)||rpad('*',100,'*')||chr(10));
end loop;
end;
/

set verify on
set feed on

miércoles, 9 de diciembre de 2009

SQL Profiles. Una ayuda adicional para que el optimizador elija el plan correcto.

Se puede recolectar estadísticas sobre tablas (nro de filas,tamaño promedio de fila,bloques, etc), sobre indíces (clustering factor, cantidad de hojas, altura,etc), sobre columnas (histogramas, densidad, valores distintos,etc) sobre el sistema (velocidad de cpu,tasa de transferencia a disco,etc) y tambien sobre la tablas de catálogo. Toda esta información permite generar el acceso a los datos mas adecuado (armar el plan correcto).De todas formas hay ciertos casos donde no es suficiente toda la estadistica recolectada como cuando se invocan funciones en la sentencias sql o cuando existe una fuerte correlación entre columnas que se filtran en un predicado.

A partir de 10g existen los SQL Profiles que corrigen estos problemas aportandole al optimizador mayor información, hay que pensarlo como estadisticas particulares sobre las sentencias. En notas anteriores mostré las caracteristicas de STORED_OUTLINES(8i) y de las SQL BASELINES (11g). En esta nota con SQL PROFILES(10g) , en orden no cronólogico, se completa la trilogia de mecanismos de corrección de planes que se han ido agregando versión tras versión. Este conjunto de herramientas se podrían pensar como mecanismos de hinteo encubierto que han ido evolucionando y sofisticandose con cada nuevo realease de base.

A diferencia de los STORED_OUTLINES que fijan un determinado plan a una sentencia, los SQL PROFILES son un set de hints que corrigen ciertos calculos internos del optimizador para que no se desvien los planes.
Para obtener los profiles se puede usar el paquete DBMS_SQLTUNE que crea un tarea y realiza una pseudo ejecución tomando métricas que le permiten conocer mayor detalle y analizar las correlaciones entre las columnas referenciadas en los predicados de la sentencia.

En el ejemplo que armé, cree una tabla TBL_AUTOS que podría representar datos de un concesionario o fabrica automotriz, con 3 campos: un identificación unica del auto (ID), una categoria o segmento: (A) Segmento Bajo, (B) Segmento Medio, (C)) Segmento Alto y (D) Gama Premium y como tercer campo el precio o valor del vehiculo en dolares, es una aproximación de los valores aqui en Argentina. Ahora voy a crear la tabla tratando de generar en forma ficticia una distribución cercana a una distribución real.


rop@ROP102> create table tbl_autos (id int,categoria char(1),valor int);


Tabla creada.


rop@ROP102> create sequence seq_autos;

Secuencia creada.


rop@ROP102> insert into tbl_autos
2 select seq_autos.nextval,
3 'A',
4 trunc(dbms_random.value(8000,12000))
5 from dual
6 connect by rownum <= 60000;

60000 filas creadas.

rop@ROP102> insert into tbl_autos
2 select seq_autos.nextval,
3 'B',
4 trunc(dbms_random.value(12000,15000))
5 from dual
6 connect by rownum <= 25000;

25000 filas creadas.

rop@ROP102> insert into tbl_autos
2 select seq_autos.nextval,
3 'C',
4 trunc(dbms_random.value(15000,30000))
5 from dual
6 connect by rownum <= 14500;

14500 filas creadas.

rop@ROP102> insert into tbl_autos
2 select seq_autos.nextval,
3 'D',
4 trunc(dbms_random.value(40000,80000))
5 from dual
6 connect by rownum <= 500 ;

500 filas creadas.

rop@ROP102> create index autos_idx on tbl_autos (categoria,valor);

Índice creado.


rop@ROP102> begin
2 dbms_stats.gather_table_stats(ownname => user,
tabname => 'TBL_AUTOS',
cascade =>true);
3 end;
4 /

Procedimiento PL/SQL terminado correctamente.

Con los pasos listado arriba tengo una tabla TBL_AUTOS, indexada por (categoria,valor) y con las estadísticas recolectadas.

Los vehiculos de categoria 'A' nunca superan los 12000 dólares entonces es de esperar que si realizo una consulta filtrando por categoria='A' y valor > 20000, el resultado debería ser 0, verdad?. Veamos que pasa.


rop@ROP102> explain plan for select min(id)
2 from tbl_autos
3 where categoria = 'A'
4 and valor > 20000;

Explicado.

rop@ROP102> select * from table(dbms_xplan.display);

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
Plan hash value: 649483413

--------------------------------------------------------------------------------
Id Operation Name Rows Bytes Cost (%CPU) Time
--------------------------------------------------------------------------------
0 SELECT STATEMENT 1 11 73 (7) 00:00:01
1 SORT AGGREGATE 1 11
* 2 TABLE ACCESS FULL TBL_AUTOS 20832 223K 73 (7) 00:00:01
--------------------------------------------------------------------------------

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

2 - filter("VALOR">20000 AND "CATEGORIA"='A')

14 filas seleccionadas.

Fijense que se estimaron 20832 filas, lo cual dista muchisimo de la realidad. Al calcular tan mal la cardinalidad el optimizador optó por un plan inadecuado, dictando recorrer toda la tabla para obtener el resultado (FULL SCAN), suena ilógico, no?. Esto se da por lo que comenté antes respecto a que el optimizador no cuenta con información de distribución correlacionada o multi-columna.

Voy a ejecutar el optimizador automático o SQL TUNING ADVISOR para ver como puedo evitar que se infiera un plan sub-optimo.

rop@ROP102> declare
2 l_task_id varchar2(20);
3 begin
4 l_task_id := dbms_sqltune.create_tuning_task (
5 sql_text => 'select min(id)
6 from tbl_autos
7 where categoria = ''A''
8 and valor > 20000',
9 user_name => 'ROP',
10 scope => 'COMPREHENSIVE',
11 time_limit => 120,
12 task_name => 'test'
13 );
14 dbms_sqltune.execute_tuning_task ('test');
15 end;
16 /

Procedimiento PL/SQL terminado correctamente.

rop@ROP102> set serveroutput on size 999999
rop@ROP102> set long 999999
rop@ROP102> select dbms_sqltune.report_tuning_task ('test') from dual;

DBMS_SQLTUNE.REPORT_TUNING_TASK('TEST')
--------------------------------------------------------------------------------
GENERAL INFORMATION SECTION
-------------------------------------------------------------------------------
Tuning Task Name : test
Tuning Task Owner : ROP
Workload Type : Single SQL Statement
Scope : COMPREHENSIVE
Time Limit(seconds): 120
Completion Status : COMPLETED
Started at : 12/07/2009 14:41:01
Completed at : 12/07/2009 14:41:01

-------------------------------------------------------------------------------
Schema Name: ROP
SQL ID : 7b5twtc2yrgsy
SQL Text : select min(id)
from tbl_autos
where categoria = 'A'
and valor > 20000

-------------------------------------------------------------------------------
FINDINGS SECTION (1 finding)
-------------------------------------------------------------------------------

1- SQL Profile Finding (see explain plans section below)
--------------------------------------------------------
Se ha encontrado un plan de ejecución potencialmente mejor para esta
sentencia.

Recommendation (estimated benefit: 99.19%)
------------------------------------------
- Puede aceptar el perfil SQL recomendado.
execute dbms_sqltune.accept_sql_profile(task_name => 'test', task_owner
=> 'ROP', replace => TRUE);

Validation results
------------------
Se ha probado SQL profile ejecutando su plan y el plan original y midiendo
sus respectivas estadísticas de ejecución. Puede que uno de los planes se
haya ejecutado sólo parcialmente si el otro se ha ejecutado por completo en
menos tiempo.r

Original Plan With SQL Profile % Improved
------------- ---------------- ----------
Completion Status: COMPLETE COMPLETE
Elapsed Time(ms): 19 0 100%
CPU Time(ms): 10 0 100%
User I/O Time(ms): 0 0
Buffer Gets: 248 2 99.19%
Disk Reads: 0 0
Direct Writes: 0 0
Rows Processed: 1 1
Fetches: 1 1
Executions: 1 1

-------------------------------------------------------------------------------
EXPLAIN PLANS SECTION
-------------------------------------------------------------------------------

1- Original With Adjusted Cost
------------------------------
Plan hash value: 649483413

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

Id Operation Name Rows Bytes Cost (%CPU) Time

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

0 SELECT STATEMENT 1 11 73 (7) 00:00:01

1 SORT AGGREGATE 1 11

* 2 TABLE ACCESS FULL TBL_AUTOS 1 11 73 (7) 00:00:01

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


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

2 - filter("VALOR">20000 AND "CATEGORIA"='A')

2- Original With Adjusted Cost
------------------------------
Plan hash value: 649483413

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

Id Operation Name Rows Bytes Cost (%CPU) Time

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

0 SELECT STATEMENT 1 11 73 (7) 00:00:01

1 SORT AGGREGATE 1 11

* 2 TABLE ACCESS FULL TBL_AUTOS 1 11 73 (7) 00:00:01

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


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

2 - filter("VALOR">20000 AND "CATEGORIA"='A')

3- Using SQL Profile
--------------------
Plan hash value: 552074332

--------------------------------------------------------------------------------
----------
Id Operation Name Rows Bytes Cost (%CPU)
Time
--------------------------------------------------------------------------------
----------
0 SELECT STATEMENT 1 11 3 (0)
00:00:01
1 SORT AGGREGATE 1 11

2 TABLE ACCESS BY INDEX ROWID TBL_AUTOS 1 11 3 (0)
00:00:01
* 3 INDEX RANGE SCAN AUTOS_IDX 1 2 (0)
00:00:01
--------------------------------------------------------------------------------
----------

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

3 - access("CATEGORIA"='A' AND "VALOR">20000 AND "VALOR" IS NOT NULL)

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

El analizador automático sugiere aplicar un sql profile y estima una mejora de casi el 100%. Apliquemos el profile y veamos como resuelve la sentencia con esa información adicional.

1 begin
2 dbms_sqltune.accept_sql_profile(task_name => 'test',
3 task_owner => 'ROP',
4 replace => TRUE);
5* end;
rop@ROP102> /

Procedimiento PL/SQL terminado correctamente.

rop@ROP102> explain plan for select min(id)
2 from tbl_autos
3 where categoria = 'A'
4 and valor > 20000;

Explicado.

rop@ROP102> select * from table(dbms_xplan.display);

PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------------------------------
Plan hash value: 552074332

------------------------------------------------------------------------------------------
Id Operation Name Rows Bytes Cost (%CPU) Time
------------------------------------------------------------------------------------------
0 SELECT STATEMENT 1 11 3 (0) 00:00:01
1 SORT AGGREGATE 1 11
2 TABLE ACCESS BY INDEX ROWID TBL_AUTOS 1 11 3 (0) 00:00:01
* 3 INDEX RANGE SCAN AUTOS_IDX 1 2 (0) 00:00:01
------------------------------------------------------------------------------------------

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

3 - access("CATEGORIA"='A' AND "VALOR">20000 AND "VALOR" IS NOT NULL)

Note
-----
- SQL profile "SYS_SQLPROF_01256a3d43600000" used for this statement

Se puede observar el el optimizador ahora utilizó el profile "SYS_SQLPROF_01256a3d43600000" estimando ahora una cardinalidad de 1 (debería ser 0 (cero) pero nunca se muestra 0 a menos que se trate de un predicado con una contradicción, por ejemplo: 1=2).

A partir de 11g existen las denominadas "extended statistics" que nos permiten recolectar estadísticas del tipo function based y multi-column y minimizar los problemas de estimación de cardinalidad con filtros de columna correlacionadas.

viernes, 4 de diciembre de 2009

Una introducción a los histogramas. Para que se usan y como se interpretan

Si alguna vez se pusieron a buscar información sobre que son, como funcionan y para que sirven realmente los histogramas en Oracle se habrán percatado que no existe demasiada data al respecto. La mayoria de la información es solo de referencia y no se explica claramente la verdadera esencia. El tema es bastante extenso, en esta primera parte, mi idea es introducir los conceptos principales, como se guarda la información de histogramas en el catálogo y como se interpreta dicho contenido. En futuras notas voy a mostrarles mas detalle de como se calcula la cardinalidad y el costo de los planes en base a la información estadística y de histogramas y tambien en que casos no sirven.

Oracle utiliza los histogramas para mejorar los calculos de cardinalidad y selectividad cuando la distribución de los datos no es uniforme. Hay que pensar a los histogramas como una "dibujo" de los datos que representa la distribución del contenido en las columnas. Existen dos tipos de histogramas: "Frecuency Histograms" y "Height Balanced Histrograms". Los primeros se usan cuando los valores distintos de la columna son pocos, y los segundos cuando la columna tiene gran cantidad de valores diferentes.

En los histogramas por frecuencia cada valor de la columna se corresponde con una entrada del histograma. Cada entrada contiene en número de concurrencia para un valor. En los histogramas balanceados la columna es dividida en buckets (barras), cada barra tiene el mismo número de filas y cada valor puede ser representado por uno o mas buckets, según su popularidad (cantidad de ocurrencias).

La principal tarea de los histogramas es ayudar a obtener la cardinalidad correcta, como ya comenté en otras notas, el calculo de la cardinalidad es fundamental para que Oracle arme el plan mas adecuado, lo que implica elegir el método y el orden de los joins y la elección de los indices correctos, y asi garantizar la mejor performance posible.

La forma de almacenamiento sigue conceptos estadísticos como percentiles y cuartiles y los valores se agrupan en buckets (si se grafica un histogramas, los buckets son como las barras en un gráfico de barras común y corriente).

Ahora vamos a hacer un par de pruebas y asi tratar de entender un poco mas el concepto detrás de los histogramas.

Primero voy a crear una tabla que tendrá 11110 filas, con cuatro valores posibles (1,2,3 y 4) de forma tal de que cada valor tenga una cantidad redonda de registros (potencia de 10).



rop@DESA10G> create table t
2 as select case when (rownum between 1 and 10) then 1
3 when (rownum between 11 and 110) then 2
4 when (rownum between 111 and 1110) then 3
5 when (rownum between 1111 and 11110) then 4
6 end x
7 from dual
8 connect by rownum <= 11110;

Tabla creada.

rop@DESA10G> select x,count(1)
2 from t
3 group by x;

X COUNT(1)
---------- ----------
1 10
2 100
3 1000
4 10000


La tabla T nos quedó con la distribución que se muestra arriba. Voy a recolectar
estadísticas y ver en la tabla de catálogo USER_TAB_COL_STATISTICS los datos estadisticos de la columna X:


rop@DESA10G> begin
2 dbms_stats.gather_table_stats(ownname => user,tabname => 'T');
3 end;
4 /

Procedimiento PL/SQL terminado correctamente.

rop@DESA10G> exec print_table('select * from user_tab_col_statistics where table_name = ''T''');
TABLE_NAME : T
COLUMN_NAME : X
NUM_DISTINCT : 4
LOW_VALUE : C102
HIGH_VALUE : C105
DENSITY : .25
NUM_NULLS : 0
NUM_BUCKETS : 1

LAST_ANALYZED : 02-dic-2009 15:28:59
SAMPLE_SIZE : 11110
GLOBAL_STATS : YES
USER_STATS : NO
AVG_COL_LEN : 3
HISTOGRAM : NONE
-----------------

Procedimiento PL/SQL terminado correctamente.


De los datos mostrados arriba vemos que Oracle detectó 4 valores distintos, que no hay valores nulos, que la densidad es 0.25 (1/"cant. de valores distinto"=1/4),que hay un solo bucket y que no hay histogramas (NONE). Ya que en la recolección no explicitamos el parámetro method_opt, que da directivas de como armar los histogramas, se uso el default que es: "for all columns size auto". La unica información de distribución es la siguiente:


rop@DESA10G> select * from user_tab_histograms where table_name = 'T';

TABLE_NAME COLUMN_NAM ENDPOINT_NUMBER ENDPOINT_VALUE ENDPOINT_A
---------- ---------- --------------- -------------- ----------
T X 0 1
T X 1 4


Recolectando estadísticas diciendole a Oracle que use 4 buckets para representar la distribución obtenemos:

rop@DESA10G> begin
2 dbms_stats.gather_table_stats(ownname => user,
3 tabname => 'T',
4 method_opt=>'for all columns size 4');
5 end;
6 /

Procedimiento PL/SQL terminado correctamente.

rop@DESA10G> exec print_table('select * from user_tab_col_statistics where table_name = ''T''');
TABLE_NAME : T
COLUMN_NAME : X
NUM_DISTINCT : 4
LOW_VALUE : C102
HIGH_VALUE : C105
DENSITY : .000045004500450045
NUM_NULLS : 0
NUM_BUCKETS : 4
LAST_ANALYZED : 02-dic-2009 16:12:21
SAMPLE_SIZE : 11110
GLOBAL_STATS : YES
USER_STATS : NO
AVG_COL_LEN : 3
HISTOGRAM : FREQUENCY
-----------------

Procedimiento PL/SQL terminado correctamente.

rop@DESA10G> select * from user_tab_histograms where table_name = 'T';

TABLE_NAME COLUMN_NAM ENDPOINT_NUMBER ENDPOINT_VALUE ENDPOINT_A
---------- ---------- --------------- -------------- ----------
T X 10 1
T X 110 2
T X 1110 3
T X 11110 4
rop@DESA10G>

Notemos que ahora se creo un "Frecuency Histrogram", que la densidad es mucho menor y viendo la tabla USER_TAB_HISTOGRAMS ahora se representa exactamente la distribución. Para interpretar esa tabla pensemos que la columna ENDPOINT_VALUE es cada valor posible de la columna y el ENDPOINT_NUMBER es la cantidad de registros iguales. Para 1 tenemos 10, para 2 tenemos 110-10=100, para 3 tenemos 1110-110=1000 y asi siguiendo.

Ahora voy a armar una distribución distinta, con muchos valores distintos para mostrarles el otro tipo de histograma:

rop@DESA10G> drop table t;

Tabla borrada.

rop@DESA10G> ed
Escrito file afiedt.buf

1 create table t
2 as select rownum x
3 from dual
4* connect by rownum <= 11110
rop@DESA10G> /

Tabla creada.

rop@DESA10G> update t set x = 1 where rownum <= 5000;

5000 filas actualizadas.

rop@DESA10G> update t set x = 2 where rownum <= 3000 and x != 1;

3000 filas actualizadas.

rop@DESA10G> create index t_idx on t(x);

Índice creado.


Se creó una tabla con la misma cantidad de filas que el ejemplo anterior pero ahora tenemos una distribución muy diferente. Para X=1 tenemos 5000 valores, para x=2 hay 3000 filas y para las demas solo 1 ocurrencia. Como ahora tengo muchos valores distintos si recolecto sin especificar me va a crear el histograma. Para evitar que lo cree en el parametro method_opt le especifiqué "for columns size 1" para que no asocie ningun histograma.


rop@DESA10G> begin
2 dbms_stats.gather_table_stats(ownname => user,
3 tabname => 'T',
4 cascade => true,
5 method_opt => 'for columns size 1');
6 end;
7 /

Procedimiento PL/SQL terminado correctamente.

rop@DESA10G> exec print_table('select * from user_tab_col_statistics where table_name = ''T''');

Procedimiento PL/SQL terminado correctamente.

Como se ve, consultando la tabla USER_TAB_COL_STATISTICS no obtenemos ningua fila, es decir no tenemos información de histogramas.
Recordemos que para X=1 tenemos 5000 valores, es decir casi la mitad del total de filas, por lo tanto si filtramos en el predicado por dicho valor es de esperar que el optimizador use un full_scan, no?, veamos que pasa:


rop@DESA10G> explain plan for select count(1) from t where x = 1;

Explicado.

rop@DESA10G> select * from table(dbms_xplan.display);

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
Plan hash value: 3482591947

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 3 | 1 (0)| 00:00:01 |
| 1 | SORT AGGREGATE | | 1 | 3 | | |
|* 2 | INDEX RANGE SCAN| T_IDX | 111 | 333 | 1 (0)| 00:00:01 |
---------------------------------------------------------------------------

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

2 - access("X"=1)

14 filas seleccionadas.

Notar que el optimizador estimó (columna ROWS) 111 filas, lo cual dista mucho de la realidad, recordemos que tenemos 5000 filas que cumplen con X=1. Si recolectamos usando el método default:

rop@DESA10G> ed
Escrito file afiedt.buf

1 begin
2 dbms_stats.gather_table_stats(ownname => user,
3 tabname => 'T',
4 cascade => true);
5* end;
rop@DESA10G> /

Procedimiento PL/SQL terminado correctamente.

rop@DESA10G> exec print_table('select * from user_tab_col_statistics where table_name = ''T''');
TABLE_NAME : T
COLUMN_NAME : X
NUM_DISTINCT : 3112
LOW_VALUE : C102
HIGH_VALUE : C3020C0B
DENSITY : .00009000900090009
NUM_NULLS : 0
NUM_BUCKETS : 254
LAST_ANALYZED : 04-dic-2009 10:02:53
SAMPLE_SIZE : 11110
GLOBAL_STATS : YES
USER_STATS : NO
AVG_COL_LEN : 4
HISTOGRAM : HEIGHT BALANCED
-----------------

Procedimiento PL/SQL terminado correctamente.


El tipo de histograma ahora es: "HEIGHT BALANCED", la cantidad de filas distintas es 3312 y el nro de buckets es de 254 (el máximo posible)

La representación de los datos en la tabla T ahora es la siguiente:

rop@DESA10G> select * from user_tab_histograms where table_name = 'T';


TABLE_NAME COLUMN_NAM ENDPOINT_NUMBER ENDPOINT_VALUE ENDPOINT
------------------------------ ---------- --------------- -------------- --------
T X 113 1
T X 181 2

T X 182 8008
T X 183 8052
T X 184 8096
T X 185 8140
T X 186 8184
T X 187 8228
T X 188 8272
T X 189 8315
T X 190 8358
T X 191 8401
T X 192 8444
T X 193 8487
T X 194 8530
T X 195 8573
T X 196 8616
T X 197 8659
T X 198 8702
T X 199 8745
T X 200 8788
T X 201 8831
T X 202 8874
T X 203 8917
T X 204 8960
T X 205 9003
T X 206 9046
T X 207 9089
T X 208 9132
T X 209 9175
T X 210 9218
T X 211 9261
T X 212 9304
T X 213 9347
T X 214 9390
T X 215 9433
T X 216 9476
T X 217 9519
T X 218 9562
T X 219 9605
T X 220 9648
T X 221 9691
T X 222 9734
T X 223 9777
T X 224 9820
T X 225 9863
T X 226 9906
T X 227 9949
T X 228 9992
T X 229 10035
T X 230 10078
T X 231 10121
T X 232 10164
T X 233 10207
T X 234 10250
T X 235 10293
T X 236 10336
T X 237 10379
T X 238 10422
T X 239 10465
T X 240 10508
T X 241 10551
T X 242 10594
T X 243 10637
T X 244 10680
T X 245 10723
T X 246 10766
T X 247 10809
T X 248 10852
T X 249 10895
T X 250 10938
T X 251 10981
T X 252 11024
T X 253 11067
T X 254 11110

75 filas seleccionadas.

La tabla de arriba se lee diferente a la que info que guardaba el histograma del tipo "FRECUENCY" ya que hay muchos valores para representar Oracle uso el tipo de histograma "HEIGHT BALANCED". Analizando el histograma vemos que la columna ENDPOINT_VALUE tiene 254 valores (buckets). La primera fila:

TABLE_NAME COLUMN_NAM ENDPOINT_NUMBER ENDPOINT_VALUE ENDPOINT
------------------------------ ---------- --------------- -------------- --------
T X 113 1

Tiene 113 buckets asignados. De donde sale es valor?. Pensemeos que tenemos 11110 filas, de las cuales tenemos 5000 con valor 1. Hagamos un poco de aritmetica:

Si tenemos 11100 filas representadas con 254 buckets y para el valor 1 se asignaron 113 tenemos:

rop@DESA10G> select (11110/254)*113 from dual;

(11110/254)*113
---------------
4942.6378

Cercano a 5000, no?, veamos que pasa para el valor 2:

TABLE_NAME COLUMN_NAM ENDPOINT_NUMBER ENDPOINT_VALUE ENDPOINT
------------------------------ ---------- --------------- -------------- --------
T X 113 1
T X 181 2

Para sacar la cantidad de buckets del ENDPOINT_VALUE tenemos que hacer:

rop@DESA10G> select (11110/254)*(181-113) from dual;

(11110/254)*(181-113)
---------------------
2974.33071

Como se ve esta muy cercano a 3000 que es la cantidad real del filas del tipo 2.
La diferencia se da por lo siguiente: Ya que tenemos 11110 filas representadas por 254 barras a buckets entonces tenemos: 11110/254= 43.74 filas por bucket. Los dos valores obtenidos arriba para X=1 y X=2 no son exactos porque se calcula discretizando por bucket.

Una vez obtenido el histograma veamos el plan que se obtiene para la consulta:

rop@DESA10G> explain plan for select count(1) from t where x = 1;

Explicado.

rop@DESA10G> select * from table(dbms_xplan.display);

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
Plan hash value: 1842905362

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 3 | 6 (0)| 00:00:01 |
| 1 | SORT AGGREGATE | | 1 | 3 | | |
|* 2 | TABLE ACCESS FULL| T | 4943 | 14829 | 6 (0)| 00:00:01 |
---------------------------------------------------------------------------

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

2 - filter("X"=1)

14 filas seleccionadas.

Esta vez el optimizador eligió ir por un FULL_SCAN que es el mejor plan ya que tiene que recorrer casi la mitad de la tabla.

Si buscamos un valor con una sola ocurrencia:

rop@DESA10G> explain plan for select count(1) from t where x = 999999;

Explicado.

rop@DESA10G> select * from table(dbms_xplan.display);

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
Plan hash value: 3482591947

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 3 | 1 (0)| 00:00:01 |
| 1 | SORT AGGREGATE | | 1 | 3 | | |
|* 2 | INDEX RANGE SCAN| T_IDX | 1 | 3 | 1 (0)| 00:00:01 |
---------------------------------------------------------------------------

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

2 - access("X"=999999)

14 filas seleccionadas.

rop@DESA10G>

Se observa que se usa el indice en el plan, que obviamente es el mejor acceso posible.
Por último, es importante prestar atención a la cardinalidad (columna ROWS del plan) para el caso de X=1 el calculo fue: 4943 que es el valor redondeado que obtuvimos mas arriba haciendo: (11110/254)*113. De esto podemos confirmar que el Optimizador consultó el histograma para obtener la cardinalidad mas cercana a la real y por lo tanto generó el plan mas adecuado.

domingo, 29 de noviembre de 2009

Testeando el commit asincrónico (COMMIT_WRITE)

En 10g R2 se introdujo el parámetro COMMIT_WRITE cuya finalidad es permitir el uso de commits asincronicos. Como es sabido cada vez que efectuamos un commit (o rollback) se persiste la informacion de la transacción en los redo logs en forma sincronica, esto significa que hasta que no se haga efectiva dicha escritura en disco, no se retorna el control a la aplicación o proceso que confirmó la transacción. Ahora bien, si pensamos en un commit asincronico estariamos violando la premisa de Durabilidad de una transacción, recordemos que en Oracle las transacciones son compatibles con el concepto ACID (Atomicity, Consistency, Isolation, Durability), y por lo tanto se podrian perder transacciones si se produjera un error severo de Hardware. Al no asegurar la durabilidad de las transacciones no podemos confiar que la base de datos maneje en forma totalmente confiable nuestras transaccciones lo cual resulta en sistemas inestables y propensos a errores e insonsistencias. Despues de todo lo que les comenté se preguntaran para que puede servir este nuevo parámetro, no?, bueno... yo diría que en ciertos casos muy particulares y acotados, donde se pueda tolerar perdida de información y donde se requiera muy alta transaccionabilidad, encontrariamos un beneficio en performance (por ejemplo se reducirian las esperas en el evento log file sync) ya que la ejecución de las sentencias COMMIT es instantanea lo cual genera mayor cantidad de ejecuciones.

Antes de ir a la prueba me gustaría aclarar que si se ven tentados a usar este parámetro para acelerar el procesamiento, primero chequeen el diseño de transaccionabilidad de su aplicación y vean si es posible una vuelta de tuerca desde el lado del código, ya sea viendo si es posible "commitear" menos frecuentemente o si se esta "commiteando" en forma redundante, antes de ir una solución (quick and dirty) como es tocar el parámetro COMMIT_WRITE.

Para probar este parámetro, armé un bloque PL/SQL anónimo donde voy a probar las cuatro conbinaciones posibles del COMMIT_WRITE y voy a almacenar los resultados del test en la tabla RES para luego realizar una consulta que me permita comparar en forma sencilla cada opción.


SQL> create table T (X NUMBER(38),
2 Y VARCHAR2(10),
3 Z DATE)
/
Table created.

SQL>

SQL> create table RES (NAME VARCHAR2(200) not null,
2 VALUE NUMBER,
3 NCASE NUMBER(1) not null)
/
Table created.

rop@BPP3> begin
2 execute immediate 'truncate table res';
3
4 for i in 1..4 loop
5
6 case i when 1 then
7 execute immediate 'alter session set commit_write=''immediate,wait''';
8 when 2 then
9 execute immediate 'alter session set commit_write=''immediate,nowait''';
10 when 3 then
11 execute immediate 'alter session set commit_write=''batch,wait''';
12 when 4 then
13 execute immediate 'alter session set commit_write=''batch,nowait''';
14 end case;
15
16 insert into res
17 select 'STAT...' || a.name name,
18 b.value,
19 case i when 1 then 1
20 when 2 then 2
21 when 3 then 3
22 when 4 then 4
23 end
24 from v$statname a, v$mystat b
25 where a.statistic# = b.statistic#
26 union all
27 select 'LATCH.' || name,
28 gets,
29 case i when 1 then 1
30 when 2 then 2
31 when 3 then 3
32 when 4 then 4
33 end
34 from v$latch
35 union all
36 select 'STAT...Elapsed Time',
37 hsecs,
38 case i when 1 then 1
39 when 2 then 2
40 when 3 then 3
41 when 4 then 4
42 end
43 from v$timer;
44
45 for j in (select trunc(dbms_random.value(1,10000)) x,
46 dbms_random.string('a',10) y,
47 sysdate+dbms_random.value(-300,300) z
48 from dual
49 connect by rownum <= 100000)
50 loop
51 insert into t values (j.x,j.y,j.z);
52 commit;
53 end loop;
54
55 for k in (select 'STAT...' || a.name name, b.value
56 from v$statname a,
57 v$mystat b
58 where a.statistic# = b.statistic#
59 union all
60 select 'LATCH.' || name, gets
61 from v$latch
62 union all
63 select 'STAT...Elapsed Time',hsecs
64 from v$timer)
65 loop
66 update res
67 set value = (k.value-value)
68 where res.name = k.name
69 and res.ncase = i;
70 end loop;
71 commit;
72 end loop;
73 end;
/
PL/SQL procedure successfully completed.

Ahora voy a ejecutar una consulta sobre la tabla de resultados de forma tal de encolumnar cada caso y poder comparar mas facilmente.

rop@BPP3> select name,
max(decode(ncase,1,value,null)) "IMMEDIATE,WAIT",
max(decode(ncase,2,value,null)) "IMMEDIATE,NOWAIT",
max(decode(ncase,3,value,null)) "BATCH,WAIT",
max(decode(ncase,4,value,null)) "BATCH,NOWAIT"
from res
group by name

NAME IMMEDIATE,WAIT IMMEDIATE,NOWAIT BATCH,WAIT BATCH,NOWAIT

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

STAT...recursive cpu usage 1393 887 1293 849

STAT...messages sent 100040 10449 100008 53

STAT...commit wait requested 100000 0 100000 0

STAT...SMON posted for undo segment shrink 0 0 0 0

STAT...commit batch performed 0 0 100000 100000

STAT...commit immediate performed 100000 100000 0 0

STAT...commit nowait performed 0 100000 0 100000

LATCH.session timer 19 3 34 3

STAT...redo synch time 4042 0 8777 0

STAT...commit batch requested 0 0 100000 100000

STAT...commit wait/nowait requested 100000 100000 100000 100000

STAT...commit wait performed 100000 0 100000 0

LATCH.messages 287070 23614 462948 447

STAT...Elapsed Time 5543 965 10056 927

STAT...redo synch writes 100000 0 100000 0

STAT...commit batch/immediate requested 100000 100000 100000 100000

STAT...commit immediate requested 100000 100000 0 0

LATCH.Consistent RBA 100035 10447 100015 54

LATCH.cache buffers lru chain 652 600 4870 3282

LATCH.object queue header heap 93 16 216728 344

LATCH.redo allocation 587541 414896 504629 304247

LATCH.JS queue state obj latch 396 72 720 72

STAT...CPU used by this session 1493 956 1388 915

STAT...session pga memory max 4587520 65536 0 0

STAT...commit nowait requested 0 100000 0 100000

LATCH.client/application info 4 0 8 0

LATCH.OS process allocation 19 3 34 3

LATCH.active checkpoint queue latch 24 5 54186 89

LATCH.archive process latch 26 9 46 9

LATCH.post/wait queue 150161 5 150132 3

LATCH.parameter table allocation management 2 0 4 0

LATCH.redo writing 300168 31357 354299 258



El resultado de la consulta me arrojó mas de 700 estadisticas y latches. Solo copié
las filas donde noté diferencias importantes en los casos. En especial, la fila resaltada (Elapsed Time) muestra el tiempo total de procesamiento en cada caso en centisegundos. Como se observa para los casos donde se usa WAIT los tiempos son bastante mayores, lo cual comprueba la mejora en performance al usar asincronismo (NOWAIT).

Mientras escribia esta nota descubrí que en 11g el parametro commit_write esta "deprecated", parece que tuvo poca vida, no?. De todas formas la documentación aclara que fue reemplazado por los parametros COMMIT_WAIT y COMMIT_LOGGING.
Usando estos dos nuevos parametros en conjunto se obtendría el mismo funcionamiento que COMMIT_WRITE, aunque que el parametro COMMIT_WAIT agregó una nueva opción para forzar el wait (force_wait)

jueves, 22 de octubre de 2009

Variante para evitar la creación de sinonimos locales en usuarios de aplicación

Es una práctica recomendada y en general impuesta por el area de seguridad de las empresas, crear un esquema de datos (owner) y un esquema de acceso (usuario de explotación o usuario de aplicación) cuando se realiza el setup o deploy de una nueva aplicación o modulo en un base de datos. Este esquema imposibilita a los usuarios (excepto obviamente al los usuarios administradores o dba) realizar cambios sobre los objetos del esquema propietario. Las pautas mas importantes a seguir en dicho esquema de layout son:

  • Los permisos sobre los objetos del owner se deben otorgar por medio de roles hacia los usuarios de explotación (nunca en forma directa).
  • Los usuarios de explotación solo deben tener otorgado el rol de conexión y el rol de definido para la aplicación. No tendrán quota en ningun tablespace ni podrán crear ningun objeto.
  • Los objetos otorgados desde el owner hacia el usuario de explotacion deberan ser referenciados mediante un sinonimo local en el usuario de explotación (no referenciar ningun objeto anteponiendo el esquema owner).
  • Los esquema owner y de explotación y los roles deberán ser creados por el area de base de datos. En el deploy de las app se deberá otorgar los grants al rol y crear los sinonimos locales.
Una variante interesante para evitar la gestión de sinonimos locales es alterar la sesion (podría ser mediante un on-logon trigger) de forma tal de cambiar el esquema corriente (de explotación a owner) y asi no tener la necesidad de calificar el objeto anteponiendo el esquema. Les voy a mostrar como implementar esta variante:

Lo primero es crear un on logon trigger. En el ejemplo mi usuario de explotación de llama exp_rop


create or replace trigger SYSTEM.onLogon_trg
after logon on database
Begin
if ( user in ('EXP_ROP')) then
execute immediate 'alter session set current_schema=ROP';
end if;
end;


Creo un esquema de explotación y el rol de aplicación (rol_rop). El objeto usado para la prueba
es la tabla ROP.T.

rop@DESA10G> create user exp_rop identified by exp_rop;

Usuario creado.

rop@DESA10G> grant create session to exp_rop;

Concesión terminada correctamente.

rop@DESA10G> desc t
Nombre ¿Nulo? Tipo
----------------------------------------------------- -------- ------------------------------------
C1 VARCHAR2(40)
N1 NUMBER
N2 NUMBER

rop@DESA10G> create role rol_rop;

Rol creado.

rop@DESA10G> grant select on t to rol_rop;

Concesión terminada correctamente.

rop@DESA10G> grant rol_rop to exp_rop;

Concesión terminada correctamente.


Ahora pruebo de conectarme con el usuario de explotación para ver si se ejecuta el trigger y se altera la sesión:

rop@DESA10G> conn exp_rop/exp_rop@movi10d
Conectado.
exp_rop@DESA10G> desc t
Nombre ¿Nulo? Tipo
----------------------------------------------------- -------- ------------------------------------
C1 VARCHAR2(40)
N1 NUMBER
N2 NUMBER

exp_rop@DESA10G> select count(1) from t;

COUNT(1)
----------
5000

exp_rop@DESA10G>

Como se vió pude acceder la tabla T que esta en el esquema ROP sin la necesidad de utilizar un sinonimo local.

jueves, 24 de septiembre de 2009

Reporte de Tablas sin estadisticas o con estadisticas viejas (stale) para una sesion dada

Una gran parte de los problemas repentinos de performance se da porque el optimizador arma un plan ineficiente producto de que las estadisticas actuales de las tablas, particiones o subparticiones involucradas no reflejan la realidad. Esto se debe a que los segmentos sufrieron un cambio de datos mayor al 10% y no se actualizaron las estadisticas en el catalogo. Uno de los datos con el que comenzamos a analizar este tipo de problema es el sid asociado a la sesion que esta ejecutando con demoras. Teniendo el sid el siguiente paso es ver la sentencia en ejecución y chequear si los segmentos referenciados cuentan con estadisticas frescas. Si la sentencia en cuestion es compleja y referencia varios segmentos nos demorará un tiempo revisar cada uno de los segmentos. Por tal motivo pensé en armar un query que basado principalmente en la vista dinamica v$sql_plan, obtiene los segmentos usados en los paths del plan de ejecucion y luego verifica si estan STALE o si estan nulas usando la vista dba_tab_statistics.
El script de abajo permite determinar automaticamente que tablas, particiones y subparticiones tienen estadisticas desactualizadas para un sid determinado.


set line 120
set pagesize 999
set verify off

col owner format a15
col table_name format a30
col partition_name format a30
col subpartition_name format a30

PROMPT
PROMPT "---------------------------------------------"
PROMPT "Reporte de Tablas con estadisticas STALE "
PROMPT "o nulas para una sesion dada "
PROMPT "---------------------------------------------"
ACCEPT sid PROMPT "Ingrese SID a evaluar: "

select st.owner owner,
st.table_name table_name,
st.partition_name partition_name,
st.subpartition_name subpartition_name
from v$session s,
v$sql_plan p,
dba_tab_statistics st
where s.sql_id = p.sql_id
and p.object_owner = st.owner
and p.object_name = st.table_name
and s.sid = &sid
and nvl(st.stale_stats,'YES') = 'YES'
and ((nvl(st.partition_position,1)
between
(case when (REGEXP_LIKE(nvl(p.partition_start,'a'),'[^[:digit:]]'))
then 1
else to_number(p.partition_start) end)
and (case when
(REGEXP_LIKE(nvl(p.partition_stop,'a'),'[^[:digit:]]'))
then 10000
else to_number(p.partition_stop) end))
or
(nvl(st.subpartition_position,1) between
(case when (REGEXP_LIKE(nvl(p.partition_start,'a'),'[^[:digit:]]'))
then 1
else to_number(p.partition_start) end)
and (case when
(REGEXP_LIKE(nvl(p.partition_stop,'a'),'[^[:digit:]]')) then 10000
else to_number(p.partition_stop) end))
)
/

set verify on


IMPORTANTE: Para asegurar que esten impactados los cambios mas recientes en la
vista dba_tab_statistics es recomendable flushear la memoria de la
siguiente forma: dbms_stats.flush_database_monitoring_info.

miércoles, 23 de septiembre de 2009

Reporte historico de tiempo de ejecucion máxima, mínima y promedio de sentencias SQL

Cualquier DBA que haya trabajado un tiempo administrando bases de datos de producción, seguramente fue consultado, y a veces acusado, debido a demoras en los procesos o reportes. Ante ese tipo de cuestionamientos, lo primero que tenemos que asegurar es si realmente el proceso esta demorado o si se trata de la percepción o ansiedad del usuario u operador. La unica manera de saber eso, es analizando la historia de ejecución de las sentencias involucradas en las rutinas afectadas. Como es sabido, desde 10g contamos con un completo repositorio que se actualiza automaticamente, que entre otras estadisticas y metricas posee información sobre las sentencias ejecutadas. La vista DBA_HIST_SQLSTAT recolecta para cada sentencia, el tiempo de ejecucion general, el tiempo en cpu, el tiempo en i/o, cantidad de ejecuciones, etc. Utilizando dicha información armé un query que muestra la ejecucion mas larga, la ejecucion mas corta y un promedio para cada sentencia registrada.
El query de abajo realiza las agregaciones (max,min y avg) para todas las sentencias ejecutadas durante toda la historia almacenada en AWR (por default 7 dias). Tambien se podria reescribir levemente la query para que dado un sql_id retorne los resultados particulares, recordemos que sql_id es la identificación unica desde 10g para las sentencias sql (antes se usaba hash_value para identificar univocamente una sentencia).


select sql_id,
to_char(trunc(max(max_ela_time)/60/60),'09')||
to_char(trunc(mod(max(max_ela_time),3600)/60),'09')||
to_char(mod(mod(max(max_ela_time),3600),60),'09') max_ela_time,
to_char(max(max_ela_time_dt),'DD/MM/YYYY HH24:MI') max_ela_time_dt,
to_char(trunc(min(min_ela_time)/60/60),'09')||
to_char(trunc(mod(min(min_ela_time),3600)/60),'09')||
to_char(mod(mod(min(min_ela_time),3600),60),'09') min_ela_time,
to_char(min(min_ela_time_dt),'DD/MM/YYYY HH24:MI') min_ela_time_dt,
to_char(trunc(avg(avg_ela_time)/60/60),'09')||
to_char(trunc(mod(avg(avg_ela_time),3600)/60),'09')||
to_char(mod(mod(avg(avg_ela_time),3600),60),'09') avg_ela_time
from
(select unique sql_id,
round((first_value(elapsed_time)
over (partition by sql_id order by elapsed_time desc))/executions/1000000) max_ela_time,
first_value(dt) over (partition by sql_id order by elapsed_time desc) max_ela_time_dt,
round((first_value(elapsed_time)
over (partition by sql_id order by elapsed_time))/executions/1000000) min_ela_time,
first_value(dt)
over (partition by sql_id order by elapsed_time) min_ela_time_dt,
round((avg(elapsed_time) over (partition by sql_id))/executions/1000000) avg_ela_time
from (select unique
ss.sql_id,
s.snap_id,
lag (s.snap_id) over (partition by s.startup_time,ss.sql_id order by ss.snap_id desc) snap_id_n,
ss.elapsed_time_total elapsed_time,
s.begin_interval_time dt,
lag (ss.elapsed_time_total)
over (partition by s.startup_time,ss.sql_id order by s.snap_id desc ) elapsed_time_n,
lag (s.begin_interval_time)
over (partition by s.startup_time,ss.sql_id order by s.snap_id desc ) dt_n,
executions_total executions,
lag (ss.executions_total)
over (partition by s.startup_time,ss.sql_id order by s.snap_id desc ) executions_n
from dba_hist_sqlstat ss,
dba_hist_snapshot s
where s.snap_id = ss.snap_id)
where elapsed_time > elapsed_time_n
and executions != 0)
group by sql_id
order by avg_ela_time desc
/

SQL_ID MAX_ELA_T MAX_ELA_TIME_DT MIN_ELA_T MIN_ELA_TIME_DT AVG_ELA_T
------------- --------- ---------------- --------- ---------------- ---------
89qyn4bbt03jq 00 05 24 22/09/2009 18:00 00 00 56 22/09/2009 06:00 00 03 06
gfjvxb25b773h 00 00 13 22/09/2009 17:48 00 00 13 22/09/2009 17:48 00 00 13
a1axyycsv1fb1 00 00 06 21/09/2009 18:00 00 00 06 21/09/2009 18:00 00 00 06
fqmpmkfr6pqyk 00 00 05 21/09/2009 12:00 00 00 05 21/09/2009 12:00 00 00 05
b7jn4mf49n569 00 00 05 21/09/2009 20:00 00 00 05 21/09/2009 20:00 00 00 05
4c1xvq9ufwcjc 00 00 03 22/09/2009 17:48 00 00 03 22/09/2009 17:48 00 00 03
06fhnfwzpzvug 00 00 03 22/09/2009 17:48 00 00 03 22/09/2009 17:48 00 00 03
ahtrk133zdqa5 00 00 02 21/09/2009 22:00 00 00 02 21/09/2009 22:00 00 00 02
bunssq950snhf 00 00 02 21/09/2009 19:00 00 00 02 21/09/2009 19:00 00 00 02
d92h3rjp0y217 00 00 01 23/09/2009 03:00 00 00 00 21/09/2009 19:00 00 00 01
abtp0uqvdb1d3 00 00 02 23/09/2009 08:00 00 00 00 22/09/2009 06:00 00 00 01
8bfst16kjukv6 00 00 01 22/09/2009 17:48 00 00 01 22/09/2009 17:48 00 00 01
6jgrbypm756nu 00 00 01 21/09/2009 11:00 00 00 01 21/09/2009 11:00 00 00 01

miércoles, 16 de septiembre de 2009

Como minimizar problemas luego de un upgrade de versión de Oracle (Caso2: Cambio del orden de evaluación de predicados)

En esta nueva nota les voy a contar un problema con el que se pueden encontrar al migrar desde version 8i hacia 9i o superior. El principal tema radica en la evolución constante que va teniendo el optimizador para estimar el costo de acceso a los datos de las sentencias. En la versión 7, donde apareció por primera vez el optimizador por costos, el costo se calculaba simplemente ponderando por la cantidad de requerimientos de lectura a disco. Esto, como es sabido, provocó un rechazo a cambiar de RBO a CBO bastante generalizado en su momento, dado que los planes de ejecución comenzaban a hacer cosas extrañas, causando importantes problemas de rendimiento generalizado. Por tal motivo, la mayoria de las compañias continuaron usando el optimizador por reglas, ya que les garantizaba que no se alteraran los planes y que no se destabilizaran las aplicaciones. El principal problema de usar solo los read request para generar el costo en Oracle 7 fue no considerar el caching que existe en distintos niveles.

A partir de 8i, se comenzó a ponderar por tipo de lectura (single reads y multiblock reads) y por tamaño y tiempo de lectura. Esto mejoró bastante la calidad de los planes generados y dió mayor confianza a las empresas para animarse a cambiar a CBO, mas que nada porque Oracle Corporation comenzó a incentivar fuertemente a salir de RBO, discontinuado a partir de 7.x (año 1992) y desoportado desde 10g. Si bien el comportamiento en 8i fue mucho mas estable faltaba tomar en cuenta algo muy importante, el tiempo de cpu.

Recien a partir de 9i se incluyó en el calculo del costo el tiempo insumido en procesamiento de cpu, anteriormente la formula para calcular solo tomaba en cuenta caracteristicas de i/o. En consecuencia con esta nueva variable de ponderación el optimizador puede cambiar el orden de evaluación de los predicados si estima que con ese nuevo orden se minimiza el tiempo de cpu. Este reordenamiento puede causar que ciertas consultas fallen en 9i+ y no en 8i. Este fallo generalmente se debe a una inconsistencia de datos, que antes quedaba "tapada" y que ahora al evaluar en otro orden genera un error, por ejemplo con una conversión implicita.

Vayamos a los ejemplos para graficar mejor este tema:

Primero voy a crear la siguiente tabla:

rop@DESA10G> create table t as
2 select to_char(mod(rownum,30)) c1,
3 rownum n1,
4 mod(rownum,30) n2
5 from dual
6 connect by rownum <= 5000; Tabla creada.

La tabla T tiene 3 columnas:
C1: solo tendrá valores 30 posibles valores (de 0 a 29) y es de tipo varchar2.
N1: tiene valores distintos del 1 al 5000.
N2: tiene los mismos valores de C1 pero es de tipo number.

Para simular estar en 8i, voy a cambiar el parametro de session "optimizer_features_enable" para que el optimizador se comporte con un optimizador de 8i.

rop@DESA10G> alter session set optimizer_features_enable = '8.1.7';

Sesión modificada.

Ahora, ejecuto una sentencia que cuenta la cantidad de filas filtrando la tabla
por los tres campos.

rop@DESA10G> ed
Escrito file afiedt.buf

1 explain plan for
2 select count(1) from t
3* where c1 = 1 and n1 = 1111 and n2 = 1
rop@DESA10G> /

Explicado.

rop@DESA10G> select * from table(dbms_xplan.display);

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
Plan hash value: 1842905362

-----------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
-----------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 48 | 2 |
| 1 | SORT AGGREGATE | | 1 | 48 | |
|* 2 | TABLE ACCESS FULL| T | 1 | 48 | 2 |
-----------------------------------------------------------

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

2 - filter(TO_NUMBER("C1")=1 AND "N1"=1111 AND "N2"=1)

Note
-----
- cpu costing is off (consider enabling it)

18 filas seleccionadas.

Mirando la información predicados (Predicate Information) notamos dos cosas interesantes. Primero hay una nota que nos advierte que el costo por cpu esta en off. Lo cual es lógico, por lo que comenté mas arriba respecto a que 8i no ponderaba por cpu y si bien estamos en 10g, recordemos que cambié el comportamiento del optimizador a 8i. Segundo vemos que los predicados se evaluaron en orden y que hubo una conversión implicita (TO_NUMBER()).

Vuelvo a poner el optimizador a su valor default y me fijo evaluo el plan:

rop@DESA10G> alter session set optimizer_features_enable = '10.2.0.4';

Sesión modificada.

rop@DESA10G> explain plan for
2 select count(1) from t
3 where c1 = 1 and n1 = 1111 and n2 = 1;

Explicado.

rop@DESA10G> select * from table(dbms_xplan.display);

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
Plan hash value: 1842905362

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 48 | 5 (0)| 00:00:01 |
| 1 | SORT AGGREGATE | | 1 | 48 | | |
|* 2 | TABLE ACCESS FULL| T | 1 | 48 | 5 (0)| 00:00:01 |
---------------------------------------------------------------------------

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

2 - filter("N1"=1111 AND "N2"=1 AND TO_NUMBER("C1")=1)

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

18 filas seleccionadas.

El plan no cambió, se hizo sampleo dinamico ya que no habia recolectado estadisticas, pero lo mas importante a destacar es el cambio en la evaluación de los filtros. Notemos que ahora la conversión implicita se dejó para lo ultimo. Por que hizo eso el optimizador?, bien, pensemos que una conversión implica ciclos de cpu y entonces, porque no mejor evaluarlo al final cuando seguramente queden menos filas, ya que se van filtrando con los dos predicados o filtros anteriores, y asi minimizar la cantidad de conversiones, no?. Como ya dijimos el optimizador en versiones 9i+ se preocupa por el costo de procesamiento de cpu y por lo tanto puede realizar ciertos ajustes (reordenamiento de predicados, merge de subqueries, etc) si con eso se reduce la utilización de cpu.

Analicemos mas en detalle el ejemplo y como se evalua con cpu costing en ON y en OFF


Con CPU Costing en OFF (8i)

filter(TO_NUMBER("C1")=1 AND "N1"=1111 AND "N2"=1)

El primer predicado (TO_NUMBER("C1")=1) evalua 5000 filas y como resultado de ese filtro se queda con 167 filas.
El segundo predicado (N1=1111) evalua 167 filas y se queda con 1.
El tercer predicado (N2=1) evalua 1 y devuelve 1 (el resultado del count()).

Con CPU Costing en ON (9i+)

filter("N1"=1111 AND "N2"=1 AND TO_NUMBER("C1")=1)


El primer predicado (N1=1111) evalua 5000 filas y solo devuelve 1 fila.
El segundo predicado (N2=1) compara la fila y se la pasa al siguiente predicado.
El tercer predicado (TO_NUMBER("C1")=1) evalua una sola fila y devuelve el resultado.

En base al ejemplo analizado se ve claramente que con cpu costing apagado se deben realizar 5000 operaciones implicitas contra 1 operacion implicita cuando se tiene activado el costeo por cpu. Con este ejemplo sencillo, pero representativo, se puede ver lo importante del ordenamiento de la evaluación para minimizar el uso de recurso de cpu y por ende mejorar el tiempo de respuesta general.


Ahora veamos como fijar el orden de evaluación sin cambiar el comportamiento general de la sesion usando el hint "ordered_predicates":

rop@DESA10G> ed
Escrito file afiedt.buf

1 explain plan for
2 select /*+ ordered_predicates */ count(1) from t
3* where c1 = 1 and n1 = 1111 and n2 = 1
rop@DESA10G> /

Explicado.

rop@DESA10G> select * from table(dbms_xplan.display);

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
Plan hash value: 1842905362

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 48 | 5 (0)| 00:00:01 |
| 1 | SORT AGGREGATE | | 1 | 48 | | |
|* 2 | TABLE ACCESS FULL| T | 1 | 48 | 5 (0)| 00:00:01 |
---------------------------------------------------------------------------

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

2 - filter(TO_NUMBER("C1")=1 AND "N1"=1111 AND "N2"=1)

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

18 filas seleccionadas.

Con el hint la evaluación es ordenada, tal cual hubiese sido en 8i por default.
En el plan no se muestró el costo de cpu, pero consultando la tabla de soporte PLAN_TABLE podemos ver que costos fueron asignados de acuerdo al orden de evaluación.

rop@DESA10G> ed
Escrito file afiedt.buf

1 select filter_predicates,cpu_cost from plan_table
2* where filter_predicates is not null
rop@DESA10G> /

FILTER_PREDICATES CPU_COST
-------------------------------------------------- ----------
"N1"=1000 AND "N2"=1 AND TO_NUMBER("C1")=1 1302275
TO_NUMBER("C1")=1 AND "N1"=1111 AND "N2"=1 1802225

rop@DESA10G>

El costo de cpu al evaluar en 10g fue de 1302275 (con reordenamiento) y el costo de cpu sin ordenamiento fue de 1802225 (8i)

Como demostré con el ejemplo, el optimizador evalua los predicados de forma tal de optimizar el uso de cpu. Este cambio podría hacer fallar ciertos codigos que antes funcionaban y que tenian inconsistencias de datos, como por ejemplo, tener valores no numericos sobre campos varchar que deben tener numeros, y entonces al convertir implicitamente se genere un error de invalid number (ORA-01722). Queda claro que si ocurre esto es debido a un mal diseño, ya que una columna que aloja solo números no debería ser de tipo varchar, ya que de esta forma se promueve, por un lado inconsistencias en los datos y por otro lado, se afecta el rendimiento debido a las conversiones implicitas.

jueves, 10 de septiembre de 2009

Como minimizar problemas luego de un upgrade de versión de Oracle (Caso1: Ordenamiento implicito de GROUP BY en versiones anteriores a 10g R2)

Esta nota pretende ser la primera de una serie de notas en las que voy a comentarles problemas que se pueden dar luego de un upgrade de versión de base de datos. Este año estuve involucrado en unos cuantos upgrades de versiones, siempre hacia version 10g R2 pero partiendo desde distintas versiones base (8i, 9i R2 y 10g R1). Aqui en Argentina todavia son muy pocos los que migraron a 11g ya que es una practica habitual, y en mi opinión muy acertada, esperar al segundo release para upgradear. Al momento de esta nota acaba de salir el 11g R2, pero solo para linux, asi que estimo que en poco tiempo tendremos disponible el nuevo release para las plataformas unix "grandes", tales como solaris, aix, hp-ux y tambien para la familia de SO's de windows.

Es sabido que cada nueva versión de Oracle introduce nuevos features, y en especial features que cambian el comportamiento del optimizador y que producen cambios en los paths de los planes de ejecución. Los cambios en el optimizador son generalmente para mejorar el acceso a los datos y por ende reducir el tiempo de respuesta. Esta mejora en la "inteligencia" del optimizador no debería ocacionar cambios de comportamiento, a menos que no se cumplan las Buenas Practicas de confección de sentencias sql. Una mala práctica, y por desgracia bastante común, es confiar en el ordenamiento implicito que se da, por ejemplo, al usar distint/unique o en el ordenamiento que se produce con el group by. Este último es a veces innecesario y agrega un path implicito para ordenar que suma un tiempo mas antes de retornar la respuesta.

A partir de 10g R2 cambió el path SORT GROUP BY por el path HASH GROUP BY mejorando el rendimiento dado que no se infiere la necesidad de retornar el resultado ordenado. Todas las unidades de código que "confiaban" en este ordenamiento implicito y que necesitan por negocio un cierto orden van a comenzar a devolver resultados erroneos al upgradear a 10g R2 o versión superior, recordemos que las "Best Practices" dictan usar siempre ORDER BY cuando debe haber un orden ya que el comportamiento no esta garantizado a futuro.

Ahora, como suelo hacer, les voy a mostrar el ejemplo del group by, en próximas notas le voy a mostrar otros "issues" que pueden causar fuertes dolores de cabeza cuando no se detectan a tiempo.

Voy a usar mi conocida, y nunca bien ponderada, tablita de ejemplo T


rop@TEST10G> select * from v$version;

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


rop@TEST10G> create table t as select * from dba_objects;

Tabla creada.

rop@TEST10G> exec dbms_stats.gather_table_Stats(user,'T');

Procedimiento PL/SQL terminado correctamente.


rop@TEST10G> select object_type,count(1)
2 from t
3 group by object_type

OBJECT_TYPE COUNT(1)
------------------- ----------
INDEX 22538
JOB CLASS 2
CONTEXT 5
TABLE SUBPARTITION 18
TYPE BODY 174
INDEXTYPE 10
PROCEDURE 252
RESOURCE PLAN 4
RULE 4
JAVA CLASS 16417
SCHEDULE 1
TABLE PARTITION 625
WINDOW 2
WINDOW GROUP 1
JAVA RESOURCE 770
TABLE 22631
TYPE 1941
VIEW 3804
LIBRARY 150
FUNCTION 329
TRIGGER 565
PROGRAM 12
MATERIALIZED VIEW 3
DATABASE LINK 5
CLUSTER 10
SYNONYM 23307
PACKAGE BODY 807
QUEUE 27
CONSUMER GROUP 6
EVALUATION CONTEXT 14
RULE SET 19
DIRECTORY 16
UNDEFINED 6
OPERATOR 57
JAVA DATA 306
DIMENSION 5
SEQUENCE 2759
LOB 713
PACKAGE 866
JOB 18
INDEX PARTITION 617
LOB PARTITION 1
XML SCHEMA 26

43 filas seleccionadas.

El listado salio desordenado, veamos el plan que genera:

rop@TEST10G> explain plan for
2 select object_type,count(1)
3 from t
4 group by object_type;

Explicado.

rop@TEST10G> select * from table(dbms_xplan.display);

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
Plan hash value: 2963600285

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 26 | 208 | 339 (7)| 00:00:05 |
| 1 | HASH GROUP BY | | 26 | 208 | 339 (7)| 00:00:05 |
| 2 | TABLE ACCESS FULL| T | 99843 | 780K| 324 (2)| 00:00:04 |
---------------------------------------------------------------------------

9 filas seleccionadas.

El path es HASH GROUP BY en reemplazo de SORT HASH GROUP

Ahora, voy a usar, el truco mas rápido para salir del paso, tipo de solución "quick and dirty", pero solución al fin, que me ha salvado varias veces cuando se pasó por alto algún nuevo mecanismo y se comienzan a ver los problemas en plena hora pico o cuando cancelan procesos baths al dia siguiente del upgrade. Podemos setear a nivel sesion el parámetro "optimizer_features_enable", tambien se puede setear a nivel de hint con opt_param(param,valor), para hacer un flashback al comportamiento de un versión anterior. Como el caso de esta nota se da a partir de 10g R2, como estrategia siempre tomo la decisión de ir al upgrade anterior mas próximo donde funciona como antes, para asi estabilizar el comportamiento y no tener que retornar al release original. Como dije antes, en este caso la solución definitiva sera disparar un requerimiento de cambio de código y que el sector de desarrollo agregue el order by en las sentencias que necesitan del ordenamiento para funcionar correctamente.



rop@TEST10G> alter session set optimizer_features_enable = '10.1.0.5';

Sesión modificada.


rop@TEST10G> select object_type,count(1)
2 from t
3 group by object_type;

OBJECT_TYPE COUNT(1)
------------------- ----------
CLUSTER 10
CONSUMER GROUP 6
CONTEXT 5
DATABASE LINK 5
DIMENSION 5
DIRECTORY 16
EVALUATION CONTEXT 14
FUNCTION 329
INDEX 22538
INDEX PARTITION 617
INDEXTYPE 10
JAVA CLASS 16417
JAVA DATA 306
JAVA RESOURCE 770
JOB 18
JOB CLASS 2
LIBRARY 150
LOB 713
LOB PARTITION 1
MATERIALIZED VIEW 3
OPERATOR 57
PACKAGE 866
PACKAGE BODY 807
PROCEDURE 252
PROGRAM 12
QUEUE 27
RESOURCE PLAN 4
RULE 4
RULE SET 19
SCHEDULE 1
SEQUENCE 2759
SYNONYM 23307
TABLE 22631
TABLE PARTITION 625
TABLE SUBPARTITION 18
TRIGGER 565
TYPE 1941
TYPE BODY 174
UNDEFINED 6
VIEW 3804
WINDOW 2
WINDOW GROUP 1
XML SCHEMA 26

43 filas seleccionadas.

Cambié la version del optimizador y el resultado fue el esperado, salió ordenado. Miremos el nuevo plan:

rop@TEST10G> explain plan for
2 select object_type,count(1)
3 from t
4 group by object_type;

Explicado.

rop@TEST10G> select * from table(dbms_xplan.display);

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
Plan hash value: 3156910365

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 26 | 208 | 337 (6)| 00:00:05 |
| 1 | SORT GROUP BY | | 26 | 208 | 337 (6)| 00:00:05 |
| 2 | TABLE ACCESS FULL| T | 99843 | 780K| 322 (2)| 00:00:04 |
---------------------------------------------------------------------------

9 filas seleccionadas.

Ahora uso en antiguo path SORT GROUP BY y en consecuencia la salida no altero el orden
Por ultimo, agregamos el order by y vemos como se agrega (ahora explicitamente) un path para ordenar el resultado antes de retornarlo.

rop@TEST10G> ed
Escrito file afiedt.buf

1 explain plan for
2 select object_type,count(1)
3 from t
4 group by object_type
5* order by object_type
rop@TEST10G> /

Explicado.

rop@TEST10G> select * from table(dbms_xplan.display);

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
Plan hash value: 3861070257

----------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 26 | 208 | 354 (11)| 00:00:05 |
| 1 | SORT ORDER BY | | 26 | 208 | 354 (11)| 00:00:05 |
| 2 | HASH GROUP BY | | 26 | 208 | 354 (11)| 00:00:05 |
| 3 | TABLE ACCESS FULL| T | 99843 | 780K| 324 (2)| 00:00:04 |
----------------------------------------------------------------------------



Este es un caso interesante de cambio de comportamiento que saca a la luz problemas de mala programación, que en versiones anteriores pasaban desapercibidas. Por tal motivo es muy importante tener código de calidad ,y que no solo funcione, para evitar sorpresas a futuro. En la próxima nota les voy a mostrar otro caso de cambio interesante, relacionado con cambios en el orden de evaluación de predicados.

viernes, 21 de agosto de 2009

Instalación, Configuración y uso del paquete DBMS_PROFILER

En esta nota les voy a mostrar un poco acerca del paquete DBMS_PROFILER, que no es tan conocido, pero si se usa correctamente, puede ayudar a detectar en forma precisa en que lineas de código se encuentran las mayores demoras y asi focalizar la tarea de mejora de rendimiento de una aplicación o bloque de código PL/SQL.
El paquete DBMS_PROFILER fue introducido en 8i y permite al equipo de desarrollo obtener detalle y comportamiento del código PL/SQL. No existe por default y hay que correr dos scripts para instalarlo. Por medio de esta herramienta los desarrolladores podrán obtener con granularidad de línea de código el detalle de ejecuciones, tiempo total, tiempo máximo, tiempo mínimo, etc.


Instalación y Configuración

La instalación consta de 2 pasos: La etapa 1 debe ser ejecutada por un DBA conectado como sys. Esta etapa crea el paquete. La etapa 2 no requiere privilegios especiales y puede ser ejecutada por el dueño (owner) en donde esta definido el modulo de código a instrumentar.


Paso 1

Este paso crea el paquete DBMS_PROFILER en y realiza los grants y creación de sinónimo para poder ser utilizado desde cualquier usuario.

CONNECT sys/password@service AS SYSDBA
@$ORACLE_HOME/rdbms/admin/profload.sql



Paso 2

Este paso crea las 3 tablas y la secuencia necesarias para persistir la información recolectada por el DBMS_PROFILER.

CONNECT profiler/profiler@service
@$ORACLE_HOME/rdbms/admin/proftab.sql
GRANT SELECT ON plsql_profiler_runnumber TO PUBLIC;
GRANT SELECT, INSERT, UPDATE, DELETE ON plsql_profiler_data TO PUBLIC;
GRANT SELECT, INSERT, UPDATE, DELETE ON plsql_profiler_units TO PUBLIC;
GRANT SELECT, INSERT, UPDATE, DELETE ON plsql_profiler_runs TO PUBLIC;

Como Usar DBMS_PROFILER

Para activar el profiler se debe encerrar la porción de código a evaluar por las siguientes sentencias:

Begin
….
dbms_profiler.start_profiler();

dbms_profiler.stop_profiler;

End;


Una vez ejecutado el modulo a analizar se deberá consultar en las tablas PLSQL_xxx para obtener los resultados.

Ahora les voy a mostrar su uso con un ejemplo sencillo:

Creamos un procedimiento sencillo que recorre un cursor implícito y va contando las iteraciones.

create or replace procedure foo
is
l_cnt int := 0;
begin
dbms_profiler.start_profiler('Test'); -- Comienzo de traza
for i in (select * from all_objects)
loop
l_cnt := l_cnt+1;
end loop;
dbms_profiler.stop_profiler; -- Fin de traza
end;

Luego de ejecutarlo, verificamos los resultados con la siguiente consulta:


SELECT u.runid,
u.unit_type,
u.unit_name,
d.line#,
s.text,
d.total_occur,
d.total_time,
d.min_time,
d.max_time
FROM plsql_profiler_units u,
plsql_profiler_data d,
user_source s
WHERE u.runid = d.runid AND
u.unit_number = d.unit_number AND
u.unit_name = s.name AND
d.line# = s.line AND
u.runid = 2
ORDER BY u.unit_number, d.line#



La unidad de medida es en nanosegundos (10 elevado a la -9), para pasar a segundos hay que dividir por 1000 millones.
En base a los resultados obtenidos se ve que demoró casi 3s en obtener los datos de las filas de la tabla (linea 6) y 0,03s en ejecutar la asignación (linea 8).

viernes, 14 de agosto de 2009

Verificando el uso de "Buenas Practicas" en código PL/SQL

Para verificar si se aplican "Buenas Practicas" de programación y para contribuir a realizar código mas robusto y menos propenso a que se generen errores en tiempo de ejecución, a partir de 10g R1, se introdujo un nuevo mecanismo que permite advertir en tiempo de compilación sobre potenciales problemas (WARNINGS), que si bien dejan compilada la unidad de código, pueden darnos dolores de cabeza y conducir a que las aplicaciones que utilizan dicho código generen errores imprevistos o peor aún, que no se obtengan los datos correctos alterando la semántica pretendida y siendo, en muchas ocasiones, muy complicados de detectar.

Existe 4 categorias de WARNINGS:

SEVERE : Pueden causar acciones inesperadas, errores que hagan
cancelar una operatoria o resultados erroneos.

PERFORMANCE : Pueden causar problemas de rendimiento

INFORMATIONAL: No afectan el rendimiento ni altera los resultados pero
advierte sobre complicaciones en el mantenimiento del
codigo a futuro.

ALL : Contempla todos los casos anteriores.


Para activar los mensajes de warning se puede usar el parametro PLSQL_WARNINGS a nivel sesion o a nivel instancia (cosa que no recomiendo), tambien se puede usar el paquete DBMS_WARNING para setear el nivel de warning deseado a nivel de código PL en procedures, packages, triggers, etc. Consultando la vista [USER | ALL | DBA]_WARNING_SETTINGS se puede saber que objetos tienen activado el warning y consultando la vista [USER | ALL | DBA]_ERRORS, filtrando por el campo ATTRIBUTE='WARNING' se ven todos los warnings generados.

Ahora que ya hice una introduccion rapida al tema, vayamos a los ejemplos:

Habilito para detectar todas las categorias:

rop@DESA10G> alter session set plsql_warnings='ENABLE:ALL';

Sesión modificada.

Creo una tabla sencila

rop@DESA10G> create table t (x int,y varchar(5));

Tabla creada.

rop@DESA10G> insert into t
2 select rownum,
3 trunc(dbms_random.value(1,99999))
4 from dual
5 connect by rownum <= 100000;

100000 filas creadas.

rop@DESA10G> commit;

Confirmación terminada.

Voy a crear un procedimiento P_PRUEBA1 de forma tal de que se detecte un warning:


rop@DESA10G>ed
1 create or replace procedure p_prueba1 (p_val int)
2 is
3 l_cnt int;
4 begin
5 select count(1) into l_cnt
6 from t
7 where y = p_val;
8 if (l_cnt > 0) then
9 dbms_output.put_line ('El valor existe en la tabla');
10 else
11 dbms_output.put_line ('El valor NO existe en la tabla');
12 end if;
13* end;
rop@DESA10G> /

SP2-0804: Procedimiento creado con advertencias de compilación

rop@DESA10G> select text from user_errors where name = 'P_PRUEBA1';

TEXT
----------------------------------------------------------------------------------------------------
PLW-07204: puede que la conversión que no sea de tipo de columna dé como resultado un plan de consulta subóptimo


En el caso de arriba detectó un potencial problema de performance, ya que al comparar la columna "y" de tipo varchar2 con el parametro "p_val" de tipo number se hace una conversión implicita TO_NUMBER() de la columna "y". Oracle siempre pasa a number cuando se comparan los tipos number y char/varchar2.
Veamos otros ejemplos:

rop@DESA10G> ed
Escrito file afiedt.buf

1 create or replace procedure p_prueba2 (p_val int)
2 is
3 l_cnt int;
4 begin
5 select count(1) into l_cnt
6 from t
7 where y = to_char(p_val);
8 if ( 0 = 0) then
9 if (l_cnt > 0) then
10 dbms_output.put_line ('El valor existe en la tabla');
11 else
12 dbms_output.put_line ('El valor NO existe en la tabla');
13 end if;
14 else
15 null;
16 end if;
17* end;
rop@DESA10G> /

SP2-0804: Procedimiento creado con advertencias de compilación

rop@DESA10G> select text from user_errors where name = 'P_PRUEBA2';

TEXT
----------------------------------------------------------------------------------------------------
PLW-06002: Código inaccesible

Este es una advertencia informativa. Ahora voy a crear una función:


rop@DESA10G> ed
Escrito file afiedt.buf

1 create or replace function f_prueba1
2 return int
3 is
4 l_val int;
5 begin
6 l_val := dbms_random.value(1,10);
7* end;
rop@DESA10G> /

SP2-0806: Función creada con advertencias de compilación

rop@DESA10G> select text from user_errors where name = 'F_PRUEBA1';

TEXT
----------------------------------------------------------------------------------------------------
PLW-05005: la función F_PRUEBA1 se devuelve sin valor en la línea 7

al no retornar valor se podria generar un problema mas grave


rop@DESA10G> ed
Escrito file afiedt.buf

1 create or replace procedure p_prueba3 (p_val varchar2)
2 is
3 begin
4 insert into t (x) values (p_val);
5* end;
rop@DESA10G> /

SP2-0804: Procedimiento creado con advertencias de compilación

rop@DESA10G> select text from user_errors where name = 'P_PRUEBA3';

TEXT
----------------------------------------------------------------------------------------------------
PLW-07202: el tipo de enlace daría como resultado una conversión lejos del tipo de columna

rop@DESA10G>


El warning para el procedimiento P_PRUEBA3, aunque la traducción al español no es muy clara, da un posible problema en la conversión de tipos. Asi podriamos seguir probando otros tantos casos.
La idea fue mostrarles que con esta herramienta se puede mejorar la calidad del software pl/sql y detectar en forma automatica y proactiva posibles problemas en tiempo de ejecución.