miércoles, 20 de mayo de 2009

Como configurar una arquitectura de alta disponibilidad con Oracle Standard Edition (SE)

Hace un tiempo un cliente me pidio armar una arquitectura de alta disponibilidad de forma tal de que la base de datos productiva tuviera un "espejo" y que, por ejemplo, ante un fallo de hardware severo del nodo principal pudiera activarse
el nodo de respaldo con la base "espejo" y que esto no dejara mas de unos pocos minutos al sistema corporativo sin base de datos.
El cliente habia comprado un nuevo servidor, similar al equipo que alojaba la base productiva. Ante el pedido, lo primero que se me ocurrio era armarles un esquema con una base stand-by, ahora llamado DATA GUARD. El problema fue que el cliente tenia su base de datos con Oracle 10g Standard Edition. Oracle SE no tiene el feature de DATA GUARD y por lo tanto la mejor opción que encontré fue implementar un esquema stand-by manual, manejado con scripting y cron. Este esquema no provee la robustez de un DATA GUARD pero se acerca bastante. Como voy a explicar mas adelante, con este modelo de no podemos asegurar que no se pierda lo que se persistio los ultimos minutos, pero dado que el negocio podía contemplar esta perdida se decidió implementar la arquitectura que propuse. A continuación voy a explicar paso a paso, llamaré NODO1 al nodo principal y activo y NODO2 al nodo secundario o stand-by que esta en modalidad pasiva.

1. Instalar el motor de base de datos en el NODO 2. Asegurarse que el S.O sea el mismo que el instalado en el NODO 1, debe tener el mismo nivel de parches, misma arquitectura (32 ó 64 bits) y la base debe tener la misma versión y nivel de parches
que la base productiva.

2. La base de producción (NODO1) deberá estar en modo archiving (esto para mi es excluyente para cualquier base productiva asi que lo
tomo como algo obvio). Copiar con RMAN los datafiles de la siguiente manera:

$rman target / no catalog

RMAN>report schema; <-- Para ver los datafiles que necesitamos copiar.

Una vez que tengo el listado de los datafiles de la base de datos se van copiando uno por uno a un directorio de backups,
por ejemplo para copiar el datafile system01.dbf:

RMAN>copy datafile '/u01/oradata/ROP102/system01.dbf' to '/u01/bkp/ROP102/system01.dbf';

3. Crear el control file standby desde la base de datos primaria (NODO1)

SQLPLUS> ALTER DATABASE CREATE STANDBY CONTROLFILE AS '/u01/bkp/ROP102/control_stdy.ctl';

4. Copiar los datafiles respaldados con el comando copy de rman (paso 2), el control file standby creado en el paso 3
y el archivo de parametros de la base productiva al NODO 2, por ejemplo para pasar los datafiles:

$scp /u01/bkp/ROP102/*.dbf nodo2:/u01/oradata/ROP102/.

Si tenemos distinta estructura de directorios donde van los datafiles entre el NODO1 y el NODO2 tenemos que editar el archivo
de parametros y agregar el siguiente parametro:

DB_FILE_NAME_CONVERT='/u01/oradata/ROP102','u02/oradata/ROP102'

5. Ahora ya tenemos todos los archivos que necesitamos para levantar la base standy en el NODO2.

Se levanta la base en modo nomount usando el archivo de parametros copiado:

SQLPLUS>startup nomount pfile=/u01/app/oracle/products/10.2.0.1/db_1/dbs/iniROP102.ora

Se monta la base en modalidad standby:

SQLPLUS>alter database mount standby database;

6. Una vez que tenemos la base montada en standby solo resta realizar recover de la base en modo AUTO para poner los
datafiles en modo consistente.

SQLPLUS>recover standby database;
AUTO

7. Para chequear que nuestra base standby esta ok, podemos abrirla en modo read only:

SQLPLUS>alter database open read only;


Scripting necesario para la configuración inicial

En NODO 1 - Base de Datos Primaria

El siguiente script se encarga de transferir al nodo standby (NODO 2) las novedades, es decir los
archivelogs que se hayan generado desde la ultima transferencia.

-- move_stdby.sh
rsync -t -e ssh -Pazv /u01/oradata/ROP102/arch/ oracle@dbprod2:/u01/oradata/ROP102/arch/

Para purgar los archivos que ya no se necesiten usamos:

-- purge_arch_nodo1.sh
find /u01/oradata/ROP102/arch/*.arc -mtime +2 -exec rm {} \;

Una posible programación de los scripts del nodo 1 podria ser:

0,30 * * * * /home/oracle/move_stdby.sh <-- Se mueven del nodo 1 al nodo 2 cada 30'
0 7 * * * /home/oracle/purge_arch.sh <-- Se borran los archives todos los dias a las 7am


En NODO 2 - Base de Datos StandBy


Para aplicar los nuevos archives tranferidos:

-- apply_redo_stdby.sh
export ORACLE_SID=ROP102
export ORACLE_HOME=/u01/app/oracle/product/10.2.0
$ORACLE_HOME/bin/sqlplus -s "/ as sysdba" << EOF
recover standby database;
AUTO
exit
EOF

Para levantar la base en modo standby y preparada para aplicar archives:

-- manual_stdby.sh
sqlplus -s "/ as sysdba" << EOF
shutdown immediate;
startup nomount pfile=/u01/app/oracle/product/10.2.0/dbs/initROP102_stdby.ora
alter database mount standby database;
exit
EOF

Para levantar la base en modo solo lectura

-- readonly_stdby.sh
sqlplus -s "/ as sysdba" << EOF
alter database open read only;
exit
EOF

Para purgar los archives copiados al nodo standby

-- purge_arch_nodo2.sh

find /u01/oradata/ROP102/arch/*.arc -mtime +6 -exec rm {} \;
[oracle@dbprod2 ~]$

Una posible programación de los scripts del nodo 1 podria ser:

0,30 * * * * /home/oracle/apply_redo_stdby.sh <-- Se aplican los cambios a la base standby cada 30'
0 7 * * * /home/oracle/purge_arch.sh <-- Se borran los archives todos los dias a las 7am


La base standby esta continuamente aplicando archives, eso se puede chequear en el alert. Cuando la base standby esta en modo solo lectura no puede aplicar nuevos archives. En ciertas ocasiones resulta util tener la base standby en modo read only para realizar reportes y así evitar sobrecargar la base primaria.


Como hacer el switchover en el caso de que falle la base de datos primaria

Si la base primaria falla y no puede recomponerse (por ejemplo por un problema serio de HW) hay que activar la base standby. Para que pueda cumplir el rol de base primaria tendremos que levantarla en modo read write y hacer los siguiente:

1. Aplicar todos los archives que que aun no se hayan aplicado y que hayan pasado al nodo 2.

2. Hacer backup to trace del control file con la base standby en modo read only.

SQLPLUS> alter database backup controlfile to trace as '/u01/oradata/ROP102/control.txt'

3. Levantar la base standby en modo nomount y crear el control file usando en script del control file creado en el paso 2.

SQLPLUS> startup nomount pfile=/u01/app/oracle/product/10.2.0/dbs/initROP102_stdby.ora

Editar el archivo control.txt y dejar solo la ultima sentencia de creación del controlfile. Comentar todas las otras lineas

SQLPLUS>@/u01/oradata/ROP102/control.txt

4. Montar la base usando el nuevo controlfile

Editar el archivo de parametros y cambiar el nombre del controlfile para que se referencie el controlfile recien creado

SQLPLUS>startup mount pfile=/u01/app/oracle/product/10.2.0/dbs/initROP102_stdby.ora

5. Abrir la base en modo resetlogs

SQLPLUS>alter database open resetlogs;

Una vez realizado el paso 5 tenemos la base standby en modalidad read-write y totalmente operativa. Obviamente esta configuración requiere de scripting y de una tarea manual para levantar la base standby en caso de falla. Lo ideal seria tener DATA GUARD que asegura 0 perdida de información y automatización de los procesos de switchover. De todas formas, la arquitectura descripta es bastante adeacuada si no se pretende gastar mucho dinero en licencias y si la posible perdida de algunos minutos de información no afecta al negocio sensiblemente.

jueves, 14 de mayo de 2009

Truncate vs Delete

Abajo va un listado de las principales diferencias entre truncar y "deletear" todas las filas de una tabla. Por lo que me han contado gente conocida, parecería ser que enumerar las diferencias entre estas dos operaciones es una de las preguntas clásicas en los test de admisión de perfiles Oracle en las empresas.


1. TRUNCATE es una operacion DDL y es rapido y DELETE una operación DML y es lento.
2. TRUNCATE resetea el HWM y dealoca el espacio, DELETE no.
3. TRUNCATE no tiene vuelta atrás, ni siquiera se puede hacer un flashback. Es raro
que se pueda hacer flashback de un drop pero no de un truncate, no?. Con delete
se puede hacer un rollback y si ya se confirmo el borrado (commit) se podría
utilizar flashback.
4. TRUNCATE no dispara DML's triggers asociados a la tabla truncada.
5. TRUNCATE tiene un tratamiento especial para la MATERIALIZED VIEW LOG vinculada
con la tabla.
6. DELETE puede utilizarse para eliminar un subconjunto de datos, con TRUNCATE hay
que eliminar todas las filas. Sería bueno que existiera algo asi como: TRUNCATE
.. WHERE .., no?
7. TRUNCATE no puede mantener foreign keys, por el contrario con DELETE podemos
hacer delete cascade.
8. TRUNCATE invalida indices globales cuando se truncan particiones y
subparticiones. Por suerte desde 9i R2 se pueden mantener los indices globales
validos usando UPDATE GLOBAL INDEXES.
9. TRUNCATE puede validar indices que ya estaban invalidos y pasarlos a estado
valido. Cuidado cuando para acelerar procesos de carga se deshabiliten los
indices y luego se trunque, ya que la carga se hara con los indices habilitados.
Primero truncar y luego pasar los indices a unusables.
10.Ni TRUNCATE ni DELETE de todas las filas eliminan las estadísticas asociadas.
Sería interesante que se eliminen las estaditicas automaticamete con el TRUNCATE,
no?.
11.TRUNCATE invalida los cursores que referencian a la tabla en cuestión.
12.DELETE de tablas grandes genera una importante cantidad de UNDO y REDO.

Este listado no es definitivo, si alguien lee esta nota y gusta aumentar las lista con mas diferencias será bienvenido.

Script para listar las sentencias ejecutadas durante un periodo (ordenadas por elapsed time)

El siguiente script genera un listado ordenado por tiempo de ejecución de todas las sentencias ejecutadas en un cierto periodo de tiempo. La información se optiene del repositorio de AWR. Ademas se detallan los planes de ejecución de cada sentencia


-- -----------------------------------------------------------------------------------
-- Nombre : top_sqls.sql
-- Autor : Pablo A. Rovedo
-- Descripción : Lista las sentencias ejecutadas durante un periodo de tiempo
-- ordenadas por elapsed_time
-- -----------------------------------------------------------------------------------

set serverout on size unlimited
set pagesize 9999
set linesize 250

prompt *************************************************
prompt * Ingrese Fecha Inicial AWR [dd/mm/aaaa hh24:mi]*
prompt *************************************************
accept fecha1 prompt '> '

prompt *************************************************
prompt * Ingrese Fecha Final AWR [dd/mm/aaaa hh24.mi] *
prompt *************************************************
accept fecha2 prompt '> '

prompt ************************************************
prompt * Ingrese el Path para guardar la información *
prompt ************************************************
accept path prompt 'Path del reporte > '

set term off
spool &path


begin

dbms_output.put_line('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~');
dbms_output.put_line('~~~~~~~~~~~~~~~~~ Reporte de Consultas SQL y Plan de Ejecución ~~~~~~~~~~~~~~~~~');
dbms_output.put_line('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~');
dbms_output.put_line('-----------------------------------------------------------------');
dbms_output.put_line(' Sentencias ordenadas por ELAPSED TIME');
dbms_output.put_line('-----------------------------------------------------------------');
for i in ( select ss.sql_id,
ss.plan_hash_value,
max(ss.fetches_total) "fetch_total",
max(ss.executions_total) "exec_total",
max(ss.rows_processed_total) "rows_proc",
round(max(ss.elapsed_time_total)/1000000,2) "ela_time"
from dba_hist_sqlstat ss,
dba_hist_snapshot s
where s.snap_id = ss.snap_id
and parsing_schema_name = 'CR'
and s.begin_interval_time between to_date('&fecha1','DD/MM/YYYY HH24:MI') and
to_date('&fecha2','DD/MM/YYYY HH24:MI')
group by ss.sql_id,ss.plan_hash_value
order by "ela_time" desc)
loop
dbms_output.put_line(' ');
dbms_output.put_line(' ');
dbms_output.put_line('===================================================================');
dbms_output.put_line('SQL ID: '|| i.sql_id || ' Elapsed Time(s): ' ||i."ela_time");
dbms_output.put_line('===================================================================');

for j in (select plan_table_output
from table(dbms_xplan.display_awr(i.sql_id,i.plan_hash_value)))
loop
dbms_output.put_line(j.plan_table_output);
end loop;
end loop;
end;
/
set term on;
prompt Ejecutando Proceso...
set term off;


set term on;
prompt Fin Proceso!
set term off;
spool off;
set echo on
set verify on
set term on
prompt Archivo de Salida del reporte --> &path

viernes, 8 de mayo de 2009

Cuidado con usar funciones min y max en la misma consulta

El optimizador de Oracle es una pieza de software muy compleja e inteligente, pero ciertas veces hace cosas raras y poco intuitivas. Hace poco ví un caso de una consulta que tomaba el máximo y el mínino valor de una columna X sobre una cierta tabla y la almacenaba en variables para realizar cierta logica procedural. El código implementaba la siguiente sentencia para obtener dichos valores:

..
select min(x),max(x)
into l_min,l_max
from t
..

A simple vista se ve como una forma compacta de obtener el min y max con solo una consulta.La columna X tenía definido un índice btree y por lo tanto, sabiendo que la estructura de los indices es de tipo arbol con un nivel o altura generalmente de 3, sin pensar demasiado imaginamos que el optimizador necesitará acceder 3 bloques (bajar por las ramas hasta la hoja de mas a la izquierda) para obtener el valor mínimo y otro 3 bloques (bajar por las ramas hasta la hoja de mas a la derecha) para obtener el valor máximo. Por lo tanto imaginamos que aprovechando el indice Oracle necesitara acceder solo 6 bloques para retornar el resultado. Sin embargo veremos que el optimizador no usa el path adecuado cuando pretendemos obtener dichos valores utilizando una sola sentencia.

Para probar, voy a crear una tabla con 2 campos, el campo X de tipo entero y un campo Y de tipo CHAR(500) utilizado de relleno para generar muchos bloques y así desalentar al optimizador a elegir full scan sobre la tabla.

rop@DESA10G> create table t (x int,y char(500));

Tabla creada.

rop@DESA10G> insert into t select rownum,'blabla' from dual connect by rownum <= 1000000;

1000000 filas creadas.

Chequeamos que se hayan creado una cantidad considerable de bloques:

rop@DESA10G> select blocks,bytes from user_segments where segment_name = 'T';

BLOCKS BYTES
---------- ----------
72704 595591168


Creamos el indice por la columna X y recolectamos estadíticas para la tabla y el índice

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

Índice creado.

rop@DESA10G> begin
dbms_stats.gather_table_stats(ownname=>user,tabname=>'T',cascade=>true);
end;


Ya tenemos armado el escenario para comenzar a probar. Vemos que hace Oracle para obtener el max y min de una vez:

rop@DESA10G> select min(x),max(x) from t;

MIN(X) MAX(X)
---------- ----------
1 1000000


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

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 5 | 16000 (1)| 00:05:49 |
| 1 | SORT AGGREGATE | | 1 | 5 | | |
| 2 | TABLE ACCESS FULL| T | 1003K| 4899K| 16000 (1)| 00:05:49 |
---------------------------------------------------------------------------


Estadísticas
----------------------------------------------------------
178 recursive calls
0 db block gets
71601 consistent gets
71109 physical reads
0 redo size
272 bytes sent via SQL*Net to client
236 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
5 sorts (memory)
0 sorts (disk)
1 rows processed

rop@DESA10G>

El plan armado requiere recorrer todos los bloques de la tabla (full scan) que requeriran acceso fisico al disco con la consiguiente desventaja que todos sabemos involucra una excesiva i/o y una mayor demora en obtener el resultado.
Vemos que pasa al obtener el max y el min de a una sentencia por vez:

rop@DESA10G> select min(x) from t;

MIN(X)
----------
1


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

------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 5 | 3 (0)| 00:00:01 |
| 1 | SORT AGGREGATE | | 1 | 5 | | |
| 2 | INDEX FULL SCAN (MIN/MAX)| T_IDX | 1003K| 4899K| 3 (0)| 00:00:01 |
------------------------------------------------------------------------------------


Estadísticas
----------------------------------------------------------
1 recursive calls
0 db block gets
3 consistent gets
0 physical reads
0 redo size
240 bytes sent via SQL*Net to client
236 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed

rop@DESA10G> select max(x) from t;

MAX(X)
----------
1000000


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

------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 5 | 3 (0)| 00:00:01 |
| 1 | SORT AGGREGATE | | 1 | 5 | | |
| 2 | INDEX FULL SCAN (MIN/MAX)| T_IDX | 1003K| 4899K| 3 (0)| 00:00:01 |
------------------------------------------------------------------------------------


Estadísticas
----------------------------------------------------------
1 recursive calls
0 db block gets
3 consistent gets
1 physical reads
0 redo size
240 bytes sent via SQL*Net to client
236 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed

Se puede constatar que al obtener tanto el min como el max se utilizo el path "INDEX FULL SCAN (MIN/MAX)" que existe desde por lo menos 9i (no recuerdo exactamente en que versión se introdujo) y por lo tanto solo se necesitaron acceder 3 bloques por cada ejecución de las sentencias.
Ahora veamos que sucede si definimos a la columna como not null. Recordemos que los valores null no se almacenan en los indices btree. Declarando explicitamente que la columna no puede contener nulos se le da mayor información al optimizador y asi le ayudamos a que evalue otros posibles paths de acceso a los datos.


rop@MOVI10D> alter table t modify x not null;

Tabla modificada.

rop@MOVI10d>

rop@DESA10G> select min(x),max(x) from t;

MIN(X) MAX(X)
---------- ----------
1 1000000


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

-------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 5 | 506 (2)| 00:00:12 |
| 1 | SORT AGGREGATE | | 1 | 5 | | |
| 2 | INDEX FAST FULL SCAN| T_IDX | 1003K| 4899K| 506 (2)| 00:00:12 |
-------------------------------------------------------------------------------


Estadísticas
----------------------------------------------------------
1 recursive calls
0 db block gets
2241 consistent gets
2 physical reads
0 redo size
271 bytes sent via SQL*Net to client
236 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed

Ahora se necesitaron consultar 2241 bloques de memoria y 0 accesos a disco ya que cambio el plan a un "INDEX FAST FULL SCAN" con lo cual no se necesitó visitar los bloques de la tabla y el resultado se pudo obtener solo recorriendo los bloques del índice. De todas formas, si bien esto es mucho mejor que el full scan de la tabla, dista bastante de ser el camino óptimo. Para eso debemos lograr que el optimizador utilice el path "INDEX FULL SCAN (MIN/MAX)". Reescribiendo un poco la consulta original logramos obtener el min y max consultando solo 6 bloques consultados:

rop@DESA10G> select min_x,max_x
2 from (select min(x) min_x from t),
3 (select max(x) max_x from t);

MIN_X MAX_X
---------- ----------
1 1000000


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

--------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 26 | 6 (0)| 00:00:01 |
| 1 | NESTED LOOPS | | 1 | 26 | 6 (0)| 00:00:01 |
| 2 | VIEW | | 1 | 13 | 3 (0)| 00:00:01 |
| 3 | SORT AGGREGATE | | 1 | 5 | | |
| 4 | INDEX FULL SCAN (MIN/MAX)| T_IDX | 1003K| 4899K| 3 (0)| 00:00:01 |
| 5 | VIEW | | 1 | 13 | 3 (0)| 00:00:01 |
| 6 | SORT AGGREGATE | | 1 | 5 | | |
| 7 | INDEX FULL SCAN (MIN/MAX)| T_IDX | 1003K| 4899K| 3 (0)| 00:00:01 |
--------------------------------------------------------------------------------------


Estadísticas
----------------------------------------------------------
1 recursive calls
0 db block gets
6 consistent gets
0 physical reads
0 redo size
269 bytes sent via SQL*Net to client
236 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed