viernes, 12 de septiembre de 2008

Hints Globales (Global Hints)

Como se puede ver en mi entrada de Mi primer tuning, desde hace aproximadamente 10 años he usado hints en Oracle.

Para los que nunca han usado un hint, no son más que instrucciones adicionales al optimizador de costos, que permiten cambiar un plan de ejecución. En pocas palabras, los hints nos permiten decidir en lugar del optimizador.

Existen diferentes tipos de Hints, oracle los suele categorizar de la siguiente forma:

* Single Table (Index, full)
* Multi Table (use_nl, use_hash, leading)
* Query Block (unnest, nomerge)
* Statement (all_rows, first_rows(n))

Pero en este caso me voy a enfocar a una subcategoria, los "Global Hints".

En mi ejemplo de la vida real, me encontré con sentencias SQL similares a las siguientes


SELECT MIN(CLP.CAMPO_FECHA) FROM
XX_VISTA_1 CIR, XX_TABLA_1 CLP
WHERE CIR.CAMPO_TEXTO_2 = :b1
AND CIR.CAMPO_NUMBER = CLP.CAMPO_NUMBER
AND CLP.CAMPO_FECHA IS NOT NULL
AND CIR.SECUENCIA = NVL(:b2,CIR.SECUENCIA);

SELECT MIN(CLP.CAMPO_FECHA_2)
FROM XX_VISTA_1 CIR, XX_TABLA_1 CLP
WHERE CIR.CAMPO_TEXTO_2 = :b1
AND CIR.CAMPO_NUMBER = CLP.CAMPO_NUMBER
AND CLP.CAMPO_FECHA IS NOT NULL
AND CIR.SECUENCIA = NVL(:b2,CIR.SECUENCIA);



Estas sentencias SQL tenían un pésimo plan de ejecución, ya que la vista "XX_VISTA_1", contenía un inline view, y oracle decidía combinar el inline view con el outer query en una simple sentencia SQL (esto no siempre es una buena idea).

Existían en realidad unas 6 sentencias SQL similares con el mismo problema.

Tras revisar el código de la vista, me di cuenta de que eso era lo que Oracle estaba realizando, y la solución era sencilla... Cambiar el código de la vista para que no hiciera el merge del inline view:



CREATE OR REPLACE FORCE VIEW "APPS"."XX_VISTA_1"
("ROW_ID", CAMPO_FECHA, ...) AS
SELECT /*+ NO_MERGE(msi) */
pdc.ROWID row_id,
msi.FECHA campo_fecha,
...


La realidad es que esta es una de las peores soluciones y realmente no fue lo que recomendé, pero fue lo que el cliente decidió hacer como primer intento.

Internamente para ellos era más fácil modificar 1 vista que modificar 6 programas.

Lo que sucedió fue que a partir de la modificación de la vista, hubo sentencias SQL como la siguiente


SELECT DECODE (:a1,NULL, 'Normal',(
SELECT SECUENCIA_DSP FROM APPS.XX_VISTA_1
WHERE CAMPO_NUMBER =
(SELECT MAX (CAMPO_NUMBER)
FROM APPS.XX_VISTA_1
WHERE CAMPO_NUMBER_2 = :a2
AND CAMPO_TEXTO = 'Active'
AND CAMPO_TEXTO_2
IN ('P', 'O', 'I', 'F')))
) FROM DUAL;


Que al cambiar el código en la vista empezó a tener un desempeño muy pobre, cambiando su plan de ejecución a varios Full Table Scans.

Entonces, si el código de la vista no debe de ser cambiado, ¿qué es lo que se puede usar?

Global Hints

Los hints que se suelen usar en select, updates y deletes, generalmente van relacionados a una tabla que existe dentro del mismo query block. Y no a tablas relacionadas con vistas internas.

La solución son los Global Hints, es decir, se puede usar un Global Hint para decirle al optimizador de costos cómo esolver una vista que existe en nuestro query block.

En mi ejemplo, podemos decirle a la vista que no haga el merge del inline view desde el query principal, y de esta forma evitar la modificación de la vista.


SELECT /*+ NO_MERGE(CIR.msi) */ MIN(CLP.CAMPO_FECHA) FROM
XX_VISTA_1 CIR, XX_TABLA_1 CLP
WHERE CIR.CAMPO_TEXTO_2 = :b1
AND CIR.CAMPO_NUMBER = CLP.CAMPO_NUMBER
AND CLP.CAMPO_FECHA IS NOT NULL
AND CIR.SECUENCIA = NVL(:b2,CIR.SECUENCIA);

SELECT /*+ NO_MERGE(CIR.msi) */ MIN(CLP.CAMPO_FECHA_2)
FROM XX_VISTA_1 CIR, XX_TABLA_1 CLP
WHERE CIR.CAMPO_TEXTO_2 = :b1
AND CIR.CAMPO_NUMBER = CLP.CAMPO_NUMBER
AND CLP.CAMPO_FECHA IS NOT NULL
AND CIR.SECUENCIA = NVL(:b2,CIR.SECUENCIA);



De esta forma se estabilizaron los planes de ejecución de 6 sentencias SQL y no se afectó el desempeño de las demás sentencias que hacían uso de la vista.


En el manual de Oracle de performance tuning, vienen dos ejemplos muy claros sobre el uso de global hints

Ejemplo 1


CREATE OR REPLACE VIEW v AS
SELECT
e1.first_name, e1.last_name, j.job_id, sum(e2.salary) total_sal
FROM employees e1,
( SELECT *
FROM employees e3) e2, job_history j
WHERE e1.employee_id = e2.manager_id
AND e1.employee_id = j.employee_id
AND e1.hire_date = j.start_date
AND e1.salary = ( SELECT
max(e2.salary)
FROM employees e2
WHERE e2.department_id = e1.department_id)
GROUP BY e1.first_name, e1.last_name, j.job_id
ORDER BY total_sal;

SELECT /*+ INDEX(v.e2.e3 emp_job_ix) */ *
FROM v;

SELECT /*+ INDEX(@SEL$2 e2.e3 emp_job_ix) */ *
FROM v;

SELECT /*+ INDEX(@SEL$3 e3 emp_job_ix) */ *
FROM v;


Ejemplo 2


CREATE OR REPLACE VIEW v1 AS
SELECT *
FROM employees
WHERE employee_id < 150;

CREATE OR REPLACE VIEW v2 AS
SELECT v1.employee_id employee_id, departments.department_id department_id
FROM v1, departments
WHERE v1.department_id = departments.department_id;

SELECT /*+ NO_MERGE(v2) INDEX(v2.v1.employees emp_emp_id_pk)
FULL(v2.departments) */ *
FROM v2
WHERE department_id = 30;

miércoles, 10 de septiembre de 2008

Roles en Oracle

Generalmente cuando uno toma el curso de Workshop I de 10g, y llega a la parte de Roles (lección 6), se habla muy poco de cómo se pueden manejar los roles, e incluso no hay un ejemplo claro del mismo.

Las veces que me toca dar el curso a mi, me gusta explicar a fondo lo más que puedo acerca de roles.

Se nos dice que un rol, es asignado por default a un usuario. Un rol, puede ser asegurado de manera adicional, que por default, no trae seguridad alguna.

Entonces, partiendo de la teoría, un rol puede ser creado con la siguiente seguridad:

* Ninguna (Default)
* Password
* External
* Global

Por falta de hardware/software, no puedo demostrar la seguridad Global, pero las demás sí.

Empecemos por crear un usuario, solo para no perder la costumbre, crearé un usuario identificado a nivel sistema operativo.


C:\>sqlplus "/ AS SYSDBA"

Conectado a:
Oracle Database 10g Enterprise Edition Release 10.2.0.3.0 - Production
With the Partitioning, OLAP and Data Mining options


SQL> CREATE USER "OPS$HUGO-WIN\HUGO" IDENTIFIED externally;

Usuario creado.

SQL> GRANT CONNECT TO "OPS$HUGO-WIN\HUGO";

Concesión terminada correctamente.



Ahora nos conectamos como el usuario creado, identificado de manera externa, y validamos el primer rol asignado "CONNECT"


SQL> conn /
Conectado.

SQL> show user
USER es "OPS$HUGO-WIN\HUGO"

SQL> select * from session_roles;

ROLE
------------------------------
CONNECT



Aquí validamos dos cosas, la primera es que el rol de create session viene implícito en el connect, ya que de otra forma no hubieramos podido crear la sesión; y lo segundo es que el rol asignado, está activo por default.

Ahora crearemos un rol identificado por un password


SQL> conn / as sysdba
Conectado.

SQL> CREATE role dba_pass IDENTIFIED BY supersecreto;

Rol creado.

SQL> GRANT DBA TO dba_pass;

Concesión terminada correctamente.

SQL> GRANT dba_pass TO "OPS$HUGO-WIN\HUGO";

Concesión terminada correctamente.


Ya que se tiene asignado al usuario el nuevo rol, nos conectamos como el usuario


SQL> conn /
Conectado.

SQL> show user
USER es "OPS$HUGO-WIN\HUGO"
SQL> select * from session_roles;

ROLE
------------------------------
CONNECT
DBA_PASS
DBA
SELECT_CATALOG_ROLE
HS_ADMIN_ROLE
EXECUTE_CATALOG_ROLE
DELETE_CATALOG_ROLE
EXP_FULL_DATABASE
IMP_FULL_DATABASE
GATHER_SYSTEM_STATISTICS
SCHEDULER_ADMIN
WM_ADMIN_ROLE
JAVA_ADMIN
JAVA_DEPLOY
XDBADMIN
XDBWEBSERVICES
OLAP_DBA

17 filas seleccionadas.


Como vemos, el rol está asignado por default, y no nos pide el password nunca, lo que debemos hacer si es que queremos que el usuario use el password, es quitarlo del default del usuario.


SQL> conn / as sysdba
Conectado.

SQL> ALTER USER "OPS$HUGO-WIN\HUGO"
2 DEFAULT ROLE ALL EXCEPT dba_pass;

Usuario modificado.

SQL> conn /
Conectado.

SQL> show user
USER es "OPS$HUGO-WIN\HUGO"
SQL> select * from session_roles;

ROLE
------------------------------
CONNECT



Ahora intentamos usar el rol, validando que se requiere un password.


SQL> show user
USER es "OPS$HUGO-WIN\HUGO"

SQL> SET role dba_pass;
SET role dba_pass
*
ERROR en línea 1:
ORA-01979: falta la contraseña para el rol 'DBA_PASS' o no es válida


SQL> SET role dba_pass IDENTIFIED BY supersecreto;

Rol definido.

SQL> SELECT COUNT(1)
2 FROM v$session;

COUNT(1)
----------
15


Ahora crearemos un rol identificado a través de un procedimiento.

Lo primero es crear el procedimiento.


SQL> conn / as sysdba
Conectado.

SQL> CREATE OR REPLACE PROCEDURE sec_roles
2 authid CURRENT_USER AS
3 usuario VARCHAR2(50);
4 BEGIN
5 usuario := LOWER((sys_context('userenv', 'session_user')));
6 DBMS_OUTPUT.PUT_LINE(usuario);
7 IF UPPER(usuario) = 'OPS$HUGO-WIN\HUGO' THEN
8 dbms_session.set_role('DBA_PROC');
9 ELSE
10 NULL;
11 END IF;
12 END;
13 /

Procedimiento creado.


Es importante el punto de AUTHID, debe estar en current user si no, no funcionaría. Ya que se tiene el procedimiento creado, continuamos con el resto del rol y grants necesarios al usuario.



SQL> GRANT EXECUTE ON sec_roles TO "OPS$HUGO-WIN\HUGO";

Concesión terminada correctamente.

SQL> CREATE role dba_proc IDENTIFIED USING sys.sec_roles;

Rol creado.

SQL> GRANT DBA TO dba_proc;

Concesión terminada correctamente.

SQL> GRANT EXECUTE ON sys.dbms_session TO "OPS$HUGO-WIN\HUGO";

Concesión terminada correctamente.


Es necesario que el usario que queremos que use el rol, tenga un grant al procedimiento de sys.sec_roles, y tambien permisos de ejecución en sys.dbms_session.


SQL> conn /
Conectado.

SQL> select * from session_roles;

ROLE
------------------------------
CONNECT

SQL> exec sys.sec_roles;
ops$hugo-win\hugo

Procedimiento PL/SQL terminado correctamente.

SQL> select * from session_roles;

ROLE
------------------------------
DBA_PROC
DBA
SELECT_CATALOG_ROLE
HS_ADMIN_ROLE
EXECUTE_CATALOG_ROLE
DELETE_CATALOG_ROLE
EXP_FULL_DATABASE
IMP_FULL_DATABASE
GATHER_SYSTEM_STATISTICS
SCHEDULER_ADMIN
WM_ADMIN_ROLE
JAVA_ADMIN
JAVA_DEPLOY
XDBADMIN
XDBWEBSERVICES
OLAP_DBA

16 filas seleccionadas.

SQL>



Hay que notar que al usuario "ops$hugo-win\hugo" jamás se le dio un grant sobre el ROL, el ROL lo obtiene con la ejecución del procedimiento.

¿Alguien tendrá un ejemplo de la autenticación global para compartir?

viernes, 5 de septiembre de 2008

Operadores de Conjunto (caso práctico)

En la compañía, se está desarrollando una aplicación para un cliente. Ésta sirve para "clonar" esquemas dentro de una base de datos, con la flexibilidad de decidir los objetos, campos, constraints, etc. que quieres llevarte.

Una problemática que se presentó, fue la siguiente:

¿Cómo saber qué constraints se pueden replicar con una tabla que no se migra con todas las columnas?

Suponiendo que tenemos una tabla original con 3 campos, y la tabla destino, sólo contiene dos campos. Imaginemos un constraint de tipo "check" que valida que el (campo1 = 'true') or (campo2 = 'false') or (campo3 = 'lo que sea'). No podemos replicar ese constraint al nuevo esquema, ya que marcaría un error. Así que ¿cómo pudiéramos revisar, basado en nuestras columnas seleccionadas, los constraints que se pueden migrar sin problema?

La solución se puede lograr fácilmente con lógica de conjuntos



WITH registros AS
(SELECT --+materialize
constraint_name,
column_name
FROM dba_cons_columns
WHERE TABLE_NAME = 'SDO_COORD_REF_SYS')
SELECT constraint_name,
COUNT(1) numero
FROM registros
WHERE column_name IN('GEOG_CRS_DATUM_ID',
'SOURCE_GEOG_SRID')
GROUP BY constraint_name
INTERSECT
SELECT constraint_name,
COUNT(1)
FROM registros
GROUP BY constraint_name;


Primero explicaré brevemente sobre la clausula WITH.

La clausula with, que nace con Oracle 9i Release 1, en lugar de repetir código complejo, o pesado, nos permite darle un nombre, y reusar la referencia a través del nombre múltiples veces dentro de una sentencia SQL. Forma parte del estándar SQL-99. Jonathan Lewis, adicionalmente incluye el hint de "materialize" en las cláusulas with, ya que de esta forma, se materializa el resultado en una tabla temporal y al seleccionarse en múltiples partes, el resultado materializado se accede de forma más rápida.

En este ejemplo, como sé que voy a trabajar con los constraints de una sola tabla, mi universo de datos son todos los registros contenidos en "dba_cons_columns" para la tabla en cuestión. De ahí el with


WITH registros AS
(SELECT --+materialize
constraint_name,
column_name
FROM dba_cons_columns
WHERE TABLE_NAME = 'NOMBRE_TABLA')


De ahí, necesito obtener los constraints que incluyan mis columnas que voy a replicar, o por lo menos alguna de las columnas. Añado un "count" para saber cuántas de mis columnas son incluidas en el constraint.


SELECT constraint_name,
COUNT(1) numero
FROM registros
WHERE column_name IN('CAMPOS',
'A',
'BUSCAR')
GROUP BY constraint_name


Finalmente para terminar, necesito saber cuántas columnas en total tiene cada constraint.


SELECT constraint_name,
COUNT(1)
FROM registros
GROUP BY constraint_name


Teniendo esos sets de datos, usamos lógica de conjuntos y hacemos una intersección con la palabra "INTERSECT"


SQL> WITH registros AS
2 (SELECT --+materialize
3 constraint_name,
4 column_name
5 FROM dba_cons_columns
6 WHERE TABLE_NAME = 'SDO_COORD_REF_SYS')
7 SELECT constraint_name,
8 COUNT(1) numero
9 FROM registros
10 WHERE column_name IN('GEOG_CRS_DATUM_ID',
11 'SOURCE_GEOG_SRID')
12 GROUP BY constraint_name
13 INTERSECT
14 SELECT constraint_name,
15 COUNT(1)
16 FROM registros
17 GROUP BY constraint_name;

CONSTRAINT_NAME NUMERO
------------------------------ ----------
COORD_REF_SYS_FOREIGN_GEOG 1

SQL>


Esto nos da el resultado que si voy a incluir sólo las columnas 'GEOG_CRS_DATUM_ID', 'SOURCE_GEOG_SRID', sólo puedo llevarme el contraint COORD_REF_SYS_FOREIGN_GEOG.

Pasando ya de lleno a la lógica de conjuntos, Oracle cuenta con tres operadores de conjuntos

UNION [ALL]
MINUS
INTERSECT

Se pueden usar para unir varios queries y llevar a cabo la operación de conjunto deseada, llevan orden de precedencia a menos que vengan entre paréntesis.


SQL> SELECT *
2 FROM
3 (SELECT 1 FROM dual
4 UNION ALL
5 SELECT 1 FROM dual)
6 INTERSECT
7 (SELECT 1 FROM dual
8 UNION ALL
9 SELECT 2 FROM dual)
10 ;

1
----------
1



Las expresiones correspondientes en las sentencias select, deben de coincidir en número y tipo, si no se sigue esto, oracle genera un error.

Hay algunas observaciones por considerar.

No se pueden usar operadores de conjuntos en columnas de tipo BLOB, CLOB, BFILE, VARRAY, o "nested tables".

No son válidos en columnas de tipo long.

Los operadores MINUS, UNION e INTERSECT terminan agrupando los resultados. Union All no agrupa y sólo junta los resultados de las dos fuentes de datos.

Y la más importante de todas las consideraciones es que Oracle para apegarse a un estándar, cambiará (no sé si el release 11g ya lo tenga) la precedencia del comando INTERSECT para que se evalue primero, por esta razón, Oracle recomienda siempre usar paréntesis cuando se use "INTERSECT".

jueves, 28 de agosto de 2008

Passwords en Oracle

Desde hace mucho tiempo, se conoce la forma en que el hash de oracle se calcula para las versiones menores a 11g. E incluso se conoce el algoritmo del cálculo del hash para 11g.

Lo que es importante notar es que con el poder de cómputo de estos días, y con la debilidad del password de oracle, si no usamos passwords difíciles de crackear, podemos tener un riesgo de seguridad muy grande.

A continuación expongo un script, sólo con fines ilustrativos, la forma en que oracle calcula el hash

hashoracle.pl


use warnings;
use strict;
use Crypt::CBC;
use Encode;
use Unicode::String qw(utf8 latin1 utf16);

my $llave = pack("H16", "0123456789ABCDEF");
my $usuario = 'Hugo';
my $password = 'abc123';

my $textoplano = uc($usuario.$password);
my $utf16 =utf8($textoplano);
my $octeto = $utf16->utf16;
my $IV = pack("H16", "0000000000000000");
my $cifrado = Crypt::CBC->new(
-key => $llave,
-cipher => 'DES',
-iv => $IV,
-header => 'none',
-padding => 'null',
-literal_key=>1,
);

my $textocifrado = $cifrado->encrypt($octeto);
my $cifrado1 = Crypt::CBC->new(
-key => substr($textocifrado, length($textocifrado)-8,8),
-cipher => 'DES',
-iv => $IV,
-header => 'none',
-padding => 'null',
-literal_key=>1,
);

my $textocifrado1 = $cifrado1->encrypt($octeto);
my( $hex1 ) = unpack( 'H*', $textocifrado1 );
my $temp1 = uc(substr($hex1,length($hex1)-16,16));
print "\n$temp1\n";
__END__



Si este script se pusiera en un ciclo infinito a buscar passwords para hacer un match contra un hash preestablecido, podría buscar aproximadamente 800,000 hashes por segundo, si este mismo script se pasara a C, podría buscar aproximadamente 1,300,000 hashes por segundo. Alex Kornbust tiene un programa que incluso puede hacer uso de threads en los CPUs y genera hasta 2,400,000 por segundo.

Así que pensemos que, de acuerdo a estudios realizados, un password de 8 caracteres, puede ser descubierto en tan sólo 20 días, de 9 caracteres, ya puede pasar del año, pero teniendo 40 computadoras descifrando passwords, el tiempo vuelve a 20 días, así que ¿cuál es mi recomendación?

Seguir de lleno las recomendaciones de Oracle:

- Crea los passwords entre 8 y 30 caracteres (yo recomendaría de 10 a 30 caracteres).
- Utiliza símbolos del Character Set para definir los passwords (#$_).
- Además de incluir un dígito y un alfanumérico, también incluye un signo de puntuación.
- No inicies un password con un número.
- No utilices palabras reservadas de oracle.
- No uses palabras de diccionario.

Adicionalmente podemos crear passwords más complejos, pero esto también nos limita el uso un poco:

Empecemos con passwords entre comillas


C:\>sqlplus / as sysdba

SQL*Plus: Release 10.2.0.3.0 - Production on Jue Ago 28 11:56:39 2008

Copyright (c) 1982, 2006, Oracle. All Rights Reserved.


Conectado a:
Oracle Database 10g Enterprise Edition Release 10.2.0.3.0 - Production
With the Partitioning, OLAP and Data Mining options


SQL> alter user hugo identified by "de la sierra morena cielito";

Usuario modificado.

SQL> conn hugo/"de la sierra morena cielito";
Conectado.



En este punto tenemos un pequeño problema, o quizás sólo yo, pero no he podido pasar a nivel sistema operativo mi password, es decir, la conexión la debo de realizar con /nolog, o bien usando únicamente el usuario y esperar a que sqlplus nos pregunte el password.


C:\>sqlplus hugo/"de\ la\ sierra\ morena\ cielito"

SQL*Plus: Release 10.2.0.3.0 - Production on Jue Ago 28 12:00:23 2008

Copyright (c) 1982, 2006, Oracle. All Rights Reserved.

ERROR:
ORA-01017: invalid username/password; logon denied


C:\>sqlplus hugo

SQL*Plus: Release 10.2.0.3.0 - Production on Jue Ago 28 12:01:43 2008

Copyright (c) 1982, 2006, Oracle. All Rights Reserved.

Introduzca la contraseña:

Conectado a:
Oracle Database 10g Enterprise Edition Release 10.2.0.3.0 - Production
With the Partitioning, OLAP and Data Mining options


SQL>


C:\>sqlplus /nolog

SQL*Plus: Release 10.2.0.3.0 - Production on Jue Ago 28 12:02:43 2008

Copyright (c) 1982, 2006, Oracle. All Rights Reserved.


SQL> conn hugo/"de la sierra morena cielito";
Conectado.



Algo de lo que podemos hacer uso, como bien nos recomienda oracle, es el uso de caracteres especiales, por ejemplo:



C:\>set NLS_LANG=spanish_america.we8pc850

C:\>sqlplus / as sysdba

SQL*Plus: Release 10.2.0.3.0 - Production on Jue Ago 28 12:05:04 2008

Copyright (c) 1982, 2006, Oracle. All Rights Reserved.


Conectado a:
Oracle Database 10g Enterprise Edition Release 10.2.0.3.0 - Production
With the Partitioning, OLAP and Data Mining options


SQL> alter user hugo identified by querétaro;

Usuario modificado.



Al hacer esto, nos aseguramos que los passwords sólo funcionan con nuestro caracter set o alguno muy similar:


C:\>set NLS_LANG=spanish_america.we8iso8859p1

C:\>sqlplus /nolog

SQL*Plus: Release 10.2.0.3.0 - Production on Jue Ago 28 12:08:18 2008

Copyright (c) 1982, 2006, Oracle. All Rights Reserved.


SQL> conn hugo/querétaro
ERROR:
ORA-01017: invalid username/password; logon denied

SQL> exit

C:\>set NLS_LANG=spanish_america.utf8

C:\>sqlplus /nolog

SQL*Plus: Release 10.2.0.3.0 - Production on Jue Ago 28 12:09:14 2008

Copyright (c) 1982, 2006, Oracle. All Rights Reserved.


SQL> conn hugo/querétaro
ERROR:
ORA-01017: invalid username/password; logon denied

SQL> exit

C:\>set NLS_LANG=spanish_america.we8pc850

C:\>sqlplus /nolog

SQL*Plus: Release 10.2.0.3.0 - Production on Jue Ago 28 12:09:37 2008

Copyright (c) 1982, 2006, Oracle. All Rights Reserved.


SQL> conn hugo/querétaro
Conectado.


De esta forma podemos asegurar más nuestros passwords y asegurar de una mejor forma nuestra base de datos.

lunes, 25 de agosto de 2008

Rman y cómo catalogar Backups

Los esquemas de respaldo de todo mundo suelen ser muy variados, muy rara vez te encuentras con configuraciones iguales (BCVs, scripts, rman, veritas, HP, Tivoli, cartucheras virtuales, etc...). De manera personal, creo que lo más sencillo o fácil de llevar como DBA, es tener un software que administre la parte de backups y de restore de forma integrada con RMAN.

Suponiendo que tengamos Dataprotector, TSM, Netbackup, etc. integrado a RMAN, lo más sencillo es correr scripts de backup hacia canales de cinta de forma directa, mantener nuestras políticas de retención en RMAN y poder expirar y borrar de las cintas desde RMAN. Esto como DBAs nos da mucha visión de cómo se van llevando nuestros Backups, podemos generar los reportes necesarios con comandos sencillos de RMAN, etc...

Me he encontrado con algunos clientes, y recientemente a través de unos correos, con que mucha gente hace los respaldos a disco, una vez que el respaldo está en disco, se utiliza una cartuchera para llevarse el respaldo a cinta.

En este ejemplo en particular, imaginemos que el espacio en disco es reducido, y que sólo se mantiene el último backup en disco, y como se tiene respaldado en cinta la información, se pueden expirar y borrar los backups de disco anteriores.

Este es un ejemplo práctico de cómo llevar un backup, pero hay que tener varias consideraciones al hacerlos:

Debes de asegurarte de llevar un backup del controlfile. Recuerda que si tienes activado el backup automático de controlfile, y no incluyes el controlfile en tu backup, el resplado del controlfile pudier estar en otro PATH distinto a tus backupsets, considera añadir ese path a tu respaldo en cinta. Si ejecutas


RMAN> backup database include current controlfile;


El controlfile estará en el mismo path que tus backupsets.

Una vez expirado y eliminado el backup del día anterior, imaginemos que nos damos cuenta que un error de datos se tiene desde ayer y queremos recuperar la base de datos a un punto anterior (hace dos días), por lo cual el backup del día de hoy no nos sirve... ¿Qué se puede hacer para recuperar la información?, vamos a poner el ejemplo que se va a restaurar el respaldo de hace 2 días en un servidor nuevo para consultar la información sin afectar producción.

Como primer punto es, en un area de storage (en el server de desarrollo) hay que bajar el respaldo de cinta, eso lo puedes hacer con herramientas de sistema operativo o el software que usas.

Una vez que tienes tus backupsets en disco, existen 2 posibilidades, que hayas respaldado el controlfile o que no lo hayas respaldado.

En caso de que lo hayas respaldado, lo más sencillo es restaurarlo del backupset:


RMAN> SET DBID 524232147;

ejecutando el comando: SET DBID

RMAN> RUN
2> {
3> RESTORE CONTROLFILE FROM 'D:\RESPALDO\TAG20080823T172009_48DQF74D_.BKP
4> }



Este comando nos restaurará el controlfile contenido en el backupset (revisen sus logs de RMAN para poder saber cual es el backupset piece correcto), el destino del o de los controlfiles será el asignado en el parámetro control_files del archivo de inicialización (init o spfile).

Si no se respaldó el controlfile, se puede usar uno de producción, no importa que nuestro respaldo de hace 2 días no esté en el catálogo.

Una vez que ya tenemos un controlfile (el restaurado, o bien, el copiado de producción), tenemos 2 posibles escenarios:

El primero es que tengamos el controlfile del respaldo y que el path usado en el respaldo, sea el mismo usado para depositar los archivos en el server de desarrollo (en caso de Linux o Unix se puede usar un link simbólico). Si este es el caso, procedemos a montar la base de datos y empezar nuestro restore y recover:


RMAN> alter database mount;

base de datos montada
canal liberado: ORA_DISK_1

RMAN> RESTORE DATABASE;

RMAN> RECOVER DATABASE;

RMAN> ALTER DATABASE OPEN RESETLOGS;



Pero no siempre podemos tener la suerte de tener el mismo path, o tampoco contamos con un controlfile sin el registro de nuestro backup. ¿Qué podemos hacer en este caso?

Catalogar nuestro backup

No importa si se usa una base de datos de catálogo de rman o no, lo importante es que nuestro controlfile sepa que el respaldo de nuestra base de datos existe, la forma es la siguiente.

Reviso que no tenga un backup registrado, o por lo menos no uno que me sirva en mi controlfile:


RMAN> list backup of database;

RMAN>


Y ahora usando el controlfile actual de mi base de datos (restaurado o copiado), registro la copia del respaldo que tengo.



RMAN> CATALOG START WITH 'd:\RESPALDO';

buscando todos los archivos que coincidan con el patr¾n C:\oracle\product\flash_recovery_area\ORCL\BAC

Lista de Archivos Desconocidos para la Base de Datos
=====================================
Nombre de Archivo: d:\RESPALDO\respaldo.bkp

+Seguro que desea catalogar los archivos anteriores (introduzca SÝ o NO)? y
catalogando archivos...
catalogaci¾n realizada

Lista de Archivos Catalogados
=======================
Nombre de Archivo: d:\RESPALDO\respaldo.bkp



En este punto reviso que mi backup exista en la base de datos


RMAN> list backup of database;


Lista de Juegos de Copias de Seguridad
===================

Clave BS Tipo LV Tama±o Tipo de Dispositivo Tiempo Transcurrido Hora de Finalizaci¾n
------- ---- -- ---------- ----------- ------------ --------------------
56 Full 1.01G DISK 00:02:17 23/08/08
Clave BP: 58 Estado: AVAILABLE Comprimido: NO Etiqueta: TAG20080823T120433
Nombre de Parte: d:\RESPALDO\RESPALDO.BKP
Lista de Archivos de Datos en el juego de copias de seguridad 56
Tipo de Archivo LV SCN Pto. Ctrl. Hora de Punto de Control Nombre
---- -- ---- ---------- ------------------------ ----
1 Full 4921504 23/08/08 C:\ORACLE\PRODUCT\ORADATA\ORCL\SYSTEM01.DBF
2 Full 4921504 23/08/08 C:\ORACLE\PRODUCT\ORADATA\ORCL\UNDOTBS01.DBF
3 Full 4921504 23/08/08 C:\ORACLE\PRODUCT\ORADATA\ORCL\SYSAUX01.DBF
4 Full 4921504 23/08/08 C:\ORACLE\PRODUCT\ORADATA\ORCL\USERS01.DBF
5 Full 4921504 23/08/08 C:\ORACLE\PRODUCT\ORADATA\ORCL\EXAMPLE01.DBF
6 Full 4921504 23/08/08 C:\CRYPTO\ORCL\DATAFILE\O1_MF_ORCL_42K6N37T_.DBF
7 Full 4921504 23/08/08 C:\PRUEBA01.DBF


Aquí puedo comenzar con mi restore desde mis backupsets recientemente registrados


RMAN> shutdown abort

RMAN> startup mount

conectado a la base de datos destino (no iniciada)
instancia Oracle iniciada
base de datos montada

Total del -rea Global del Sistema 314572800 bytes

Fixed Size 1290328 bytes
Variable Size 142610344 bytes
Database Buffers 163577856 bytes
Redo Buffers 7094272 bytes

RMAN> restore database;

Iniciando restore en 23/08/08
canal asignado: ORA_DISK_1
canal ORA_DISK_1: sid=540 devtype=DISK

canal ORA_DISK_1: iniciando restauraci¾n del juego de copias de seguridad de archivos de datos
canal ORA_DISK_1: especificando archivo(s) de datos para restaurar del juego de copias de seguridad
restaurando el archivo de datos 00001 en C:\ORACLE\PRODUCT\ORADATA\ORCL\SYSTEM01.DBF
restaurando el archivo de datos 00002 en C:\ORACLE\PRODUCT\ORADATA\ORCL\UNDOTBS01.DBF
restaurando el archivo de datos 00003 en C:\ORACLE\PRODUCT\ORADATA\ORCL\SYSAUX01.DBF
restaurando el archivo de datos 00004 en C:\ORACLE\PRODUCT\ORADATA\ORCL\USERS01.DBF
restaurando el archivo de datos 00005 en C:\ORACLE\PRODUCT\ORADATA\ORCL\EXAMPLE01.DBF
restaurando el archivo de datos 00006 en C:\CRYPTO\ORCL\DATAFILE\O1_MF_ORCL_42K6N37T_.DBF
restaurando el archivo de datos 00007 en C:\PRUEBA01.DBF
canal ORA_DISK_1: leyendo desde la parte de copia de seguridad d:\RESPALDO\RESPALDO.BKP


Consideración adicional, usen el comando set new name en caso de que deban restaurar a un path distinto.

Espero que les sea de utilidad, ya sea para clonar una base de datos o simplemente recuperar la información.

viernes, 15 de agosto de 2008

Query para ver lo que está sucediendo

Recientemente Tanel Poder en su blog publicó esta entrada. Vale mucho la pena el enfoque qué él le da.

Generalmente cuando me toca ir con algún cliente por algún tema de performance, siempre suele haber dos situaciones distintas:


1) Están por correr procesos pesados y se quiere una foto de todo lo que está sucediendo en el sistema.

2) El sistema da la apariencia de estar pasmado.

Para el primer caso, lo que puedo sugerir es la activación de statspack



SQL> @?/rdbms/admin/spcreate.sql

Creating Package STATSPACK...

Paquete creado.

No hay errores.
Creating Package Body STATSPACK...

Cuerpo del paquete creado.

No hay errores.

NOTE:
SPCPKG complete. Please check spcpkg.lis for any errors.

SQL> conn perfstat
Introduzca la contraseña:
Conectado.

SQL> exec statspack.snap(I_SNAP_LEVEL=>7);


A partir de este punto, dejo que corran todo lo que deben de correr, y después de unos 15 mins, o que los procesos han terminado, vuelvo a ejecutar


SQL> exec statspack.snap(I_SNAP_LEVEL=>7);


Genero los reportes diferenciales, y analizo la información



SQL> define begin_snap=1;
SQL> define end_snap=2;
SQL> @?/rdbms/admin/spreport.sql


Hay que recordar que toda la información es generalizada y agregada es decir, supongamos que tenemos lo siguiente:


Top 5 Timed Events
~~~~~~~~~~~~~~~~~~
Event Waits Time (s)
----------------------------------------- ------------ -----------
CPU time 15,000
db file scattered read 21,156 830
control file sequential read 1,261 10
....


Y esto en un periodo de 1 Hora entre snapshots, debemos de tomar en cuenta que la información es agregada.

En este ejemplo (totalmente ficticio), pareciera que la instancia tiene problemas de CPU, pero si pensáramos que el equipo cuenta con 32 procesadores, estaríamos viendo que en promedio cada CPU se ocupó 13.5% y el problema podría venir por otro lado.


Para las situaciones en donde el sistema aparenta estar pasmado, o se quiere saber lo que está sucediendo, suelo tener 3 sentencias SQL (la primera muy similar a la de Tanel)


SQL> select count(1), event
2 from v$session_wait
3 group by event
4 order by 1 desc;

COUNT(1) EVENT
---------- -------------------------------------
19 enq: TM - contention
12 rdbms ipc message
1 jobq slave wait
1 SQL*Net message to client
1 smon timer
1 pmon timer
1 SQL*Net message from client

7 filas seleccionadas.


Esto nos dice que hay 19 sesiones sufriendo de encolamiento (pero lo mismo puede aplicar para cualquier wait != Idle)


SQL> select count(1), s.sql_hash_value, sw.event
2 from v$session s, v$session_wait sw
3 where s.sid=sw.sid
4 group by s.sql_hash_value,sw.event
5 order by 1 desc;

COUNT(1) SQL_HASH_VALUE EVENT
---------- -------------- ----------------------------
19 3864513861 enq: TM - contention
11 0 rdbms ipc message
1 2390948774 SQL*Net message to client
1 0 smon timer
1 3393152264 rdbms ipc message
1 0 pmon timer
1 0 jobq slave wait
1 0 SQL*Net message from client

8 filas seleccionadas.

Y finalmente, podemos ir al Shared pool a ver el plan de ejecución


SQL> select * from table(dbms_xplan.display_cursor(3864513861));

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------

HASH_VALUE 3864513861, child number 0
--------------------------------------
lock table hilos in exclusive mode

Plan hash value: 2876205575

-----------------------------------
| Id | Operation | Name |
-----------------------------------
| 0 | DDL STATEMENT | |
| 1 | TABLE ACCESS FULL| HILOS |
-----------------------------------


El último query sólo se puede hacer en instancias 10g+, en caso de nueve, se deberá buscar el plan de ejecución en V$SQL_PLAN.

Si se está usando 10g, tambien se puede evitar el acceso a la vista v$session_wait, ya que v$session contiene la información de eventos de espera.


SQL> select count(1) ,sql_hash_value, event
2 from v$session
3 group by sql_hash_value, event
4 order by 1 desc;

COUNT(1) SQL_HASH_VALUE EVENT
---------- -------------- -----------------------------
19 3864513861 enq: TM - contention
11 0 rdbms ipc message
1 0 SQL*Net message from client
1 0 smon timer
1 0 pmon timer
1 2593210877 SQL*Net message to client
1 0 jobq slave wait
1 3393152264 rdbms ipc message

8 filas seleccionadas.

lunes, 4 de agosto de 2008

Sesiones colgadas de Forms en E-business

Muy a menudo, me encuentro con clientes que quieren agregar muchos nodos aplicativos a un E-business suite porque tienen mucha carga en los nodos, y aparentemente no soportan más conexiones.

Sé que esto no va relacionado a base de datos completamente, pero para los Applications DBAs, suele ser un dolor de cabeza el estar revisando las cargas de CPU en los servidores de aplicaiones.

Me ha tocado administrar instancias de E-Business que tienen más de 2,000 usuarios concurrentes con sólo 2 servidores aplicativos, y el desempeño es excelente. Me ha tocado estar en ambientes donde la carga de un servidor de aplicación es altísima y sólo tiene 50 usuarios concurrentes.

La razón parece estar siempre ligada a procesos que se quedan en el limbo o en "stupid mode". Cada uno de estos procesos puede consumir hasta el 100% de un CPU, y si tenemos 10 de estos procesos, y sólo 6 procesadores, el servidor "estará corriendo como un perro con dos patas".



Hace un tiempo hice un script para facilitar la detección de estos procesos y finalmente eliminarlos con la tranquilidad de que no son procesos que existen en la aplicación.

sesiones_colgadas.sh


#!/usr/bin/ksh
##########################################
# Script para eliminar las sesiones
# colgadas de forms.
#
# Valida que las sesiones de forms
# Consuman mucho CPU y lleven corriendo
# mas de 5 minutos. Si cumplen estas
# caracteristicas, se evaluan a nivel
# base de datos, si no hay proceso
# relacionado, el cliente se mata.
#
#
# Elaborado por: Hugo E. Contreras
# Fecha: Abril 10, 2008
#
# Resource IT
##########################################

# Variables de ambiente
export ORACLE_HOME=/u01/oracle/PROD/apps/tech_st/10.1.2
export TWO_TASK=PROD
export USUARIO=monitor
export PASSWORD=******


for SerPid in `ps -eo pcpu,pid,etime,cmd|grep frmweb|sort -n|awk '$1 >10 && $3 > "05:00" {print $2}'`
do
ABC=`sqlplus -s $USUARIO/$PASSWORD << EOF
set head off
set pagesize 0
set feed off
select count(1) from v\\$session where process ='$SerPid';
exit;
EOF`

echo "el Sid $SerPid tiene $ABC sesion(es) en la base de datos"

if [ $ABC -lt 1 ]
then
echo "Se procede a matar el proceso $SerPid"
kill -9 $SerPid
fi

done



Hay muchas cosas a configurar en este script, y dependiendo del release del e-business, algo más puede cambiar, estas son algunas consideraciones que se deben de tener.

Si es 11i, el ORACLE_HOME a usar es el 8.0.6, en este caso el script está pensado para 12.0.3. En el caso de 11i, el proceso a buscar es f60webmx, para 12i es frmweb.

En caso de tener un RAC configurado como base de datos, lo primero es usar la vista GV$SESSION en lugar de V$SESSION. Algo adicional, es que se debe de evaluar el nombre del equipo, es decir

AND machine = `hostname`

Ya que pudiera ser que alguna instancia del RAC tuviera un proceso padre (gv$session.process) con el mismo número en una instancia distinta.

En caso de tener más de 1 application server, sea o no un RAC, también se debe poner la condición

AND machine = `hostname`

Esto es porque dos o más servidores de aplicación pudieran usar el mismo spid para un proceso de forms.

Alguna consideración en el caso de solaris; el comando

ps -eo pcpu,pid,etime,cmd

debe ser cambiado por

ps -eo pcpu,pid,etime,comm

Seguramente muchas otras cosas pueden cambiar entre HP-UX, Aix, Solaris o diferentes sabores de Linux, pero la idea es casi la misma:

Obtener la lista de procesos que contienen el ejecutable que nos interesa, buscar sólo aquellos que consumen más del 10% de 1 CPU y que lleven por lo menos 5 minutos activas.

Cuando todas estas condiciones se cumplen, se evalúa contra la base de datos para corroborar que el proceso hijo todavía existe, en caso de no existir, el proceso es eliminado a través de un kill -9.

El script se debe de ejecutar en el servidor de aplicación.