viernes, 27 de marzo de 2009

Pivot y Unpivot para reportes en forma de matriz

Oracle almacena los datos en tablas en un formato tabular, es decir, columna y valor. Para ciertos reportes resulta mas intuitivo mostrar los resultados en un formato de doble entrada o matriz. Antes de 11g era necesario usar una combinación entre una funcion de agregacion y decode para lograr mostrar el resultado en formato de matriz. Desde 11g existen dos operadores nuevos llamados PIVOT y su inversa UNPIVOT. Voy a mostrar un ejemplo usando el esquema de ejemplo SH.

Para mostrar un reporte de la cantidad de ventas por canal y pais habia que hacer el siguiente query:

SQL> conn sh/sh@rop111
Connected.
SQL> select co.country_name,
2 sum(decode(ch.channel_desc,'Direct Sales',1,0)) "Direct Sales",
3 sum(decode(ch.channel_desc,'Tele Sales',1,0)) "Tele Sales",
4 sum(decode(ch.channel_desc,'Catalog',1,0)) "Catalog",
5 sum(decode(ch.channel_desc,'Internet',1,0)) "Internet",
6 sum(decode(ch.channel_desc,'Partners',1,0)) "Partners"
7 from sales sa,
8 customers cu,
9 countries co,
10 channels ch
11 where sa.channel_id = ch.channel_id
12 and sa.cust_id = cu.cust_id
13 and cu.country_id = co.country_id
14 group by co.country_name
15 order by co.country_name
16 ;


COUNTRY_NAME Direct Sales Tele Sales Catalog Internet Partners
---------------------------------------- ------------ ---------- ---------- ---------- ----------
Argentina 126 51 0 23 2
Australia 20169 5 0 4364 9147
Brazil 75 46 0 27 32
Canada 11474 6 0 3601 7777
China 2 9 0 7 1
Denmark 9870 10 0 2084 4687
France 21807 5 0 3577 7689
Germany 53864 4 0 9426 18684
Italy 26810 69 0 5193 10498
Japan 31895 1 0 8715 19572
New Zealand 0 7 0 0 0
Poland 4 3 0 9 2
Saudi Arabia 1 0 0 6 0
Singapore 11553 8 0 4198 9494
Spain 10320 0 0 2207 4609
Turkey 154 0 0 3 11
United Kingdom 37786 5 0 6390 14457
United States of America 304418 1845 0 68586 151363

18 rows selected.

La consulta es poco clara, con pivot es mucho mas fácil:


SQL> select * from
2 (select co.country_name,
3 ch.channel_desc
4 from sales sa,
5 customers cu,
6 countries co,
7 channels ch
8 where sa.channel_id = ch.channel_id
9 and sa.cust_id = cu.cust_id
10 and cu.country_id = co.country_id )
11 pivot
12 ( count(1)
13 for channel_desc in ('Direct Sales','Tele Sales','Catalog','Internet','Partners')
14 )
15 order by country_name;

COUNTRY_NAME 'Direct Sales' 'Tele Sales' 'Catalog' 'Internet' 'Partners'
---------------------------------------- -------------- ------------ ---------- ---------- ---------
Argentina 126 51 0 23 2
Australia 20169 5 0 4364 9147
Brazil 75 46 0 27 32
Canada 11474 6 0 3601 7777
China 2 9 0 7 1
Denmark 9870 10 0 2084 4687
France 21807 5 0 3577 7689
Germany 53864 4 0 9426 18684
Italy 26810 69 0 5193 10498
Japan 31895 1 0 8715 19572
New Zealand 0 7 0 0 0
Poland 4 3 0 9 2
Saudi Arabia 1 0 0 6 0
Singapore 11553 8 0 4198 9494
Spain 10320 0 0 2207 4609
Turkey 154 0 0 3 11
United Kingdom 37786 5 0 6390 14457
United States of America 304418 1845 0 68586 151363

18 rows selected.

El resultado es el mismo pero la consulta es mas intuitiva y mas sencilla de construir.

Ahora, si quisiera pasar de formato matriz a tabular uso la función opuesta UNPIVOT:

SQLPLUS>
create table t_matriz
as
select * from
(select co.country_name,
ch.channel_desc
from sales sa,
customers cu,
countries co,
channels ch
where sa.channel_id = ch.channel_id
and sa.cust_id = cu.cust_id
and cu.country_id = co.country_id )
pivot
( count(1)
for channel_desc in ('Direct Sales','Tele Sales','Catalog','Internet','Partners')
)
order by country_name

Table created.



SQL> desc t_matriz
Name Null? Type
----------------------------------------------------------------- -------- ------------------------
COUNTRY_NAME NOT NULL VARCHAR2(40)
'Direct Sales' NUMBER
'Tele Sales' NUMBER
'Catalog' NUMBER
'Internet' NUMBER
'Partners' NUMBER



1 select * from t_matriz
2 unpivot
3 ( cant_ventas for pais in ("'Direct Sales'","'Tele Sales'","'Catalog'","'Internet'","'Partners'
4* )
SQL> /

COUNTRY_NAME PAIS CANT_VENTAS
---------------------------------------- -------------- -----------
Argentina 'Direct Sales' 126
Argentina 'Tele Sales' 51
Argentina 'Catalog' 0
Argentina 'Internet' 23
Argentina 'Partners' 2
Australia 'Direct Sales' 20169
Australia 'Tele Sales' 5
Australia 'Catalog' 0
Australia 'Internet' 4364
Australia 'Partners' 9147
Brazil 'Direct Sales' 75
Brazil 'Tele Sales' 46
Brazil 'Catalog' 0
Brazil 'Internet' 27
Brazil 'Partners' 32
Canada 'Direct Sales' 11474
Canada 'Tele Sales' 6
Canada 'Catalog' 0
Canada 'Internet' 3601
Canada 'Partners' 7777
China 'Direct Sales' 2
China 'Tele Sales' 9
China 'Catalog' 0
China 'Internet' 7
China 'Partners' 1
Denmark 'Direct Sales' 9870
Denmark 'Tele Sales' 10
Denmark 'Catalog' 0
Denmark 'Internet' 2084
Denmark 'Partners' 4687
France 'Direct Sales' 21807
France 'Tele Sales' 5
France 'Catalog' 0
France 'Internet' 3577
France 'Partners' 7689
Germany 'Direct Sales' 53864
Germany 'Tele Sales' 4
Germany 'Catalog' 0
Germany 'Internet' 9426
Germany 'Partners' 18684
Italy 'Direct Sales' 26810
Italy 'Tele Sales' 69
Italy 'Catalog' 0
Italy 'Internet' 5193
Italy 'Partners' 10498
Japan 'Direct Sales' 31895
Japan 'Tele Sales' 1
Japan 'Catalog' 0
Japan 'Internet' 8715
Japan 'Partners' 19572
New Zealand 'Direct Sales' 0
New Zealand 'Tele Sales' 7
New Zealand 'Catalog' 0
New Zealand 'Internet' 0
New Zealand 'Partners' 0
Poland 'Direct Sales' 4
Poland 'Tele Sales' 3
Poland 'Catalog' 0
Poland 'Internet' 9
Poland 'Partners' 2
Saudi Arabia 'Direct Sales' 1
Saudi Arabia 'Tele Sales' 0
Saudi Arabia 'Catalog' 0
Saudi Arabia 'Internet' 6
Saudi Arabia 'Partners' 0
Singapore 'Direct Sales' 11553
Singapore 'Tele Sales' 8
Singapore 'Catalog' 0
Singapore 'Internet' 4198
Singapore 'Partners' 9494
Spain 'Direct Sales' 10320
Spain 'Tele Sales' 0
Spain 'Catalog' 0
Spain 'Internet' 2207
Spain 'Partners' 4609
Turkey 'Direct Sales' 154
Turkey 'Tele Sales' 0
Turkey 'Catalog' 0
Turkey 'Internet' 3
Turkey 'Partners' 11
United Kingdom 'Direct Sales' 37786
United Kingdom 'Tele Sales' 5
United Kingdom 'Catalog' 0
United Kingdom 'Internet' 6390
United Kingdom 'Partners' 14457
United States of America 'Direct Sales' 304418
United States of America 'Tele Sales' 1845
United States of America 'Catalog' 0
United States of America 'Internet' 68586
United States of America 'Partners' 151363



SQL> ed
Wrote file afiedt.buf

1 create table t_tabular
2 as
3 select * from t_matriz
4 unpivot
5 ( cant_ventas for pais in ("'Direct Sales'","'Tele Sales'","'Catalog'","'Internet'","'Partners'
6* )
SQL> /

Table created.

SQL> desc t_tabular
Name Null? Type
----------------------------------------------------------------- -------- ------------------------
COUNTRY_NAME VARCHAR2(40)
PAIS VARCHAR2(14)
CANT_VENTAS NUMBER

SQL>

Como mostré en el ejemplo la creación de sentencias para mostrar la información en formato matricial (muy utilizada en reportes de DataWarehouse) ahora es muy simple y sobre todo mas entendible

jueves, 19 de marzo de 2009

SQL Baselines, la evolución de stored outlines

No es raro que una aplicación, que estuvo funcionando perfectamente durante mucho tiempo de repente comience a "arrastrarse", causando pánico generalizado. Los telefonos del área de soporte comienzan a sonar sin parar y mientras el problema persista el incidente ira escalando y seguramente en cuestión de minutos tendremos a nuestro jefe detrás nuestro preguntandonos cada 1 minuto que está pasando. Es bastante habitual que esa degradación drástica de los aplicativos se deba a solo una sentencia, si.. una sola sentencia central puede hacer desastres!. Afortunadamente, las versiones mas modernas de Oracle proveen a los dba's con herramientas de diagnóstico que permiten encontrar el foco del problema en forma rápida y precisa.
Ahora, por que una sentencia comienza a funcionar mal de un día para el otro?. La respuesta generalemte es que el CBO (optimizador por costos) cambió el plan para ejecutar dicha sentencia y el plan resultó ser poco performante. Este tipo de problemas fue el principal motivo por el cual muchas empresas demoraron años en dejar el RBO (optimizador por reglas) y comenzaron a usar CBO en sus sistemas productivos. Una de las formas de convencer a los gerentes de sistemas para implementar CBO era asegurarles que las consultas principales de las aplicaciones iban a mantener los mismos planes en el tiempo, lo cuál no suena muy lógico, ya que si cambia la cardinalidad, distribucion, selectividad, etc, no se le dará la oportunidad a Oracle a evaluar un mejor plan, pero se les garantizaba un funcionamiento mas estable. Una de las formas de fijar los planes para ciertas sentencias es usando outlines, pero desde 11g existe un mecanismo mas inteligente para evitar cambios no deseados en los planes. Ese nuevo feature se llama SQL Plan Baselines.
Por default, 11g tiene habilitado el manejo de SQL Baselines, lo cual permite comparar el plan de cada sentencia parseada con un historico de planes, previamente capturados. El historial de planes se denomina "SQL plan history" en donde se pueden encontrar distintos planes para cada sentencia con los siguiente información entre otras:

ACCEPTED: Si el plan se considera aceptable y con posibilidades de ejecutarse.
ENABLED : Si el plan esta activo y disponible
FIXED : Si el plan se usará en forma permanente para la sentencia

Cuando se evalua el plan para una sentencia se compara el plan con el "SQL Baselines" que son los planes del historial aceptados, y si ya existe se toma el plan almacenado, si el plan no existe se guarda en el historial, sin aceptar para ser analizado posteriormente. Esto asegura que no cambie el plan repentinamente.
Los planes del historial no aceptados se pueden "evolucionar" si resultan ser mejores que el mejor plan aceptado.

Ahora voy a mostrarles algunas pruebas para entender mejor como funciona todo esto,

Para poder trabajar con un caso sencillo pero representativo voy a crear y llenar una tabla T con un poco mas de 2M de registros y un indice sobre la columna object_id:

SQL> create table t as select * from dba_objects;

Table created.

SQL> insert into t select * from t;

68864 rows created.

SQL> insert into t select * from t;

137728 rows created.

SQL> /

275456 rows created.

SQL> /

550912 rows created.

SQL> /

1101824 rows created.

SQL> /

2203648 rows created.

SQL> create index t_idx on t(object_id);

Index Created.

Ahora voy poner en 1 el object_id de casí todas las filas, de forma tal de hacer una distribución ideal para full scan para la prueba.

SQL> update t set object_id=1 where rownum<=2000000; 2000000 rows updated. SQL> commit;

Commit complete.

SQL>

Antes de comenzar a ver los planes voy a activar la captura a nivel sesion, para ir guardando los planes en el "Sql Historial":

SQL> alter session set optimizer_capture_sql_plan_baselines=true;

Session altered.

La consulta que voy a usar para la prueba es la siguiente:

SQL> select count(unique object_name)
2 from t
3 where object_id=1;

Execution Plan
----------------------------------------------------------
Plan hash value: 162046943

--------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 79 | 4932 (1)| 00:01:00 |
| 1 | SORT GROUP BY | | 1 | 79 | | |
| 2 | TABLE ACCESS BY INDEX ROWID| T | 2223K| 167M| 4932 (1)| 00:01:00 |
|* 3 | INDEX RANGE SCAN | T_IDX | 2223K| | 4180 (1)| 00:00:51 |
--------------------------------------------------------------------------------------

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

3 - access("OBJECT_ID"=1)

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

SQL>

Cono la tabla no tiene estadísticas se activa el dynamic_sampling (nivel 2 default) pero a pesar de eso no encuentra el plan ideal (usando full scan)
Para lograr que el CBO se de cuenta que le conviene ir por full scan en lugar de por indice, ya que casi todas las filas estan involucradas, recolectamos estadísticas y volvemos a ejecutar la consulta:

begin
dbms_stats.gather_table_stats (
ownname => user,
tabname => 'T',
cascade => TRUE,
method_opt => 'FOR ALL INDEXED COLUMNS SIZE AUTO');
end;

SQL> select count(unique object_name)
2 from t
3 where object_id=1;

Execution Plan
----------------------------------------------------------
Plan hash value: 1476560607

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 70 | 9030 (1)| 00:01:49 |
| 1 | SORT GROUP BY | | 1 | 70 | | |
|* 2 | TABLE ACCESS FULL| T | 2004K| 133M| 9030 (1)| 00:01:49 |
---------------------------------------------------------------------------

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

2 - filter("OBJECT_ID"=1)

Esta vez el optimizador,basandose en la info de distribución de la columna object_id obtenida de la info estadistica, se dió cuenta de que le conviene acceder por full scan.
Voy a usar el procedimiento print_table para mostrar en forma vertical los planes capturados para la sentencia de test. Uso la vista DBA_SQL_PLAN_BASELINES:

SQL> exec print_table('select * from dba_sql_plan_baselines where sql_handle=''SYS_SQL_aa67186957099
2f5''');
SIGNATURE : 12278809749759824629
SQL_HANDLE : SYS_SQL_aa671869570992f5
SQL_TEXT : select count(unique object_name)
from t
where object_id=1
PLAN_NAME : SYS_SQL_PLAN_570992f54b85249e
CREATOR : ROP
ORIGIN : AUTO-CAPTURE
PARSING_SCHEMA_NAME : ROP
DESCRIPTION :
VERSION : 11.1.0.6.0
CREATED : 19-MAR-09 11.00.26.000000 AM
LAST_MODIFIED : 19-MAR-09 11.00.26.000000 AM
LAST_EXECUTED :
LAST_VERIFIED :
ENABLED : YES
ACCEPTED : NO
FIXED : NO
AUTOPURGE : YES
OPTIMIZER_COST : 9029
MODULE : SQL*Plus
ACTION :
EXECUTIONS : 0
ELAPSED_TIME : 0
CPU_TIME : 0
BUFFER_GETS : 0
DISK_READS : 0
DIRECT_WRITES : 0
ROWS_PROCESSED : 0
FETCHES : 0
END_OF_FETCH_COUNT : 0
-----------------
SIGNATURE : 12278809749759824629
SQL_HANDLE : SYS_SQL_aa671869570992f5
SQL_TEXT : select count(unique object_name)
from t
where object_id=1
PLAN_NAME : SYS_SQL_PLAN_570992f5c8551679
CREATOR : ROP
ORIGIN : AUTO-CAPTURE
PARSING_SCHEMA_NAME : ROP
DESCRIPTION :
VERSION : 11.1.0.6.0
CREATED : 19-MAR-09 10.46.01.000000 AM
LAST_MODIFIED : 19-MAR-09 10.46.01.000000 AM
LAST_EXECUTED : 19-MAR-09 10.48.03.000000 AM
LAST_VERIFIED :
ENABLED : YES
ACCEPTED : YES
FIXED : NO
AUTOPURGE : YES
OPTIMIZER_COST : 4931
MODULE : SQL*Plus
ACTION :
EXECUTIONS : 0
ELAPSED_TIME : 0
CPU_TIME : 0
BUFFER_GETS : 0
DISK_READS : 0
DIRECT_WRITES : 0
ROWS_PROCESSED : 0
FETCHES : 0
END_OF_FETCH_COUNT : 0
-----------------
PL/SQL procedure successfully completed.

Vemos que para el sql_handler SYS_SQL_aa671869570992f5 hay dos planes posibles, uno se aceptó (el primero, que es el que accede por indice) y el segundo, que ya cuenta con estadisticas armó otro plan y se almacenó en el historico pero sin aceptar.
Voy a ejecutar la misma consulta de siempre para ver que plan toma:

SQL> select count(unique object_name)
2 from t
3 where object_id=1;

COUNT(UNIQUEOBJECT_NAME)
------------------------
41640


Execution Plan
----------------------------------------------------------
Plan hash value: 162046943

--------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 79 | 4932 (1)| 00:01:00 |
| 1 | SORT GROUP BY | | 1 | 79 | | |
| 2 | TABLE ACCESS BY INDEX ROWID| T | 2223K| 167M| 4932 (1)| 00:01:00 |
|* 3 | INDEX RANGE SCAN | T_IDX | 2223K| | 4180 (1)| 00:00:51 |
--------------------------------------------------------------------------------------

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

3 - access("OBJECT_ID"=1)

Note
-----
- dynamic sampling used for this statement
- SQL plan baseline "SYS_SQL_PLAN_570992f5c8551679" used for this statement


Statistics
----------------------------------------------------------
54 recursive calls
43 db block gets
33312 consistent gets
36344 physical reads
5996 redo size
429 bytes sent via SQL*Net to client
381 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
1 sorts (memory)
0 sorts (disk)
1 rows processed

Que pasaría si cambiamos el valor default del parametro optimizer_use_sql_plan_baselines para que no tomé mas las SQL Baselines:

SQL> alter session set optimizer_use_sql_plan_baselines =false;

Session altered.

SQL> set autotr off
SQL> set autotr on
SQL> select count(unique object_name)
2 from t
3 where object_id=1;

COUNT(UNIQUEOBJECT_NAME)
------------------------
41640


Execution Plan
----------------------------------------------------------
Plan hash value: 1476560607

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 70 | 9030 (1)| 00:01:49 |
| 1 | SORT GROUP BY | | 1 | 70 | | |
|* 2 | TABLE ACCESS FULL| T | 1995K| 133M| 9030 (1)| 00:01:49 |
---------------------------------------------------------------------------

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

2 - filter("OBJECT_ID"=1)


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

Tal como esperabamos, ahora accedió por full scan. Como sabemos que el plan correcto es el que accede por full scan, usando el paquete dbms_spm analizamos la performance y promovemos el plan con costo mayor pero con acceso full de forma tal de que sea considerado en la proxima ejecución:

SQL> select dbms_spm.evolve_sql_plan_baseline(sql_handle => 'SYS_SQL_aa671869570992f5') from dual;

DBMS_SPM.EVOLVE_SQL_PLAN_BASELINE(SQL_HANDLE=>'SYS_SQL_AA671869570992F5')
--------------------------------------------------------------------------------

-------------------------------------------------------------------------------
Evolve SQL Plan Baseline Report
-------------------------------------------------------------------------------

Inputs:
-------
SQL_HANDLE = SYS_SQL_aa671869570992f5
PLAN_NAME =
TIME_LIMIT = DBMS_SPM.AUTO_LIMIT
VERIFY = YES

DBMS_SPM.EVOLVE_SQL_PLAN_BASELINE(SQL_HANDLE=>'SYS_SQL_AA671869570992F5')
--------------------------------------------------------------------------------
COMMIT = YES

Plan: SYS_SQL_PLAN_570992f54b85249e
-----------------------------------
Plan was verified: Time used 45.079 seconds.
Failed performance criterion: Compound improvement ratio <= 1.03. Baseline Plan Test Plan Improv. Ratio ------------- --------- ------------- Execution Status: COMPLETE COMPLETE Rows Processed: 1 1 DBMS_SPM.EVOLVE_SQL_PLAN_BASELINE(SQL_HANDLE=>'SYS_SQL_AA671869570992F5')
--------------------------------------------------------------------------------
Elapsed Time(ms): 20750 8597 2.41
CPU Time(ms): 6718 3656 1.84
Buffer Gets: 33203 32444 1.02

Mirando la comparativa se observa que el ratio de mejora con el plan full scan y es 2.41 veces mejor. Que habrá pasado con el historial SQL?:

SQL> exec print_table('select * from dba_sql_plan_baselines where sql_handle=''SYS_SQL_aa67186957099
2f5''');
SIGNATURE : 12278809749759824629
SQL_HANDLE : SYS_SQL_aa671869570992f5
SQL_TEXT : select count(unique object_name)
from t
where object_id=1
PLAN_NAME : SYS_SQL_PLAN_570992f54b85249e
CREATOR : ROP
ORIGIN : AUTO-CAPTURE
PARSING_SCHEMA_NAME : ROP
DESCRIPTION :
VERSION : 11.1.0.6.0
CREATED : 19-MAR-09 11.00.26.000000 AM
LAST_MODIFIED : 19-MAR-09 11.09.14.000000 AM
LAST_EXECUTED :
LAST_VERIFIED :
ENABLED : YES
ACCEPTED : YES
FIXED : NO
AUTOPURGE : YES
OPTIMIZER_COST : 9029
MODULE : SQL*Plus
ACTION :
EXECUTIONS : 0
ELAPSED_TIME : 0
CPU_TIME : 0
BUFFER_GETS : 0
DISK_READS : 0
DIRECT_WRITES : 0
ROWS_PROCESSED : 0
FETCHES : 0
END_OF_FETCH_COUNT : 0
-----------------
SIGNATURE : 12278809749759824629
SQL_HANDLE : SYS_SQL_aa671869570992f5
SQL_TEXT : select count(unique object_name)
from t
where object_id=1
PLAN_NAME : SYS_SQL_PLAN_570992f5c8551679
CREATOR : ROP
ORIGIN : AUTO-CAPTURE
PARSING_SCHEMA_NAME : ROP
DESCRIPTION :
VERSION : 11.1.0.6.0
CREATED : 19-MAR-09 10.46.01.000000 AM
LAST_MODIFIED : 19-MAR-09 10.46.01.000000 AM
LAST_EXECUTED : 19-MAR-09 10.48.03.000000 AM
LAST_VERIFIED :
ENABLED : YES
ACCEPTED : YES
FIXED : NO
AUTOPURGE : YES
OPTIMIZER_COST : 4931
MODULE : SQL*Plus
ACTION :
EXECUTIONS : 0
ELAPSED_TIME : 0
CPU_TIME : 0
BUFFER_GETS : 0
DISK_READS : 0
DIRECT_WRITES : 0
ROWS_PROCESSED : 0
FETCHES : 0
END_OF_FETCH_COUNT : 0
-----------------

PL/SQL procedure successfully completed.

Ahora el nuevo plan fue aceptado y paso a ser parte del SQL Baselines. Probamos de ejecutar la sentencia de prueba para ver que pasa

SQL> select count(unique object_name)
2 from t
3 where object_id=1;

COUNT(UNIQUEOBJECT_NAME)
------------------------
41640


Execution Plan
----------------------------------------------------------
Plan hash value: 1476560607

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 70 | 9030 (1)| 00:01:49 |
| 1 | SORT GROUP BY | | 1 | 70 | | |
|* 2 | TABLE ACCESS FULL| T | 1995K| 133M| 9030 (1)| 00:01:49 |
---------------------------------------------------------------------------

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

2 - filter("OBJECT_ID"=1)

Note
-----
- SQL plan baseline "SYS_SQL_PLAN_570992f54b85249e" used for this statement


Statistics
----------------------------------------------------------
13 recursive calls
15 db block gets
32444 consistent gets
32425 physical reads
5592 redo size
429 bytes sent via SQL*Net to client
381 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
1 sorts (memory)
0 sorts (disk)

Escogió el nuevo plan "SYS_SQL_PLAN_570992f54b85249e" que había sido evolucionado recientemente ya que según su evaluación es superior al que estaba usando.
La próxima prueba será cambiar la distribución de la columna object_id de forma tal de cambiar todos las columnas menos una con valor 2 en lugar de 1. Con dicha selectividad deberia usar el indice.

SQL> update t
2 set object_id=2
3 where object_id=1
and rownum <= 1999999; 1999999 rows updated. SQL> commit;
Commit complete.

SQL> select count(unique object_name)
2 from t
3 where object_id=1;

Execution Plan
----------------------------------------------------------
Plan hash value: 1476560607

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 70 | 9030 (1)| 00:01:49 |
| 1 | SORT GROUP BY | | 1 | 70 | | |
|* 2 | TABLE ACCESS FULL| T | 1995K| 133M| 9030 (1)| 00:01:49 |
---------------------------------------------------------------------------

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

2 - filter("OBJECT_ID"=1)

Note
-----
- SQL plan baseline "SYS_SQL_PLAN_570992f54b85249e" used for this statement

Siguio utilizando el plan por full scan, por que?, el problema fue que no se afectó el plan ya que no estan actualizadas las estadisticas, veamos que paso cuando actualizamos:

SQL> begin
2 dbms_stats.gather_table_stats (
3 ownname => user,
4 tabname => 'T',
5 cascade => TRUE,
6 method_opt => 'FOR ALL INDEXED COLUMNS SIZE AUTO');
7 end;
8 /

PL/SQL procedure successfully completed.

SQL> select count(unique object_name)
2 from t
3 where object_id=1;

Execution Plan
----------------------------------------------------------
Plan hash value: 162046943

--------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 70 | 4 (0)| 00:00:01 |
| 1 | SORT GROUP BY | | 1 | 70 | | |
| 2 | TABLE ACCESS BY INDEX ROWID| T | 3 | 210 | 4 (0)| 00:00:01 |
|* 3 | INDEX RANGE SCAN | T_IDX | 3 | | 3 (0)| 00:00:01 |
--------------------------------------------------------------------------------------

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

3 - access("OBJECT_ID"=1)

Note
-----
- SQL plan baseline "SYS_SQL_PLAN_570992f5c8551679" used for this statement

SQL>

Tomó el otro plan en SQL baselines ya que cdo generó el plan y lo comparó con el SQL Baselines pudo matchear con un plan existente.
El último test consiste en simular el comportamiento de stored outlines (ver la nota: "Usando Stored Outlines") para mas información. Con lo cual vamos a fijar un plan, es decir, le decimos al CBO que escoja dicho plan si o si.

SQL> declare
2 l_plan int;
3 begin
4 l_plan := dbms_spm.alter_sql_plan_baseline(sql_handle => 'SYS_SQL_aa671869570992f5',
5 plan_name => 'SYS_SQL_PLAN_570992f54b85249e',
6 attribute_name => 'fixed',
7 attribute_value => 'YES');
8 end;
9 /

PL/SQL procedure successfully completed.

SQL> exec print_table('select * from dba_sql_plan_baselines where sql_handle=''SYS_SQL_aa67186957099
2f5''');

PL/SQL procedure successfully completed.

SQL> set serverout on
SQL> exec print_table('select * from dba_sql_plan_baselines where sql_handle=''SYS_SQL_aa67186957099
2f5''');
SIGNATURE : 12278809749759824629
SQL_HANDLE : SYS_SQL_aa671869570992f5
SQL_TEXT : select count(unique object_name)
from t
where object_id=1
PLAN_NAME : SYS_SQL_PLAN_570992f54b85249e
CREATOR : ROP
ORIGIN : AUTO-CAPTURE
PARSING_SCHEMA_NAME : ROP
DESCRIPTION :
VERSION : 11.1.0.6.0
CREATED : 19-MAR-09 11.00.26.000000 AM
LAST_MODIFIED : 19-MAR-09 02.02.06.000000 PM
LAST_EXECUTED : 19-MAR-09 11.11.19.000000 AM
LAST_VERIFIED :
ENABLED : YES
ACCEPTED : YES
FIXED : YES
AUTOPURGE : YES
OPTIMIZER_COST : 9029
MODULE : SQL*Plus
ACTION :
EXECUTIONS : 0
ELAPSED_TIME : 0
CPU_TIME : 0
BUFFER_GETS : 0
DISK_READS : 0
DIRECT_WRITES : 0
ROWS_PROCESSED : 0
FETCHES : 0
END_OF_FETCH_COUNT : 0
-----------------
SIGNATURE : 12278809749759824629
SQL_HANDLE : SYS_SQL_aa671869570992f5
SQL_TEXT : select count(unique object_name)
from t
where object_id=1
PLAN_NAME : SYS_SQL_PLAN_570992f5c8551679
CREATOR : ROP
ORIGIN : AUTO-CAPTURE
PARSING_SCHEMA_NAME : ROP
DESCRIPTION :
VERSION : 11.1.0.6.0
CREATED : 19-MAR-09 10.46.01.000000 AM
LAST_MODIFIED : 19-MAR-09 10.46.01.000000 AM
LAST_EXECUTED : 19-MAR-09 10.48.03.000000 AM
LAST_VERIFIED :
ENABLED : YES
ACCEPTED : YES
FIXED : NO
AUTOPURGE : YES
OPTIMIZER_COST : 4931
MODULE : SQL*Plus
ACTION :
EXECUTIONS : 0
ELAPSED_TIME : 0
CPU_TIME : 0
BUFFER_GETS : 0
DISK_READS : 0
DIRECT_WRITES : 0
ROWS_PROCESSED : 0
FETCHES : 0
END_OF_FETCH_COUNT : 0
-----------------

PL/SQL procedure successfully completed.

Como vimos mas arriba, antes de fijar el plan, se escogía el plan que accede por el índice, veamos que pasa ahora despues de fijar el plan por full scan:

SQL> select count(unique object_name)
2 from t
3 where object_id=1;

Execution Plan
----------------------------------------------------------
Plan hash value: 1476560607

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 70 | 9030 (1)| 00:01:49 |
| 1 | SORT GROUP BY | | 1 | 70 | | |
|* 2 | TABLE ACCESS FULL| T | 3 | 210 | 9030 (1)| 00:01:49 |
---------------------------------------------------------------------------

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

2 - filter("OBJECT_ID"=1)

Note
-----
- SQL plan baseline "SYS_SQL_PLAN_570992f54b85249e" used for this statement

Se uso el plan fijado.

Como vimos, resulta interensante implementar este nuevo feature ya que tener un mecanismo inteligente de control de planes nos asegura mayor estabilidad de las consultas, y nos permite evaluar mejoras y promover los planes mas adecuados. Muchas veces los mismos desarrolladores conocen la mejor forma de acceder y podrían fomentar dicho acceso agregando baselines adecuados. En comparación con los stored outlines el funcionamiento de SQL Baselines es mucho mas flexible y dinamico. Una oportunidad para probar SQL Baselines es en las pruebas de upgrade de BD. Se puede capturar las sentencias de forma tal de armar un historial completo y luego exportar e importar las baselines (tal como se exportan e importan las estadisticas) en la base actualizada para asi hacer mas controlado el cambio de version. Luego los dba's iran viendo las consultas criticas en particular y promoviendo mejoras graduales para aprovechar la nueva versión de Oracle. También se podrían usar en los deployment de nuevas aplicaciones. Cada nueva app tiene un conjunto propio de sentencias que se podrían evaluar y luego generar baselines sobre ambientes de test o QA para que en momento de la puesta en producción, con los baselines previamente importados, garanticemos un funcionamiento similar a los de las pruebas y posteriormente, en forma gradua, ir viendo de evolucionar las consultas que tengan mejores planes sobre nuevo ambiente.

Ref 1: Performance & Tuning Guide (11g)
Ref 2: Baselines and Better Plans

viernes, 13 de marzo de 2009

Dynamic Sampling en acción

El muestreo dinámico, introducido por primera vez en 9i R2, es un mecanismo que permite "ayudar" al optimizador por costos (CBO) a obtener un mejor plan de acceso a los datos, recordemos que CBO es el unico soportado desde 10g. El CBO usa información tal como: cardinalidad, selectividad, constraints definidas, velocidad de transferencia i/o, latencia, velocidad de cpu, etc. Es evidente que cuanto mas información pueda recolectar mejor será el plan que arme. Una de las métricas mas importantes es la cardinalidad, que nos da la cantidad de filas estimadas que devolverá nuestra consulta. Si la cardinalidad estimada es cercana a la real podemos asegurar que nuestra consulta funcionará bien.
Un problema recurrente que salió a la luz con el CBO (desde sus primeras versiones en Oracle 7.1.xx) fue y es la falta o desactualización de estadísticas en los segmentos involucrados en las sentencias parseadas, convengamos que CBO basa todo su análisis en información estadística (nro de filas, tamaño promedio de filas, clustering_factor de un índice, cantidad de hojas de un indice,etc). Para minimizar este problema se agregó un mecanismo denominado "dynamic sampling" que tiene 11 niveles posibles (desde 0 al 10, ver: Dynamic Sampling Levels) y que permite, tal como su nombre lo indica, realizar en el momento del hard parse un muestreo para las tablas que no posean estadísticas. En 9i R2 esta configurado con nivel 1 y en 10g con nivel 2, posiblemente en versiones futuras tome un valor aún mayor. Este nuevo parámetro resulta muy util para las tablas temporales ya que sobre este tipo de tablas no se pueden recolectar estadísticas (si se les puede setear con dbms_stats.set_table_stats).

Los dos parámetros relativos al muestreo dinámico son:

OPTIMIZER_DYNAMIC_SAMPLING:
se utiliza cuando se quiere setear a nivel de instancia o a nivel de sesion.
DYNAMIC_SAMPLING :
se usa como hint en la sentencias.


Ahora voy a mostrarles algunas pruebas y conclusiones, utilizando el hint DYNAMIC_SAMPLING para "jugar" con los distintos niveles y ver como es estimada la cardinalidad en cada caso

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

Tabla creada.

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

COUNT(1)
----------
102002

La tabla tiene 102002 filas en total.
Voy a activar el modo trace en el sqlplus para ver los planes:

rop@DESA10G>set autotr trace exp

Ahora realizo una consulta sin filtro con dynamic sampling en 0 sobre la tabla T sin estadísticas, de forma tal de simular el comportamiento en versiones inferiores a 9i R2:

rop@DESA10G>select /*+ dynamic_sampling(t 0) */ * from t

Plan de Ejecución
----------------------------------------------------------
Plan hash value: 2153619298

--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 120K| 20M| 333 (1)| 00:00:08 |
| 1 | TABLE ACCESS FULL| T | 120K| 20M| 333 (1)| 00:00:08 |
--------------------------------------------------------------------------

La estimación fue de 120 mil contra los 102 mil reales. Pruebo como se comportaría en 9i R2, ya que el nivel default es 1:

rop@DESA10G> select /*+ dynamic_sampling(t 1) */ * from t;

Plan de Ejecución
----------------------------------------------------------
Plan hash value: 2153619298

--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 72365 | 12M| 332 (1)| 00:00:08 |
| 1 | TABLE ACCESS FULL| T | 72365 | 12M| 332 (1)| 00:00:08 |
--------------------------------------------------------------------------

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

La estimación cambió bastante, ahora es menor a la real y dió alrededor de 72 mil filas

Ahora pruebo con el valor default del dynamic_sampling en 10g:

rop@DESA10G> select /*+ dynamic_sampling(t 2) */ * from t;

Plan de Ejecución
----------------------------------------------------------
Plan hash value: 2153619298

--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 101K| 17M| 333 (1)| 00:00:08 |
| 1 | TABLE ACCESS FULL| T | 101K| 17M| 333 (1)| 00:00:08 |
--------------------------------------------------------------------------

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

Como observamos la cardinalidad estimada fue muy cercana a la realidad.

Por último pruebo con el nivel más alto posible de dynamic_sampling:

rop@DESA10G> select /*+ dynamic_sampling(t 10) */ * from t;

Plan de Ejecución
----------------------------------------------------------
Plan hash value: 2153619298

--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 102K| 17M| 333 (1)| 00:00:08 |
| 1 | TABLE ACCESS FULL| T | 102K| 17M| 333 (1)| 00:00:08 |
--------------------------------------------------------------------------

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

Se puede ver que ahora la estimación fue precisa, pero tuvimos que usar el nivel mas alto, lo cual implica una sampling muy agresivo que va demorar un tiempo considerable para tablas grandes.

En la siguiente prueba voy a analizar que pasa cuando la tabla posee estadísticas (utilizo analyze por simplicidad ya que es solo una prueba, pero todos sabemos que hay que usar siempre dbms_stats)

rop@DESA10G> analyze table t compute statistics;

Tabla analizada.

rop@DESA10G> select * from t;

Plan de Ejecución
----------------------------------------------------------
Plan hash value: 2153619298

--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 102K| 8865K| 333 (1)| 00:00:08 |
| 1 | TABLE ACCESS FULL| T | 102K| 8865K| 333 (1)| 00:00:08 |
--------------------------------------------------------------------------

En el plan se nota que no fue necesario usar dynamic_sampling y tal como esperaba la estimación fue precisa.

Trunco la tabla para mostrar que sucede cdo no hay estadísticas y no usa muestreo:

rop@DESA10G> truncate table t;

Tabla truncada.

rop@DESA10G> select /*+ dynamic_sampling(t 0) */ * from t;

Plan de Ejecución
----------------------------------------------------------
Plan hash value: 2153619298

--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 102K| 8865K| 333 (1)| 00:00:08 |
| 1 | TABLE ACCESS FULL| T | 102K| 8865K| 333 (1)| 00:00:08 |
--------------------------------------------------------------------------

En realidad no hay filas pero la estimación me dio 102 mil filas, imaginen que puede pasar con una sentencia similar si el optimizador arma un plan con tal error de cardinalidad.


rop@DESA10G> select /*+ dynamic_sampling(t 1) */ * from t;

Plan de Ejecución
----------------------------------------------------------
Plan hash value: 2153619298

--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 89 | 331 (1)| 00:00:08 |
| 1 | TABLE ACCESS FULL| T | 1 | 89 | 331 (1)| 00:00:08 |
--------------------------------------------------------------------------

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

Con tan solo elevar un solo nivel el sampling la estimación cambia sensiblemente y refleja la realidad
de los datos.

En la siguiente voy a crear una tabla con dos columnas, de forma tal de que nunca tengan valores iguales. Esto lo hago para mostrar que aunque se use sampling default, si no hay estadísticas el CBO no estima bien la cantidad de filas a retornar:

Voy a crear una tabla con 1M de filas de forma tal que la columna c2 = c1 +1, y voy a realizar un select de forma tal de buscar la filas cuyas columnas c1 y c2 sean iguales, obviamente dada la lógica que se uso para llenar la tabla deberá devolver 0 filas:

rop@DESA10G> create table t as select rownum c1,rownum+1 c2 from dual connect by rownum <= 1000000; Tabla creada.


Para dynamic_sampling 0:

rop@DESA10G> set autotr trace exp


rop@DESA10G> select /*+ dynamic_sampling(t 0) */ * from t
2 where c1 = 1 and c2 =1;

Plan de Ejecución
----------------------------------------------------------
Plan hash value: 2153619298

--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 17 | 442 | 476 (1)| 00:00:11 |
|* 1 | TABLE ACCESS FULL| T | 17 | 442 | 476 (1)| 00:00:11 |
--------------------------------------------------------------------------

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

1 - filter("C1"=1 AND "C2"=1)


La cardinalidad fue de 17 filas

rop@DESA10G> select /*+ dynamic_sampling(t 2) */ * from t
2 where c1 = 1 and c2 =1;

Plan de Ejecución
----------------------------------------------------------
Plan hash value: 2153619298

--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 88 | 2288 | 486 (3)| 00:00:11 |
|* 1 | TABLE ACCESS FULL| T | 88 | 2288 | 486 (3)| 00:00:11 |
--------------------------------------------------------------------------

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

1 - filter("C1"=1 AND "C2"=1)

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

extrañamente para nivel default en 10g de dynamic_sampling la estimación fue aun peor, 88 filas.

Con el valor de muetreo máximo:

rop@DESA10G> ed
Escrito file afiedt.buf

1 select /*+ dynamic_sampling(t 10) */ * from t
2* where c1 = 1 and c2 =1
rop@DESA10G> /

Plan de Ejecución
----------------------------------------------------------
Plan hash value: 2153619298

--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 26 | 487 (3)| 00:00:11 |
|* 1 | TABLE ACCESS FULL| T | 1 | 26 | 487 (3)| 00:00:11 |
--------------------------------------------------------------------------

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

1 - filter("C1"=1 AND "C2"=1)

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


Con nivel 10 la estimación fue correcta, no probé pero tal vez la cardinalidad correcta se alcance con un nivel menor, pero mayor a 2.

Actualizo las estádisticas:

rop@DESA10G> analyze table t compute statistics;

Tabla analizada.

rop@DESA10G> select /*+ dynamic_sampling(t 2) */ * from t
2 where c1 = 1 and c2 =1;

Plan de Ejecución
----------------------------------------------------------
Plan hash value: 2153619298

--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 8 | 487 (3)| 00:00:11 |
|* 1 | TABLE ACCESS FULL| T | 1 | 8 | 487 (3)| 00:00:11 |
--------------------------------------------------------------------------

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

1 - filter("C2"=1 AND "C1"=1)

Con dynamic_sampling default la cardinalidad estimada es la real.

La última prueba que voy a hacer es usando binding, sin estadísticas:


rop@DESA10G> variable x number;

rop@DESA10G> exec :x := 1;

Procedimiento PL/SQL terminado correctamente.


rop@DESA10G> analyze table t delete statistics;

Tabla analizada.

rop@DESA10G> select /*+ dynamic_sampling(t 2) */ * from t
2 where c1=:x and c2=:x;

Plan de Ejecución
----------------------------------------------------------
Plan hash value: 2153619298

--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 88 | 2288 | 491 (4)| 00:00:11 |
|* 1 | TABLE ACCESS FULL| T | 88 | 2288 | 491 (4)| 00:00:11 |
--------------------------------------------------------------------------

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

1 - filter("C1"=TO_NUMBER(:X) AND "C2"=TO_NUMBER(:X))

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

La estimación fue la misma que cuando usamos literales.

Veamos que pasa si elevamos el nivel a 4:

rop@DESA10G> ed
Escrito file afiedt.buf

1 select /*+ dynamic_sampling(t 4) */ * from t
2* where c1=:x and c2=:x
rop@DESA10G> /

Plan de Ejecución
----------------------------------------------------------
Plan hash value: 2153619298

--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 97 | 2522 | 493 (5)| 00:00:11 |
|* 1 | TABLE ACCESS FULL| T | 97 | 2522 | 493 (5)| 00:00:11 |
--------------------------------------------------------------------------

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

1 - filter("C1"=TO_NUMBER(:X) AND "C2"=TO_NUMBER(:X))

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

La estimación es mas lejana a la realidad.

Con nivel 10:

rop@DESA10G> ed
Escrito file afiedt.buf

1 select /*+ dynamic_sampling(t 10) */ * from t
2* where c1=:x and c2=:x
rop@DESA10G> /

Plan de Ejecución
----------------------------------------------------------
Plan hash value: 2153619298

--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 100 | 2600 | 493 (5)| 00:00:11 |
|* 1 | TABLE ACCESS FULL| T | 100 | 2600 | 493 (5)| 00:00:11 |
--------------------------------------------------------------------------

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

1 - filter("C1"=TO_NUMBER(:X) AND "C2"=TO_NUMBER(:X))

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


Sorpresivamente, por lo menos para mi, la estimación con dynamic_sampling en 10 es de 100 filas.
Con solo analizar la tabla, vemos que la cardinalidad del plan es la correcta:

rop@DESA10G> analyze table t compute statistics;

Tabla analizada.

rop@DESA10G> select * from t
2 where c1=:x and c2=:x;

Plan de Ejecución
----------------------------------------------------------
Plan hash value: 2153619298

--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 8 | 493 (5)| 00:00:11 |
|* 1 | TABLE ACCESS FULL| T | 1 | 8 | 493 (5)| 00:00:11 |
--------------------------------------------------------------------------

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

1 - filter("C1"=TO_NUMBER(:X) AND "C2"=TO_NUMBER(:X))

rop@DESA10G>

En conclusión se ve que cuando usamos binding el dynamic_sampling no parece ser muy útil, lo cual no estaría tan mal ya que este parámetro fue diseñado pensando en sistemas DW y no OLTP ya que la idea del sampling es para sistemas que ejecutan pocas consultas largas (tipicamente sistemas de DataWarehouse) y no para sistemas OLTP donde se ejecutan muchas veces consultas cortas. En sistemas OLTP es recomendable usar profiles (10g r1 en adelante) ya que permiten persistir en el diccionario información estadística de las consultas (sampling) para ayudar y complementar a las estadísticas típicas de los objetos. También podemos pensar que en ciertos casos, aunque estemos en sistemas OLTP, el muestreo puede minimizar el sesgo con la realidad para tablas temporales o tablas normales con estadísticas nulas y así lograr un mejor plan.

Ref 1: Database Performance & Tuning Guide (10g)
Ref 2: On Dynamic Sampling

viernes, 6 de marzo de 2009

Script para analizar tendendencia de crecimiento de tablespaces

A continuación les voy a pasar un script que permite monitorear como van llenandose los tablespaces y ademas obtiene una proyección de crecimiento a futuro. A mi me sirve para minimizar el riesgo de que se queden sin espacio los tablespaces (no suelo usar autoextend) y así poder realizar realocaciones o resizing en forma anticipada.


Rem
Rem Tendencia_Crecimiento_x_Tablespace
Rem
Rem NOMBRE
Rem Tendencia_Crecimiento_x_Tablespace.sql
Rem
Rem DESCRIPCION
Rem Reporte para mostrar como fue creciendo un tablespace por hora, dia,
Rem semana y mes. Tambien realiza proyecciones de crecimiento por semana -
Rem mes y muestra STATUS (Aplica para 10+)
Rem
Rem
Rem provedo 04/03/09 -- Creado
Rem
set line 150
col "%Used" format a10
col "%Proy_1s" format a10
col "%Proy_1m" format a10
col tsname format a20
select tsname,
round(tablespace_size*t2.block_size/
1024/1024,2) TSize,
round(tablespace_usedsize*t2.block_size/1024/1024,2) TUsed,
round((tablespace_size-tablespace_usedsize)*t2.block_size/1024/1024,2) TFree,
round(val1*t2.block_size/1024/1024,2) "Dif_1h",
round(val2*t2.block_size/1024/1024,2) "Dif_1d",
round(val3*t2.block_size/1024/1024,2) "Dif_1s",
round(val4*t2.block_size/1024/1024,2) "Dif_1m",
round((tablespace_usedsize/tablespace_size)*100)||'%' "%Used",
round(((tablespace_usedsize+val3)/tablespace_size)*100)||'%' "%Proy_1s",
round(((tablespace_usedsize+val4)/tablespace_size)*100)||'%' "%Proy_1m",
case when ((((tablespace_usedsize+val3)/tablespace_size)*100 < 80) and
(((tablespace_usedsize+val4)/tablespace_size)*100 < 80)) then 'NORMAL'
when ((((tablespace_usedsize+val3)/tablespace_size)*100 between 80 and 90)
or
(((tablespace_usedsize+val4)/tablespace_size)*100 between 80 and 90))
then 'WARNING'
else 'CRITICAL' end STATUS
from
(select distinct tsname,
rtime,
tablespace_size,
tablespace_usedsize,
tablespace_usedsize-first_value(tablespace_usedsize)
over (partition by tablespace_id order by rtime rows 1 preceding) val1,
tablespace_usedsize-first_value(tablespace_usedsize)
over (partition by tablespace_id order by rtime rows 24 preceding) val2,
tablespace_usedsize-first_value(tablespace_usedsize)
over (partition by tablespace_id order by rtime rows 168 preceding) val3,
tablespace_usedsize-first_value(tablespace_usedsize)
over (partition by tablespace_id order by rtime rows 720 preceding) val4
from (select t1.tablespace_size, t1.snap_id, t1.rtime,t1.tablespace_id,
t1.tablespace_usedsize-nvl(t3.space,0) tablespace_usedsize
from dba_hist_tbspc_space_usage t1,
dba_hist_tablespace_stat t2,
(select ts_name,sum(space) space
from recyclebin group by ts_name) t3
where t1.tablespace_id = t2.ts#
and t1.snap_id = t2.snap_id
and t2.tsname = t3.ts_name (+)) t1,
dba_hist_tablespace_stat t2
where t1.tablespace_id = t2.ts#
and t1.snap_id = t2.snap_id) t1,
dba_tablespaces t2
where t1.tsname = t2.tablespace_name
and rtime = (select max(rtime) from dba_hist_tbspc_space_usage)
and t2.contents = 'PERMANENT'
order by "Dif_1h" desc,"Dif_1d" desc,"Dif_1s" desc, "Dif_1m" desc


Un ejemplo de la salida resultado de correr el script es la siguiente:


TSNAME TSIZE TUSED TFREE Dif_1h Dif_1d Dif_1s Dif_1m %Used %Proy_1s %Proy_1m STATUS
-------------------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- --------
SYSAUX 580 551 29 .13 -3.31 -17.81 -17.81 95% 92% 92% CRITICAL
TS_TEL 61440 41097.13 20342.88 0 748.63 -2776 -2776 67% 62% 62% NORMAL
DESA_TS 7552 3278.56 4273.44 0 12 14.19 14.19 43% 44% 44% NORMAL
USERS 23205 720.44 22484.56 0 .06 .25 .25 3% 3% 3% NORMAL
SYSTEM 3620 1067.75 2552.25 0 0 2.06 2.06 29% 30% 30% NORMAL
TS_DATA 10240 10220.44 19.56 0 0 0 0 100% 100% 100% CRITICAL
SCH_DATA 900 899.69 .31 0 0 0 0 100% 100% 100% CRITICAL
EXAMPLE 100 68.19 31.81 0 0 0 0 68% 68% 68% NORMAL
ROP 1024 0 1024 0 0 0 0 0% 0% 0% NORMAL
TS_INDEX 4096 506.31 3589.69 0 0 0 0 12% 12% 12% NORMAL
SCH_INDEX 100 6.94 93.06 0 0 0 0 7% 7% 7% NORMAL


Ahora voy a describir las columnas del reporte:

TSNAME : Nombre del Tablespace
TSIZE : Espacio total del Tablespace en Mb (no toma en cuenta autoextend del
tablespace)
TUSED : Espacio utilizado (Mb)
TFREE : Espacio Libre (Mb)
Dif_1h : Diferencia entre el espacio alocado hace 1 hora y el espacio actual
(Mb).
Dif_1d : Diferencia entre el espacio alocado hace 1 dia y el espacio actual
(Mb).
Dif_1s : Diferencia entre el espacio alocado hace 1 semana y el espacio actual
(Mb).
Dif_1m : Diferencia entre el espacio alocado hace 1 mes y el espacio actual
(Mb).
%Used : Porcentaje de Uso actual del tablespace.
%Proy_1s : Porcentaje de Uso Proyectado a 1 semana adelante.
%Proy_1m : Porcentaje de Uso Proyectado a 1 mes adelante.
STATUS : Status de alocación: menor al 80% es normal, entre 80% y 90% es
warning y mayor al 90% es critical.

El script toma en cuenta que el AWR guarda 1 mes de historia y corre cada 1 hora.

jueves, 5 de marzo de 2009

Como resetear y como obtener el próximo valor de una secuencia

Varias veces me preguntaron como reiniciar (resetear) una secuencia de modo de que comience a generar valores desde el principio nuevamente. Esto suele ser util en la etapa de testing de aplicativos ya que en las pruebas se necesita probar desde cero varias veces. Oracle, al menos hasta ahora, no provee un forma nativa de reiniciar una secuencia (por ejemplo algo como alter sequence reset) pero si permite cambiar el incremento, que como default es 1, y setearlo a un valor negativo, por lo tanto en el próximo nextval va a restar (decrementar) el valor. Si hacemos un decremento que logre que pase de su valor actual a 1 conseguiriamos hacer que el contador se resetee. Les voy a mostrar como hacerlo creando un stored procedure:


rop@DESA10G> create sequence seq;

Secuencia creada.

rop@DESA10G> create or replace procedure reset_seq( p_seq_name in varchar2 ) is
2 l_val number;
3 begin
4 execute immediate
5 'select ' || p_seq_name || '.nextval from dual' INTO l_val;
6 execute immediate
7 'alter sequence ' || p_seq_name || ' increment by -' || l_val || ' minvalue 0';
8 execute immediate
9 'select ' || p_seq_name || '.nextval from dual' INTO l_val;
10 execute immediate
11 'alter sequence ' || p_seq_name || ' increment by 1 minvalue 0';
12 end;
13 /

Procedimiento creado.

Ahora voy a incrementar el valor de la secuencia varias veces:

rop@DESA10G> select seq.nextval from dual;

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

rop@DESA10G> select seq.nextval from dual;

NEXTVAL
----------
2

rop@DESA10G> /

NEXTVAL
----------
3

rop@DESA10G> /

NEXTVAL
----------
4

rop@DESA10G> /

NEXTVAL
----------
5

rop@DESA10G> /

NEXTVAL
----------
6

Si hicieramos nextval el próximo valor será el 7. Ahora vamos a reiniciar la secuencia usando el procedimiento creado mas arriba.

rop@DESA10G> exec reset_seq('SEQ');

Procedimiento PL/SQL terminado correctamente.

rop@DESA10G> select seq.nextval from dual;

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

rop@DESA10G> /

NEXTVAL
----------
2

Como observamos en el ejemplo, la secuencia comenzó a generar valores nuevamente.


Ahora voy a mostrarles como obtener el próximo valor de una secuencia. En principio uno piensa en obtener ese dato desde una vista de catalogo, pero si la sequencia utiliza cache mayor a 0 (el default es 20) no nos va mostrar el próximo valor real sino que nos va a mostrar el próximo no cacheado. Para ser mas claro les voy a mostrar un ejemplo
 
rop@DESA10G> create sequence seq;

Secuencia creada.

rop@DESA10G> select seq.nextval from dual;

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

rop@DESA10G> /

NEXTVAL
----------
2

rop@DESA10G> /

NEXTVAL
----------
3

rop@DESA10G> /

NEXTVAL
----------
4

rop@DESA10G> /

NEXTVAL
----------
5


rop@DESA10G> select * from user_sequences where sequence_name = 'SEQ';

SEQUENCE_NAME MIN_VALUE MAX_VALUE INCREMENT_BY C O CACHE_SIZE LAST_NUMBER
------------------------------ ---------- ---------- ------------ - - ---------- -----------
SEQ 0 1.0000E+27 1 N N 20 21

1 filas seleccionadas.


Consultando la vista USER_SEQUENCES vemos que la columna last_number podría servirnos para saber cual será el próximo valor, pero vemos que el valor es 21 y el próximo valor deberia ser 6, entonces no nos sirve esa vista para lo que queremos. Para poder conocer exactamente el valor siguiente que nos entregará una secuencia tenemos que usar v$_sequences que no es visible desde ningun usuario y no tiene sinonimo publico. Lo que voy a hacer es presentar esa vista a todos los usuarios para que se pueda consultar:



sys@ROP10G> desc v$_sequences
Name Null? Type
----------------------------------------------------- -------- ------------------------------
SEQUENCE_OWNER VARCHAR2(64)
SEQUENCE_NAME VARCHAR2(1000)
OBJECT# NUMBER
ACTIVE_FLAG VARCHAR2(1)
REPLENISH_FLAG VARCHAR2(1)
WRAP_FLAG VARCHAR2(1)
NEXTVALUE NUMBER
MIN_VALUE NUMBER
MAX_VALUE NUMBER
INCREMENT_BY NUMBER
CYCLE_FLAG VARCHAR2(1)
ORDER_FLAG VARCHAR2(1)
CACHE_SIZE NUMBER
HIGHWATER NUMBER
BACKGROUND_INSTANCE_LOCK VARCHAR2(1)
INSTANCE_LOCK_FLAGS NUMBER


sys@ROP10G> create view v$sequences as select * from v$_sequences;

View created.

sys@ROP10G> create public synonym v$sequences for sys.v$sequences;

Synonym created.

sys@ROP10G> grant select on v$sequences to public;

Grant succeeded.

Ahora que ya publicamos la vista podemos hacer la siguiente consulta para obtener el próximo valor:

rop@DESA10G> ed
Escrito file afiedt.buf

1 select b.object_name,a.nextvalue
2 from v$sequences a, user_objects b
3 where a.object# = b.object_id and
4* b.object_name = 'SEQ'
rop@DESA10G> /

OBJECT_NAME NEXTVALUE
----------------------------
SEQ 6

Como se observa, el valor retornado fue lo que esperabamos. Ahora verificamos haciendo un nextval:


rop@DESA10G> select seq.nextval from dual;

NEXTVAL
----------
6

rop@DESA10G>


Efectivamente el próximo valor era 6. No se si esto pueda tener gran utilidad pero la idea era mostrarles como hacerlo.

lunes, 2 de marzo de 2009

Herramienta para Testeo de Rendimiento de I/O (Orion)

Orion es una herramienta de calibración de I/O que permite predicir el rendimiento que tendrá una base de datos Oracle sin tener creada una base de datos, incluso sin tener el software de Oracle instalado. A diferencia de otras herramientas de calibración de I/O, Orion esta diseñado para simular el acceso a disco tal cual lo hace Oracle ya que usa la misma capa de acceso.
Los tipos de carga que pueden ser simulados son los siguientes:

Small Random I/O: Los sistemas OLTP se caracterizan por tener este tipo de acceso. En este contexto las métricas que importan son I/O por segundo (IOPS) y latencia promedio por acceso

Large Sequential I/O: Simula carga típica en aplicaciones de DataWarehouse, carga masiva, backups, restore, etc. Tales aplicaciones procesan gran cantidad de datos y lo importante es medir Megabytes por segundo (MBPS).

Large Random I/O: Cuando se usa striping las lecturas secuenciales se realizan como una cantidad concurrente de lecturas secuenciales aleatorias de 1Mb, conocido como I/O secuencial multiusuario.

Mixed Workload: Se simulan dos cargas de trabajo simultaneas: Small Random I/O y Large Sequential I/O o Large Random I/O. Esto permite simular por ejemplo el comportamiento de una base OLTP (Small Random I/O) mientras se esta efectuando un backup completo de la base de datos en caliente (Large Sequential I/O).

Orion puede utilizarse para testear cualquier tipo de discos que soporte I/O asincrónica. Se pueden testear:

• DAS (direct-attached storage)
• SAN (storage-area network)
• NAS (network-attached storage)

Y se encuentra disponible para las siguientes plataformas:

• AIX
• Solaris 64_sparc
• Solaris 64_x86
• Linux 32
• Linux 64
• Windows

y recientemente se agregaron: zlinux,HP itanium y PA RISC, linux sobre sobre itanium y power.

Instalación y Configuración


Orion es una herramienta totalmente gratuita, que puede descargarse desde otn.roacle.com. La instalación solo requiere de la descompresión del archivo .gz bajado. Los siguientes pasos describen una configuración básica:

1. Una vez descomprimido, asignar permisos de ejecución al usuario desde el que se va a ejecutar el test.

2. Crear un archivo (ej: mytest.lun) con la lista de los volumen o raw devices a calibrar. El archivo debe contener un nombre del volumen o filesystem por linea, por ejemplo:
/dev/vx/dsk/sgu/vol01
/dev/vx/dsk/sgu/vol02

3. Verificar que los volúmenes sean accesibles con herramientas de copiado, tipo dd, por ejemplo:
dd if=/dev/raw/raw1 of=/dev/null bs=32k count=1

4. Verificar que la plataforma tenga instaladas y accesibles las librerias de acceso asincrónico. Orion funciona solo con I/O asincrónico.

5. Es conveniente comenzar con una prueba simple, por ejemplo, el siguiente comando:
$./orion –run simple –testname mytest –num_disks 4

Ejecuta un test simple con small random reads y large random reads con diferentes cargas que permitiran tener una primera noción de cómo se comporta la I/O para distintos tipos de accesos y de carga.


Archivos de Resultado


El resultado del Test se encuentra detallado en los siguientes archivos:

1. mytest_summary.txt: Este archivo contiene:
a. Parámetros de Entrada.
b. Throughput maximo para carga Large Random/Sequential.
c. Tasa Maxima de I/O para carga Small Random.
d. Minima Latencia para carga Small Random.

2. mytest_mbps.txt: Archivo separado por comas, que contiene detalle de la tasa de transferencia para tipo de acceso Large Sequential/Random.

3. mytest_iops.txt: Archivo separado por comas, que detalla el I/O throughput medido en IOPS para carga de trabajo tipo Small Random.

4. mytest_lat.csv: Archivo separado por comas con los resultados de latencia según distintos tipos de carga para tipo de carga Small Random.

5. mytest_trace.txt: Archivo con información “cruda” producto de la traza del test.

Parametrización de Entrada

La ejecución de Orion puede ser parametrizada por distintas opciones. Los parámetros obligatorios son:

-run: Puede ser simple, normal o advanced. Para comenzar conviene usar simple y luego utilizar advanced para tener mas control de la calibración.
-testname: Aca va el nombre del archivo definido en el paso 2 de la configuración (mytest).
-num_disks: Define la cantidad de volúmenes a evaluar, generalmente este valor es igual a la cantidad de filas que contenga el archivo mytest.
Los parámetros opcionales son:
-help: Información de ayuda. Descripción de parametros
-size_small: Tamaño de I/O (en Kb) para carga Small Random I/O (el default es 8)
-size_large: Tamaño de I/O (en kb) para carga Large Sequential/Random I/O (el default es 1Mb)
-type: Tipo de carga de trabajo Large (default rand)
-num_streamIO: Numero de I/O’s por cada stream
-simulate: Permite similar como estan los datos dispuestos en los discos (el default es concat)
-write: Porcentaje de I/O que será:n writes.
-cache_size: Tamaño de cache del storage array
-duration: Duración del test por cada punto de datos (el default es 60 segundos)
-matriz: Tipo de carga (el default de detailed)
-num_small: Máximo numero de I/O para tipo de carga Small Random.
-num_large: Máximo numero de I/O para tipo de carga Large Random o numero de Large I/O por stream.
-verbose: Muestra información de estado y progreso en la salida estándar.

Ejemplos de Uso

A continuación se detallan algunos ejemplos de uso:
Ejemplo1: Ejecución básica
$./orion –run simple –testname mytest –num_disks 1

Ejemplo 2: Ejecución avanzada de tipo Large Sequential con nivel de carga Small fijado en 2
$ ./orion -run advanced -testname mytest -num_disks 2 -type seq -num_streamIO 10 -matrix col -num_small 2

Ejemplo 3: Ejecución para generar multiples escrituras de 1mb simulado RAID0 con stripes de 1Mb
$./orion –run advanced –testname mytest –num_disks 8 –simulate raid 0 –stripe 1024 –write 100 –type seq –matrix col –num_small 0


Si bien en la documentación sigue diciendo que esta herramienta es beta y no es soportada por Oracle Corporation ya tiene varios años y se ha portado a casi todas las plataformas por lo cual daría la impresión que ya ha madurado lo suficiente. Orion permite evaluar Storage y ver como se comporta con los requerimientos de rendimiento esperado sin necesidad de instalar ni el motor ni una base de datos Oracle. Los administradores podrán comparar distintos arreglos de storage de acuerdo a la carga de trabajo esperada y poder optar por la configuración mas conveniente.

Para mayor información ver:
Oracle Orion Users Guide
Para bajar Orion (Oracle I/O Calibration Test):
Oracle Orion Downloads