miércoles, 28 de octubre de 2009

Cambia Puerto http del EM de Oracle XE - (XE: Changing the default http port )

Determinas la configuracion actual setea en tu Oracle XE. Se accede mediante SQLPLUS con usuario SYSTEM (o cualquier otro con privilegios de DBA):

C:\WINDOWS\system32>sqlplus system@xe

SQL*Plus: Release 10.1.0.2.0 - Production on Mi Jan 25 11:44:33 2006

Copyright (c) 1982, 2004, Oracle. All rights reserved.

Enter password:

Connected to:
Oracle Database 10g Express Edition Release 10.2.0.1.0 - Beta

SQL> -- get current status
SQL> select dbms_xdb.gethttpport as "HTTP-Port"
, dbms_xdb.getftpport as "FTP-Port" from dual;

HTTP-Port FTP-Port
---------- ----------
8080 0

Tu puedes cambiar el puerto HTTP y el puerto de FTP cuando quieras solo ten algunas precausiones tales como
  • Tener en cuenta que se necesita privilegios especiales para los puertos <1024>
  • debes chequear los puertos utilizados (netstat -ano)
  • Que el parametro del httpport define el puerto que correra el EM.


SQL> -- set http port and ftp port
SQL> begin
2 dbms_xdb.sethttpport('80'); --El valor 80 es el nuevo parametro
3 dbms_xdb.setftpport('2100'); --El valor 80 es el nuevo parametro
4 end;
5 /

PL/SQL procedure successfully completed.

SQL> select dbms_xdb.gethttpport as "HTTP-Port"
, dbms_xdb.getftpport as "FTP-Port" from dual;

HTTP-Port FTP-Port
---------- ----------
80 2100

Si sólo deseas utilizar la base de datos sin permitir el acceso a través de HTTP o FTP, puedes deshabilitar estas opciones de la siguiente manera:


SQL> -- disable http and ftp access
SQL> begin
2 dbms_xdb.sethttpport('0');
3 dbms_xdb.setftpport('0');
4 end;
5 /

PL/SQL procedure successfully completed.

SQL> -- get current status
SQL> select dbms_xdb.gethttpport as "HTTP-Port"
, dbms_xdb.getftpport as "FTP-Port" from dual;

HTTP-Port FTP-Port
---------- ----------
0 0

martes, 27 de octubre de 2009

BULK COLLECT con miles de registros y la cláusula LIMIT


Veo muchísimas veces, que cuando se utiliza BULK COLLECT en los códigos PL/SQL, no se coloca la cláusula LIMIT. Cuando no utilizamos ésta cláusula, lo único que ganamos es arruinar la memoria del proceso.
La cláusula LIMIT nos permite definir la cantidad de 'datos' que vamos a colocar en memoria. Cuando utilizamos LIMIT, lo ideal es definirlo en un valor entre 100 a 500. Personalmente, elijo el valor 100 porque en base a mi experiencia suele ser el mejor valor. Pero porque elijo un valor tan chico y no 1000 o 5000 por ejemplo? Bueno, por la simple razón que manejar gran cantidad de datos en memoria es más costoso que manejar poca cantidad.
Si elegimos un valor alto en el LIMIT, puede darse 3 casos:
- Que nuestro código se ejecute más rápidamente (improbable).
- Que nuestro código se ejecute en igual tiempo (improbable).
- Que nuestro código se ejecute más lentamente (probable).

Veamos un ejemplo para entender mejor las consecuencia de no utilizar la cláusula LIMIT.

Primero creamos una tabla TEST con 10.000 registros (para ver la diferencia del uso de la cláusula LIMIT, no hace falta realizar el ejemplo con millones de registros. Con unos miles de registros nos alcanza para entender el tema) y una tabla TEST_2 con la estructura de la tabla TEST pero sin registros:

SQL_9iR2> CREATE TABLE test AS
2 SELECT level id , 'oracle_'||level texto
3 FROM dual
4 CONNECT BY level <= 10000 ;

Table created.

SQL_9iR2> CREATE TABLE test_2 AS
2 SELECT *
3 FROM test
4 WHERE 1 = 2 ;

Table created.

Antes de ejecutar el primer código, voy a liberar la memoria que ya no se está utilizando en mi sesión actual para poder ver claramente la diferencia de las estadísticas tomadas en cada ejecución.

SQL_9iR2> exec dbms_session.FREE_UNUSED_USER_MEMORY ;

PL/SQL procedure successfully completed.

Ejecutamos un código PL/SQL con Bulk Collect pero SIN la cláusula LIMIT:

SQL_9iR2> DECLARE
2 TYPE t_array_number IS TABLE OF NUMBER ;
3 TYPE t_array_varchar2 IS TABLE OF VARCHAR2(50) ;
4 t_array_id t_array_number ;
5 t_array_texto t_array_varchar2 ;
6 CURSOR cur IS
7 SELECT * FROM test ;
8 BEGIN
9 OPEN cur ;
10 LOOP
11
12 FETCH cur BULK COLLECT INTO t_array_id , t_array_texto ;
13
14 FORALL i IN 1 .. t_array_id.COUNT
15 INSERT INTO test_2
16 VALUES (t_array_id(i) , t_array_texto(i)) ;
17
18 EXIT WHEN cur%NOTFOUND ;
19
20 END LOOP ;
21 COMMIT ;
22 CLOSE cur ;
23 END ;
24 /

PL/SQL procedure successfully completed.

Elapsed: 00:00:00.04

Antes de ejecutar el segundo código, voy a liberar nuevamente la memoria que ya no se está utilizando en mi sesión actual.

SQL_9iR2> exec dbms_session.FREE_UNUSED_USER_MEMORY ;

PL/SQL procedure successfully completed.

Ahora ejecutamos el mismo código PL/SQL con Bulk Collect pero CON la cláusula LIMIT:

SQL_9iR2> DECLARE
2 TYPE t_array_number IS TABLE OF NUMBER ;
3 TYPE t_array_varchar2 IS TABLE OF VARCHAR2(50) ;
4 t_array_id t_array_number ;
5 t_array_texto t_array_varchar2 ;
6 CURSOR cur IS
7 SELECT * FROM test ;
8 BEGIN
9 OPEN cur ;
10 LOOP
11
12 FETCH cur BULK COLLECT INTO t_array_id , t_array_texto LIMIT 100 ;
13
14 FORALL i IN 1 .. t_array_id.COUNT
15 INSERT INTO test_2
16 VALUES (t_array_id(i) , t_array_texto(i)) ;
17
18 EXIT WHEN cur%NOTFOUND ;
19
20 END LOOP ;
21 COMMIT ;
22 CLOSE cur ;
23 END ;
24 /

PL/SQL procedure successfully completed.

Elapsed: 00:00:00.04

Podemos ver que en la segunda ejecución, estoy cargando en memoria 100 registros en cada Fetch que realizo. A diferencia de la primer ejecución, que carga todos los datos de una vez.

Veamos las estadísticas obtenidas de las 2 ejecuciones anteriores:

Nombre Ejecución_1 Ejecución_2 Diferencia
------------------------------ ----------- ----------- -----------
LATCH.kwqit: protect wakeup ti 1 0 -1
LATCH.simulator lru latch 1 0 -1
LATCH.spilled msgs queues list 1 0 -1
LATCH.transaction allocation 3 0 -3
LATCH.session timer 5 0 -5
LATCH.multiblock read objects 8 0 -8
LATCH.channel operations paren 11 0 -11
LATCH.child cursor hash table 20 8 -12
LATCH.Consistent RBA 56 6 -50
LATCH.lgwr LWN SCN 56 6 -50
LATCH.mostly latch-free SCN 56 6 -50
LATCH.active checkpoint queue 63 7 -56
LATCH.session idle bit 183 45 -138
LATCH.enqueues 219 49 -170
LATCH.redo writing 241 25 -216
LATCH.SQL memory manager worka 337 0 -337
LATCH.messages 427 42 -385
LATCH.simulator hash latch 844 4 -840
LATCH.dml lock allocation 1,428 154 -1,274
LATCH.shared pool 1,586 271 -1,315
LATCH.row cache enqueue latch 1,648 194 -1,454
LATCH.cache buffers lru chain 1,521 5 -1,516
LATCH.library cache pin alloca 2,900 362 -2,538
LATCH.row cache objects 4,438 502 -3,936
LATCH.enqueue hash chains 4,841 508 -4,333
LATCH.checkpoint queue latch 6,296 521 -5,775
LATCH.session allocation 19,333 1,893 -17,440
LATCH.undo global data 30,208 2,945 -27,263
LATCH.redo allocation 30,716 3,231 -27,485
LATCH.sequence cache 42,506 4,124 -38,382
LATCH.library cache pin 60,580 6,241 -54,339
LATCH.library cache 76,555 7,911 -68,644

LATCHES:

Ejecución_1 Ejecución_2 Diferencia Porcentaje
670,187 67,521 -602,666 992.56%


Una de las cosas que me interesa mostrarles acerca de éstas ejecuciones son los LATCHES (loqueos). Si bien la ejecución de los 2 códigos PL/SQL demoraron exactamente lo mismo en ejecutarse (en éste ejemplo procesando solamente 10.000 registros), las estadísticas nos muestran que estamos empleados muchísimos más loqueos en la primer ejecución que en la segunda. Pero porqué sucede ésto? Recordemos que en la segunda ejecución, lo único que modificamos en el código fue el agregado de la cláusula LIMIT. Bien, como dijimos en el comienzo, manejar gran cantidad de memoria es más costoso que manejar poca cantidad, por lo que Oracle tiene que emplear mayor cantidad de loqueos para manejar los 10.000 registros que subimos a memoria en la primer ejecución, que manejar solamente 100 registros en la segunda ejecución.
Este ejemplo lo realizamos con un sólo usuario concurrente.... pero imagínese qué sucedería si tenemos muchos usuarios concurrentes realizando lo mismo que nosotros... y por lo tanto, generando gran cantidad de loqueos....

NOTA: Tenemos que evitar los loqueos a toda costa ya que los loqueos afectan la performance del sistema. Mientras mayor sea la cantidad de loqueos, nuestro sistema se vuelve cada vez menos escalable; y como consecuencia, cada vez soporta menor cantidad de usuarios concurrentes.

Tablas Particionadas - Comando ' EXCHANGE '


Exchange Partition

Exchange Partition permite cargar en tablas particionadas datos en forma rápida y con muy poco impacto para los usuarios que se encuentran activos.
En resumen, lo que hace la sentencia Exchange Partition es modificar el diccionario de datos y simular que los datos que ya tenemos cargados en una tabla, corresponden a una partición determinada de otra tabla.

Veamos un ejemplo muy sencillo para entender mejor éste tema:

SQL_10gR2> CREATE TABLE datos_1 AS
2 SELECT level id, timestamp'2000-11-02 09:00:00' fecha
3 FROM dual
4 CONNECT BY level <= 100000 ; Table created. Elapsed: 00:00:01.06 SQL_10gR2> CREATE TABLE datos_2 AS
2 SELECT level id, timestamp'2001-09-10 13:00:00' fecha
3 FROM dual
4 CONNECT BY level <= 100000 ; Table created.

Lo que hicimos fue crear 2 tablas con distintas fechas en cada una de ellas.

Ahora creamos solamente la estructura de la tabla particionada en donde vamos a cargar los datos:

SQL_10gR2> CREATE TABLE test
2 ( id, fecha )
3 PARTITION BY RANGE ( fecha )
4 (
5 PARTITION year_2000 VALUES LESS THAN ( timestamp'2000-12-02 00:00:00' ),
6 PARTITION year_2001 VALUES LESS THAN ( timestamp'2001-10-10 00:00:00' )
7 )
8 AS
9 SELECT 1, timestamp'2000-11-02 09:00:00'
10 FROM dual
11 WHERE 1 = 0 ;

Vamos a realizar un alter para modificar el diccionario de datos y relacionar cada una de las 2 tablas que creamos con la respectiva partición de la tabla TEST...

SQL_10gR2> ALTER TABLE test
2 EXCHANGE PARTITION year_2000
3 WITH table datos_1
4 WITHOUT VALIDATION ;

Table altered.

Elapsed: 00:00:00.03

SQL_10gR2> ALTER TABLE test
2 EXCHANGE PARTITION year_2001
3 WITH table datos_2
4 WITHOUT VALIDATION ;

Table altered.

Elapsed: 00:00:00.02

SQL_10gR2> SELECT count(*)
2 FROM test ;

COUNT(*)
----------
200000

1 row selected.

SQL_10gR2> SELECT count(*)
2 FROM datos_1 ;

COUNT(*)
----------
0

1 row selected.

SQL_10gR2> SELECT count(*)
2 FROM datos_2 ;

COUNT(*)
----------
0

1 row selected.

Como podemos ver, con el Exchange Partition no tardamos casi nada en cargar los datos en la tabla particionada ya que en realidad no estamos cargando los datos, simplemente se modifica el diccionario de datos.

Pueden notar que agregué la sentencia WITHOUT VALIDATION. Que es ésto? WITHOUT VALIDATION suele ser una operación rápida porque sólo realiza modificaciones en el diccionario de datos. Si la tabla o tabla particionada que colocamos en el Exchange Partition tiene una primary key o unique constraint habilitado, entonces el Exchange Partition se realiza como WITH VALIDATION para mantener la integridad de las constraints.

Vamos a ejecutar nuevamente los 2 alter anteriores sin la sentencia WITHOUT VALIDATION...

SQL_10gR2> ALTER TABLE test
2 EXCHANGE PARTITION year_2000
3 WITH table datos_1 ;

Table altered.

Elapsed: 00:00:01.00

SQL_10gR2> ALTER TABLE test
2 EXCHANGE PARTITION year_2001
3 WITH table datos_2 ;

Table altered.

Elapsed: 00:00:01.05

Si hubiera ejecutado esos alter con un Trace, el reporte del Trace me mostraría, entre otras sentencias, las siguientes...

select 1
from
"DATOS_1" where TBL$OR$IDX$PART$NUM("TEST", 0, 3,1048576,"FECHA") != :1

call count cpu elapsed disk query current rows
------- ------ -------- ---------- ---------- ---------- ---------- ----------
Parse 1 0.00 0.00 0 0 0 0
Execute 1 0.00 0.00 0 1 0 0
Fetch 1 0.04 0.04 0 65 0 0
------- ------ -------- ---------- ---------- ---------- ---------- ----------
total 3 0.04 0.04 0 66 0 0

Misses in library cache during parse: 1
Misses in library cache during execute: 1
Optimizer mode: ALL_ROWS
Parsing user id: 81 (recursive depth: 1)

Rows Row Source Operation
------- ---------------------------------------------------
0 TABLE ACCESS FULL DATOS_1 (cr=65 pr=0 pw=0 time=44582 us)


select 1
from
"DATOS_2" where TBL$OR$IDX$PART$NUM("TEST", 0, 3,1048576,"FECHA") != :1

call count cpu elapsed disk query current rows
------- ------ -------- ---------- ---------- ---------- ---------- ----------
Parse 1 0.00 0.00 0 0 0 0
Execute 1 0.00 0.00 0 1 0 0
Fetch 1 0.04 0.04 0 65 0 0
------- ------ -------- ---------- ---------- ---------- ---------- ----------
total 3 0.04 0.04 0 66 0 0

Misses in library cache during parse: 1
Misses in library cache during execute: 1
Optimizer mode: ALL_ROWS
Parsing user id: 81 (recursive depth: 1)

Rows Row Source Operation
------- ---------------------------------------------------
0 TABLE ACCESS FULL DATOS_2 (cr=65 pr=0 pw=0 time=46957 us)

Antes que nada, notamos que los alter se ejecutaron en mayor tiempo, cierto? De las consultas que observamos del Trace, vemos que se está realizando un FULL SCAN de las tablas y que se está ejecutando una función en el WHERE de cada consulta. Imagínense si tenemos que realizar ésta clase de procesos en ambientes con gran volumen de datos y en donde el sistema se encuentra saturado por el I/O a disco.

Qué sucede si no tenemos los datos separados por año en distintas tablas, y en cambio, tenemos todos los datos en una misma tabla? Bueno, tomando como ejemplo la tabla TEST que acabamos de cargar, podríamos realizar lo siguiente...

SQL_10gR2> CREATE TABLE test_2
2 ( id, fecha )
3 PARTITION BY RANGE ( fecha )
4 (
5 PARTITION year_2000 VALUES LESS THAN ( timestamp'2000-12-02 00:00:00' ),
6 PARTITION year_2001 VALUES LESS THAN ( timestamp'2001-10-10 00:00:00' )
7 )
8 AS
9 SELECT *
10 FROM test ;

Table created.

Elapsed: 00:00:05.04

SQL_10gR2> DROP TABLE test ;

Table dropped.

SQL_10gR2> ALTER TABLE test_2 RENAME TO test ;

Table altered.

SQL_10gR2> SELECT count(*)
2 FROM test ;

COUNT(*)
----------
200000

1 row selected.

miércoles, 16 de septiembre de 2009

Oracle 11g R2 --- Liberado

Despues de varios meses de pruebas, se libero el dia de hoy la version final del Oracle 11g R2 (11.2.0.1) para linux (32 y 64 bit), esta version trae mas de 200 nuevas mejoras destacando el nuevo ASM FileSystem, mejoras en comprension y mucho mas. Uds. podran encontrar mas informacion y bajar el software en:

http://www.oracle.com/database/index.html

Un gran saludos a todos,

lunes, 31 de agosto de 2009

Copiar una base de datos en Windows

La petición textual era la creación de una base de datos igual que otra, pero con otro nombre. En algunos casos se trataba de replicar el entorno de producción para usarlo como test. En otros, simplemente, testear que el backup permitía recuperar la base de datos sobre otra máquina y dejar esa base de datos operativa como entorno auxiliar.

Cuando la base de datos puede conservar el mismo nombre y mismo SID, es tan fácil como copiar datafiles, redolog, controlfiles y archivos ORA sobre los mismos directorios originales en una máquina con un servidor Oracle instalado*.
* Por supuesto, misma plataforma O.S., versión y release de Oracle.

No obstante, en los casos en los que el nombre debe ser diferente, hay que ser muy preciso con los pasos a seguir. Incluyo dos pasos previos como opcionales por si son de utilidad.

En este ejemplo se asume que disponemos de la posibilidad de hacer un backup en frío de la base de datos.

Pasos preliminares (recomendado):

- Backup frío, backup caliente, export, Transport Tablespaces, backup del backup, backup de todo lo posible.
- Reducción de tamaño de los datafiles. (Tom Kyte tiene un fabuloso script para ello). Y otra vez backup.

Pasos a seguir:

1.- Backup en frío de la BBDD original
2.- Generación del pfile para la nueva BBDD
3.- Edición del pfile para sustituir: CONTROL_FILES, DB_NAME, y carpetas _DUMP_DEST,...
4.- Definir el nuevo ORACLE_SID
5.- Crear el nuevo servicio.
6.- Creación del fichero de passwords (sólo si el pfile tiene REMOTE_LOGIN_PASSWORDFILE=exclusive)
7.- Conexión a sqlplus como SYSDBA
8.- Arranque de la instancia y creación del fichero de parámetros SPFILE
9.- Backup del controlfile de la BBDD original a traza
10.- Recreación del controlfile con la cláusula SET NAME.
11.- Abrir la base de datos con modo OPEN RESETLOGS.




1.- Backup en frío de la BBDD original.
Restauración del backup sobre nueva ubicación.


-- Ejecutar el resultado de la siguiente sentencia como script.
-- NOTA: cuidado con los nombres duplicados de fichero.
-------------------------------------------------------------
select 'shutdown immediate;' from dual
union all
select 'host copy '||name||' &&directorio_destino' from v$controlfile
union all
select 'host copy '||member||' &directorio_destino' from v$logfile
union all
select 'host copy '||name||' &directorio_destino' from v$datafile
union all
select 'startup' from dual;

-- copia de los ficheros a los directorios destino
---------------------------------------------------



2.- Generación del pfile para la nueva base de datos

SQL> create pfile='?\admin\sid\pfile\inittest.ora' from spfile;

Archivo creado.



3.- Edición del pfile para sustituir: CONTROL_FILES, DB_NAME, y carpetas _DUMP_DEST,...



4.- Definir el nuevo ORACLE_SID

c:\>set ORACLE_SID=test



5.- Crear el nuevo servicio.

c:\>oradim -NEW -SRVC OracleServicetest -startmode auto



6.- Creación del fichero de passwords

c:\>orapwd file=C:\orant\ora92\database\PWDtest.ora password=xxxxxxxxx



7.- Conexión a sqlplus como SYSDBA

C:\>sqlplus

SQL*Plus: Release 9.2.0.6.0 - Production on Vie Jul 29 16:41:22 2005

Copyright (c) 1982, 2002, Oracle Corporation. All rights reserved.

Introduzca el nombre de usuario: sys/xxxxxxxxxx as sysdba
Conectado a una instancia inactiva.



8.- Arranque de la instancia y creación del fichero de parámetros SPFILE

SQL> STARTUP NOMOUNT PFILE='C:\DIRECTORIO_DESTINO\inittest.ora'
Instancia ORACLE iniciada.

Total System Global Area 1074866044 bytes
Fixed Size 456572 bytes
Variable Size 905969664 bytes
Database Buffers 167772160 bytes
Redo Buffers 667648 bytes
SQL> create spfile from pfile='C:\DIRECTORIO_DESTINO\inittest.ora';

Archivo creado.



9.- Backup del controlfile de PROD a trace.

SQL> alter database backup controlfile to trace;

Base de datos modificada.



10.- A partir de la traza del fichero de control.
Recreación del controlfile con el SET NAME al nuevo nombre.

CREATE CONTROLFILE SET DATABASE "TEST" RESETLOGS NOARCHIVELOG REUSE
MAXLOGFILES 50
MAXLOGMEMBERS 5
MAXDATAFILES 133
MAXINSTANCES 1
MAXLOGHISTORY 453
LOGFILE
GROUP 2 'C:\DATA\TEST\REDO\REDO02.LOG' SIZE 100M,
GROUP 3 'C:\DATA\TEST\REDO\REDO03.LOG' SIZE 100M,
GROUP 4 'C:\DATA\TEST\REDO\REDO04.LOG' SIZE 150M,
GROUP 5 'C:\DATA\TEST\REDO\REDO05.LOG' SIZE 150M,
GROUP 6 'C:\DATA\TEST\REDO\REDO06.LOG' SIZE 200M
-- STANDBY LOGFILE
DATAFILE
(... ficheros...)
'C:\DATA\TEST\SYSTEM\SYSTEM01.DBF',
'C:\DATA\TEST\DATA\DATA01.DBF'
CHARACTER SET WE8MSWIN1252
;

Archivo de control creado.



11.- Abrir la base de datos con modo OPEN RESETLOGS.

SQL> alter database open resetlogs;

Base de datos modificada.



12.- Verificación.

SQL> select instance_name from v$instance;

INSTANCE_NAME
----------------
test

SQL> select name from v$database;

NAME
---------
TEST

SQL> select status from v$thread;

STATUS
------
OPEN



12.- Shutdown y Open de la base de datos.

SQL> shutdown immediate;
Base de datos cerrada.
Base de datos desmontada.
Instancia ORACLE cerrada.

SQL> startup
Instancia ORACLE iniciada.

Total System Global Area 1074866044 bytes
Fixed Size 456572 bytes
Variable Size 905969664 bytes
Database Buffers 167772160 bytes
Redo Buffers 667648 bytes
Base de datos montada.
Base de datos abierta.

Uso de logminer

En alguna ocasión he necesitado recuperar una transacción validada con COMMIT, pero no deseada o errónea. Pongo un ejemplo:

- Usuario borra una fila.
- Por accidente.
- No sabe de qué fila se trata, pero 'algo' ha dicho "registro borrado".
- (...borrado y validado).
- Ni idea de qué datos tenía la fila.
- Ni él, ni nadie.
- Desde ese momento, la fila X ha desaparecido sin rastro.

Con variaciones, éste caso es bastante frecuente, y si el usuario que ha borrado el registro es lo suficientemente rápido, existe una posibilidad de recuperar los datos perdidos.

Generalmente, todas las transacciones convencionales procesan la información pasando por las áreas de redo log. En estos archivos está toda la información correspondiente al nuevo cambio. Oracle basa toda su actividad en el buen funcionamiento del registro de redo log y, por tanto, en esta área únicamente almacena los datos físicos elementales para rehacer la operación. Por tanto, no se almacena información sobre los valores anteriores.

Lo cual sí sucede en el área de rollback, y ¡la gestión de rollback también pasa por redolog!.

Oracle dispone de un paquete, el LOG MINER que permite rastrear los redo logs (online y archivados) junto con la información de rollback disponible también en éstos. Una vez ejecutado el paquete que crea el fichero de diccionario de objetos, Oracle es capaz de interpretar esos redologs y mostrar información tan válida como el SQL generado y el SQL inverso para "reparar" la acción.



SQL> connect / as sysdba
Conectado.
SQL> show parameters utl

NAME TYPE VALUE
------------------------------------ ----------- ------------------------------
utl_file_dir string c:\oraclefiles

SQL> exec DBMS_LOGMNR_D.BUILD( DICTIONARY_FILENAME =>'dictionary.ora', DICTIONARY_LOCATION => 'c:\oraclefiles');

Procedimiento PL/SQL terminado correctamente.

SQL> select member from v$logfile;

MEMBER
--------------------------------------------------------------------------------
C:\ORACLE\ORADATA\ORCL\REDO04A.LOG
C:\ORACLE\ORADATA\ORCL\REDO04B.LOG
C:\ORACLE\ORADATA\ORCL\REDO05A.LOG
C:\ORACLE\ORADATA\ORCL\REDO05B.LOG
C:\ORACLE\ORADATA\ORCL\REDO06A.LOG
C:\ORACLE\ORADATA\ORCL\REDO06B.LOG

6 filas seleccionadas.

SQL> exec DBMS_LOGMNR.add_logfile('C:\ORACLE\ORADATA\ORCL\REDO04A.LOG');

Procedimiento PL/SQL terminado correctamente.

SQL> exec DBMS_LOGMNR.add_logfile('C:\ORACLE\ORADATA\ORCL\REDO05A.LOG');

Procedimiento PL/SQL terminado correctamente.

SQL> exec DBMS_LOGMNR.add_logfile('C:\ORACLE\ORADATA\ORCL\REDO06A.LOG');

Procedimiento PL/SQL terminado correctamente.

SQL> EXECUTE DBMS_LOGMNR.START_LOGMNR(DICTFILENAME =>'c:\oraclefiles\dictionary.ora');

Procedimiento PL/SQL terminado correctamente.

SQL> set pages 100
SQL> set lines 120
SQL> column sql_redo format a50
SQL> column sql_undo format a50

SQL> select sql_redo, sql_undo from v$logmnr_contents where rownum<10;

SQL_REDO SQL_UNDO
-------------------------------------------------- --------------------------------------------------
insert into "DISCOVERER9I"."EUL5_MV101651"("Import delete from "DISCOVERER9I"."EUL5_MV101651" where "
e SUM","Importe COUNT","Importe MAX","Importe MIN" Importe SUM" = '170,01' and "Importe COUNT" = '1'
,"Importe AVG","Ciudad","Pais","Rango Edad","Sexo" and "Importe MAX" = '170,01' and "Importe MIN" = '
,"Detalle Vuelo") values ('170,01','1','170,01','1 170,01' and "Importe AVG" = '170,01' and "Ciudad"
70,01','170,01','Madeira','Portugal','Mayores de 5 = 'Madeira' and "Pais" = 'Portugal' and "Rango Eda
0 a±os','Mujer','VUELO52149'); d" = 'Mayores de 50 a±os' and "Sexo" = 'Mujer' and
"Detalle Vuelo" = 'VUELO52149' and ROWID = 'AAALV
mAAPAAAAn8AAA';

insert into "DISCOVERER9I"."EUL5_MV101651"("Import delete from "DISCOVERER9I"."EUL5_MV101651" where "
e SUM","Importe COUNT","Importe MAX","Importe MIN" Importe SUM" = '112,95' and "Importe COUNT" = '1'
,"Importe AVG","Ciudad","Pais","Rango Edad","Sexo" and "Importe MAX" = '112,95' and "Importe MIN" = '
,"Detalle Vuelo") values ('112,95','1','112,95','1 112,95' and "Importe AVG" = '112,95' and "Ciudad"
12,95','112,95','Madeira','Portugal','Mayores de 5 = 'Madeira' and "Pais" = 'Portugal' and "Rango Eda
0 a±os','Mujer','VUELO52206'); d" = 'Mayores de 50 a±os' and "Sexo" = 'Mujer' and
"Detalle Vuelo" = 'VUELO52206' and ROWID = 'AAALV
mAAPAAAAn8AAB';

insert into "DISCOVERER9I"."EUL5_MV101651"("Import delete from "DISCOVERER9I"."EUL5_MV101651" where "
e SUM","Importe COUNT","Importe MAX","Importe MIN" Importe SUM" = '123,29' and "Importe COUNT" = '1'
,"Importe AVG","Ciudad","Pais","Rango Edad","Sexo" and "Importe MAX" = '123,29' and "Importe MIN" = '
,"Detalle Vuelo") values ('123,29','1','123,29','1 123,29' and "Importe AVG" = '123,29' and "Ciudad"
23,29','123,29','Madeira','Portugal','Mayores de 5 = 'Madeira' and "Pais" = 'Portugal' and "Rango Eda
0 a±os','Mujer','VUELO52331'); d" = 'Mayores de 50 a±os' and "Sexo" = 'Mujer' and
"Detalle Vuelo" = 'VUELO52331' and ROWID = 'AAALV
mAAPAAAAn8AAC';

insert into "DISCOVERER9I"."EUL5_MV101651"("Import delete from "DISCOVERER9I"."EUL5_MV101651" where "
e SUM","Importe COUNT","Importe MAX","Importe MIN" Importe SUM" = '178,55' and "Importe COUNT" = '1'
,"Importe AVG","Ciudad","Pais","Rango Edad","Sexo" and "Importe MAX" = '178,55' and "Importe MIN" = '
,"Detalle Vuelo") values ('178,55','1','178,55','1 178,55' and "Importe AVG" = '178,55' and "Ciudad"
78,55','178,55','Madeira','Portugal','Mayores de 5 = 'Madeira' and "Pais" = 'Portugal' and "Rango Eda
0 a±os','Mujer','VUELO52419'); d" = 'Mayores de 50 a±os' and "Sexo" = 'Mujer' and
"Detalle Vuelo" = 'VUELO52419' and ROWID = 'AAALV
mAAPAAAAn8AAD';

insert into "DISCOVERER9I"."EUL5_MV101651"("Import delete from "DISCOVERER9I"."EUL5_MV101651" where "
e SUM","Importe COUNT","Importe MAX","Importe MIN" Importe SUM" = '107,36' and "Importe COUNT" = '1'
,"Importe AVG","Ciudad","Pais","Rango Edad","Sexo" and "Importe MAX" = '107,36' and "Importe MIN" = '
,"Detalle Vuelo") values ('107,36','1','107,36','1 107,36' and "Importe AVG" = '107,36' and "Ciudad"
07,36','107,36','Madeira','Portugal','Mayores de 5 = 'Madeira' and "Pais" = 'Portugal' and "Rango Eda
0 a±os','Mujer','VUELO52488'); d" = 'Mayores de 50 a±os' and "Sexo" = 'Mujer' and
"Detalle Vuelo" = 'VUELO52488' and ROWID = 'AAALV
mAAPAAAAn8AAE';

insert into "DISCOVERER9I"."EUL5_MV101651"("Import delete from "DISCOVERER9I"."EUL5_MV101651" where "
e SUM","Importe COUNT","Importe MAX","Importe MIN" Importe SUM" = '120,5' and "Importe COUNT" = '1' a
,"Importe AVG","Ciudad","Pais","Rango Edad","Sexo" nd "Importe MAX" = '120,5' and "Importe MIN" = '12
,"Detalle Vuelo") values ('120,5','1','120,5','120 0,5' and "Importe AVG" = '120,5' and "Ciudad" = 'M
,5','120,5','Madeira','Portugal','Mayores de 50 a± adeira' and "Pais" = 'Portugal' and "Rango Edad" =
os','Mujer','VUELO52561'); 'Mayores de 50 a±os' and "Sexo" = 'Mujer' and "De
talle Vuelo" = 'VUELO52561' and ROWID = 'AAALVmAAP
AAAAn8AAF';

insert into "DISCOVERER9I"."EUL5_MV101651"("Import delete from "DISCOVERER9I"."EUL5_MV101651" where "
e SUM","Importe COUNT","Importe MAX","Importe MIN" Importe SUM" = '109,01' and "Importe COUNT" = '1'
,"Importe AVG","Ciudad","Pais","Rango Edad","Sexo" and "Importe MAX" = '109,01' and "Importe MIN" = '
,"Detalle Vuelo") values ('109,01','1','109,01','1 109,01' and "Importe AVG" = '109,01' and "Ciudad"
09,01','109,01','Madeira','Portugal','Mayores de 5 = 'Madeira' and "Pais" = 'Portugal' and "Rango Eda
0 a±os','Mujer','VUELO52690'); d" = 'Mayores de 50 a±os' and "Sexo" = 'Mujer' and
"Detalle Vuelo" = 'VUELO52690' and ROWID = 'AAALV
mAAPAAAAn8AAG';

insert into "DISCOVERER9I"."EUL5_MV101651"("Import delete from "DISCOVERER9I"."EUL5_MV101651" where "
e SUM","Importe COUNT","Importe MAX","Importe MIN" Importe SUM" = '174,91' and "Importe COUNT" = '1'
,"Importe AVG","Ciudad","Pais","Rango Edad","Sexo" and "Importe MAX" = '174,91' and "Importe MIN" = '
,"Detalle Vuelo") values ('174,91','1','174,91','1 174,91' and "Importe AVG" = '174,91' and "Ciudad"
74,91','174,91','Madeira','Portugal','Mayores de 5 = 'Madeira' and "Pais" = 'Portugal' and "Rango Eda
0 a±os','Mujer','VUELO52693'); d" = 'Mayores de 50 a±os' and "Sexo" = 'Mujer' and
"Detalle Vuelo" = 'VUELO52693' and ROWID = 'AAALV
mAAPAAAAn8AAH';

insert into "DISCOVERER9I"."EUL5_MV101651"("Import delete from "DISCOVERER9I"."EUL5_MV101651" where "
e SUM","Importe COUNT","Importe MAX","Importe MIN" Importe SUM" = '131,5' and "Importe COUNT" = '1' a
,"Importe AVG","Ciudad","Pais","Rango Edad","Sexo" nd "Importe MAX" = '131,5' and "Importe MIN" = '13
,"Detalle Vuelo") values ('131,5','1','131,5','131 1,5' and "Importe AVG" = '131,5' and "Ciudad" = 'M
,5','131,5','Madeira','Portugal','Mayores de 50 a± adeira' and "Pais" = 'Portugal' and "Rango Edad" =
os','Mujer','VUELO52951'); 'Mayores de 50 a±os' and "Sexo" = 'Mujer' and "De
talle Vuelo" = 'VUELO52951' and ROWID = 'AAALVmAAP
AAAAn8AAI';


9 filas seleccionadas.

Acceso a una bbdd MSAccess desde Oracle.

Es posible acceder a bases de datos no oracle de forma transparente con Oracle Transparent Gateways.

Esta funcionalidad permite configurar una entrada DNS de ODBC como si se tratara de un servicio Oracle cualquiera. De este modo, mediante un dblink, un usuario podría acceder a una tabla de una base de datos access (o SQLServer o MySQL, por ejemplo) desde una query o desde PL/SQL.

La arquitectura de servicios heterogeneos se basa en la siguiente configuración:

Se especifica una entrada de TNS en el fichero tnsnames.ora que resuelva una conexión al host, puerto y nombre de servicio determinado, con el parámetro HS=OK (Oracle Heterogeneous Services).

Por otro lado, el listener destino tiene definido para ese servicio la invocación a un programa llamado "hsodbc" que, mediante un fichero de parámetros llamado init.ora accede al DNS asociado a la base de datos MSAccess concreta.

De este modo, y creando un dblink, cualquier usuario podría hacer una select que cruzara datos de su esquema Oracle y de una bbdd Access.

PASOS A SEGUIR
1.- Instalar en el diccionario los servicios heterogeneos.
2.- Configurar el tnsnames.ora
3.- Configurar el listener.
4.- Comprobar que el listener y el tnsnames.ora funcionan correctamente.
5.- Configurar el ODBC para la bbdd access.
6.- Crear el DBLINK hacia la bbdd access.
7.- Crear el fichero init.ora
8.- Ejecutar una consulta mediante ese dblink.


1.- Instalar en el diccionario los servicios heterogeneos
@?/rdbms/admin/caths.sql

NOTA: puede que estas vistas estén instaladas ya en la bbdd.


2.- Configurar el tnsnames.ora
bbdd_access =
(DESCRIPTION=
(ADDRESS= (PROTOCOL=tcp)
(HOST=192.168.2.4)
(PORT=1521)
)
(CONNECT_DATA =
(SERVICE_NAME=odbc_access)
)
(HS = OK)
)

3.- Configurar el listener. (la entrada en negrita es la que hay que añadir)...

LISTENER =
(DESCRIPTION_LIST =
(DESCRIPTION =
(ADDRESS_LIST =
(ADDRESS = (PROTOCOL = IPC)(KEY = EXTPROC0))
)
(ADDRESS_LIST =
(ADDRESS = (PROTOCOL = TCP)(HOST = 192.168.2.4)(PORT = 1521))
)
)
)

SID_LIST_LISTENER =
(SID_LIST =
(SID_DESC =
(SID_NAME = PLSExtProc)
(ORACLE_HOME = D:\oracle\ora92)
(PROGRAM = extproc)
)
(SID_DESC =
(GLOBAL_DBNAME = orcl)
(ORACLE_HOME = D:\oracle\ora92)
(SID_NAME = orcl)
)
(SID_DESC =
(ORACLE_HOME = D:\oracle\ora92)
(SID_NAME = odbc_access)
(PROGRAM = hsodbc)
)
)

4.- Comprobar que el listener y el tnsnames.ora funciona correctamente.

C:\Documents and Settings\Administrador>tnsping bbdd_access

TNS Ping Utility for 32-bit Windows: Version 9.2.0.1.0 - Production on 08-SEP-2006 18:19:42

Copyright (c) 1997 Oracle Corporation. All rights reserved.

Archivos de parßmetros utilizados:
D:\oracle\ora92\network\admin\sqlnet.ora


Adaptador TNSNAMES utilizado para resolver el alias
Attempting to contact (DESCRIPTION= (ADDRESS= (PROTOCOL=tcp) (HOST=192.168.2.4) (PORT=1521)) (CONNECT_DATA = (SERVICE_NAME=odbc_access)) (HS = OK))
Realizado correctamente (50 mseg)


5.- Configurar el ODBC para la bbdd access.
La base de datos access utilizada en el ejemplo tiene una única tabla con cuatro filas de ejemplo...

create table ejemplo

(id counter,
valor text)
Es preciso crear un ODBC para acceder a la bbdd access determinada. Desde Windows esta opción se configura en Panel de control, Herramientas administrativas, Administrador ODBC y la pestaña DNS de sistema.

desde ahí se crea el driver odbc_access vinculado a la base de datos que uso: "test_hs.mdb" con esta tabla de ejemplo.
NOTA: esta bbdd access tiene password "tesths"


6.- Crear el DBLINK hacia la bbdd access.

SQL> CREATE DATABASE LINK bbdd_access
2 CONNECT TO ADMIN IDENTIFIED BY TESTHS
3 USING 'bbdd_access';

Enlace con la base de datos creado.



7.- Crear el fichero init.ora en el directorio de ORACLE_HOME /hs/admin/

-- FICHERO initODBC_ACCESS.ora
#
# HS init parameters
#

HS_FDS_CONNECT_INFO = odbc_access
HS_FDS_TRACE_LEVEL = 0
HS_FDS_TRACE_FILE_NAME = odbc_access.trc
HS_AUTOREGISTER = TRUE



8.- Lanzar la query via ese dblink

SQL> column "valor" format a40
SQL> set pages 1000
SQL> set lines 120
SQL> select * from ejemplo@bbdd_access;

id valor
---------- ----------------------------------------
1 dato1
2 dato2
3 dato3
4 dato4

Optimizacion de Consultas SQL en Oracle

La siguiente consulta tardaba 11 horas en ejecutarse.

SELECT DISTINCT A.NODO_ID, B.NODO_B_ID NODO_EQ1
FROM BITOWN03.BS_R_NODOS_BIT_03 A,
BITOWN03.RE_R_CONEX_EXTERNAS_BIT_03 B,
BITOWN03.TMP_NODOS_OK_EST_BIT_03 C
WHERE A.NODO_ID = B.NODO_A_ID
AND B.NODO_B_ID = C.NODO_ID;


con el siguiente plan de ejecución:
SQL> @c:\oracle\ora92\rdbms\admin\utlxpls

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

-----------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
-----------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 5554 | 124K| 23 |
| 1 | SORT UNIQUE | | 5554 | 124K| 23 |
| 2 | NESTED LOOPS | | 5554 | 124K| 2 |
| 3 | MERGE JOIN CARTESIAN| | 5985M| 61G| 2 |
| 4 | TABLE ACCESS FULL | TMP_NODOS_OK_EST_BIT_03 | 1327 | 6635 | 2 |
| 5 | BUFFER SORT | | 4510K| 25M| |
| 6 | INDEX FULL SCAN | PK_R_NODOS_BIT_03 | 4510K| 25M| |
|* 7 | INDEX RANGE SCAN | IDX_NODO_CONEX_EXTERNA | 1 | 12 | |
-----------------------------------------------------------------------------------

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

7 - access("B"."NODO_B_ID"="C"."NODO_ID" AND "A"."NODO_ID"="B"."NODO_A_ID")

Note: cpu costing is off



********************************************************************************
SOLUCIÓN al caso.
********************************************************************************

Otro producto cartesiano. En este caso el cartesiano sabe muy bien lo que hace. Cruza casi 6.000 millones de filas (en total 61 gigas de información) y un coste mínimo. Vaya paradoja.

Un detalle para entender esta decisión: no existen restricciones de Primary Key, ni
Foreign Key, ni índices únicos, ni restricciones de Not Null.

A causa de ello, Oracle encuentra práctico combinar todos los resultados de una tabla (4,5 millones) sobre las 1400 filas de la otra tabla, en un "todos con todos".

No está mal. No obstante, hay información que Oracle, por mucho que analice las tablas, no va a poder obtener a priori. Únicamente nos interesan valores únicos de la tabla de relación, que existan en sus respectivas tablas relacionadas, pero, como digo, no existe nada que aporte a Oracle esa información.

Cambio algunos detalles de la consulta y la dejo así:



SELECT DISTINCT A.NODO_ID, B.NODO_B_ID NODO_EQ1
FROM BITOWN03.BS_R_NODOS_BIT_03 A,
(select distinct nodo_a_id, nodo_b_id from BITOWN03.RE_R_CONEX_EXTERNAS_BIT_03) B,
BITOWN03.TMP_NODOS_OK_EST_BIT_03 C
WHERE A.NODO_ID = B.NODO_A_ID
AND B.NODO_B_ID = C.NODO_ID;



Informo de dos cosas: que tengo interés en obtener los códigos distintos de la tabla de relación, y que además existan en las otras dos tablas.

El plan de ejecución cambia totalmente para ejecutarse tal como lo he dicho de otra forma. Ahora Oracle realiza este otro plan de ejecución.


SQL> @c:\oracle\ora92\rdbms\admin\utlxpls

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

---------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost |
---------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 5554 | 200K| | 13626 |
| 1 | NESTED LOOPS | | 5554 | 200K| | 13626 |
| 2 | NESTED LOOPS | | 5554 | 168K| | 13626 |
| 3 | VIEW | | 7159K| 177M| | 13626 |
| 4 | SORT UNIQUE | | 7159K| 81M| 273M| 13626 |
| 5 | TABLE ACCESS FULL| RE_R_CONEX_EXTERNAS_BIT_03 | 7159K| 81M| | 2589 |
|* 6 | INDEX UNIQUE SCAN | PK_TMP_MANEL_03 | 1 | 5 | | |
|* 7 | INDEX UNIQUE SCAN | PK_R_NODOS_BIT_03 | 1 | 6 | | |
---------------------------------------------------------------------------------------------

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

6 - access("B"."NODO_B_ID"="C"."NODO_ID")
7 - access("A"."NODO_ID"="B"."NODO_A_ID")





El coste ahora parece haberse disparado por completo. 13626 unidades de coste. Respecto al coste anterior, de sólo 23. Pero ahora no aparece el cartesiano y parece que la ejecución es más fiel a lo que queremos.

Acabo de lanzar la ejecución: un minuto con veinte segundos.
Bien.

Oracle Tips : Cambiar parametro character set en la base de datos

1. Check NLS_CHARACTERSET
Connect to user SYS or SYSTEM

SELECT *
FROM V$NLS_PARAMETERS
WHERE PARAMETER = 'NLS_CHARACTERSET';

Parameter Value
NLS_CHARACTERSET AL32UTF8

If you want to change NLS_CHARACTERSET from AL32UTF8 to TH8TISASCII

2. Connect to user SYS

3. Update value

UPDATE PROPS$
SET VALUE$ = 'TH8TISASCII'
WHERE NAME = 'NLS_CHARACTERSET';

COMMIT;

4. Restart database

SHUTDOWN IMMEDIATE;

STARTUP;

5. Check NLS_CHARACTERSET
Connect to user SYS or SYSTEM

SELECT * FROM
V$NLS_PARAMETERS
WHERE PARAMETER = 'NLS_CHARACTERSET';

Parameter Value
NLS_CHARACTERSET TH8TISASCII

Optimizacion de Consultas SQL en Oracle

La siguiente consulta se ha cancelado tras 15 horas y 37 minutos de ejecución.

SQL> select count(*) from (SELECT BITOWN03.BS_C_CLIENTES_BIT_03.DNICIF_DE,
2 BITOWN03.HS_C_ESTADOS_CONTRATO_BIT_03.CONTRATO_ID,
3 HSCONTRATOS_ESTADOS.ESTADO_DE,
4 BITOWN03.HS_C_ESTADOS_CONTRATO_BIT_03.FEC_ESTADO_DT,
5 BITOWN03.BS_C_CLIENTES_BIT_03.ABONADO_ID
6 FROM
7 BITOWN03.HS_C_ESTADOS_CONTRATO_BIT_03,
8 BITOWN03.LK_C_ESTADOS_BIT_03 HSCONTRATOS_ESTADOS,
9 BITOWN03.BS_C_CLIENTES_BIT_03,
10 BITOWN03.BS_C_CONTRATOS_BIT_03 CONTRATOS
11 WHERE BITOWN03.BS_C_CLIENTES_BIT_03.ABONADO_ID = CONTRATOS.ABONADO_ID
12 AND BITOWN03.HS_C_ESTADOS_CONTRATO_BIT_03.CONTRATO_ID = CONTRATOS.CONTRATO_ID
13 AND BITOWN03.HS_C_ESTADOS_CONTRATO_BIT_03.ESTADO_ID = HSCONTRATOS_ESTADOS.ESTADO_ID
14 AND BITOWN03.HS_C_ESTADOS_CONTRATO_BIT_03.FEC_ESTADO_DT
15 BETWEEN to_date('01-06-2005 00:00:00','DD-MM-YYYY HH24:MI:SS')
16 AND to_date('30-06-2005 23:59:59','DD-MM-YYYY HH24:MI:SS')
17 AND BITOWN03.HS_C_ESTADOS_CONTRATO_BIT_03.ESTADO_ID = 'BA');
BITOWN03.HS_C_ESTADOS_CONTRATO_BIT_03,
*
ERROR at line 7:
ORA-00028: your session has been killed


Elapsed: 15:37:01.66


con el siguiente plan de ejecución:
SQL> @C:\ORACLE\ORA92\RDBMS\ADMIN\UTLXPLS

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

---------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost | Pstart| Pstop |
---------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 461 | 24433 | 16 | | |
| 1 | NESTED LOOPS | | 461 | 24433 | 16 | | |
| 2 | MERGE JOIN CARTESIAN | | 1448M| 58G| 16 | | |
| 3 | NESTED LOOPS | | 971 | 35927 | 16 | | |
| 4 | TABLE ACCESS BY INDEX ROWID | LK_C_ESTADOS_BIT_03 | 1 | 20 | 1 | | |
|* 5 | INDEX UNIQUE SCAN | PK_C_ESTADOS_BIT_03 | 1 | | | | |
|* 6 | INDEX RANGE SCAN | IDX_FECHA_ESTADO_CONT_JAVIER | 971 | 16507 | 15 | | |
| 7 | BUFFER SORT | | 1491K| 8739K| 1 | | |
| 8 | INDEX FULL SCAN | PK_C_CLIENTES_BIT_03 | 1491K| 8739K| | | |
|* 9 | TABLE ACCESS BY GLOBAL INDEX ROWID| BS_C_CONTRATOS_BIT_03 | 1 | 10 | | ROWID | ROW L |
|* 10 | INDEX UNIQUE SCAN | PK_C_CONTRATOS_BIT_03 | 1 | | | | |
---------------------------------------------------------------------------------------------------------------------

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

5 - access("HSCONTRATOS_ESTADOS"."ESTADO_ID"='BA')
6 - access("HS_C_ESTADOS_CONTRATO_BIT_03"."FEC_ESTADO_DT">=TO_DATE('2005-06-01 00:00:00', 'yyyy-mm-dd
hh24:mi:ss') AND "HS_C_ESTADOS_CONTRATO_BIT_03"."ESTADO_ID"='BA' AND
"HS_C_ESTADOS_CONTRATO_BIT_03"."FEC_ESTADO_DT"<=TO_DATE('2005-06-30 23:59:59', 'yyyy-mm-dd hh24:mi:ss'))
filter("HS_C_ESTADOS_CONTRATO_BIT_03"."ESTADO_ID"="HSCONTRATOS_ESTADOS"."ESTADO_ID" AND
"HS_C_ESTADOS_CONTRATO_BIT_03"."ESTADO_ID"='BA')
9 - filter("BS_C_CLIENTES_BIT_03"."ABONADO_ID"="CONTRATOS"."ABONADO_ID")
10 - access("HS_C_ESTADOS_CONTRATO_BIT_03"."CONTRATO_ID"="CONTRATOS"."CONTRATO_ID")

Note: cpu costing is off



********************************************************************************
SOLUCIÓN al caso.
********************************************************************************


Las tablas contienen estadísticas fieles. El producto cartesiano tampoco engaña: cruzará 58 Gb. para atender nuestra petición. De todas maneras, algo falla. Ese plan no es óptimo.

Las estadísticas se generan mediante la siguiente llamada a DBMS_STATS.

SQL> begin
2 DBMS_STATS.GATHER_TABLE_STATS(OWNNAME=>usuario,
3 TABNAME=>tabla,
4 ESTIMATE_PERCENT=>dbms_stats.auto_sample_size,
5 METHOD_OPT => 'FOR ALL INDEXED COLUMNS SIZE 1',
6 DEGREE=>4);
7 end;
8 /

PL/SQL procedure successfully completed.


Es preciso no únicamente analizar las tablas, sino también los índices. Algunos de ellos están analizados con el procedimiento DBMS_STATS.GATHER_INDEX_STATS, pero una de las tablas está particionada. Lo mejor y más simple es incluir el parámetro CASCADE=>TRUE en la llamada a DBMS_STATS.

Es cierto que el coste de análisis de estas tablas incrementará. Además, el parámetro CASCADE afecta, no sólo a índices, sino también a vistas materializadas, particiones, etc. (es decir, a todas sus dependencias).

Con las tablas analizadas de este modo, el nuevo plan de ejecución es éste:




SQL> @c:\oracle\ora92\rdbms\admin\utlxpls

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

------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 471 | 30615 | 405 |
|* 1 | HASH JOIN | | 471 | 30615 | 405 |
|* 2 | INDEX RANGE SCAN | IDX_FECHA_ESTADO_CONT_JAVIER | 8740 | 145K| 75 |
| 3 | NESTED LOOPS | | 771K| 35M| 257 |
| 4 | NESTED LOOPS | | 1626K| 46M| 257 |
| 5 | TABLE ACCESS BY INDEX ROWID| LK_C_ESTADOS_BIT_03 | 1 | 20 | 1 |
|* 6 | INDEX UNIQUE SCAN | PK_C_ESTADOS_BIT_03 | 1 | | |
| 7 | INDEX FAST FULL SCAN | IDX_ABONADO_CONTRATO | 1626K| 15M| 256 |
| 8 | TABLE ACCESS BY INDEX ROWID | BS_C_CLIENTES_BIT_03 | 1 | 18 | |
|* 9 | INDEX UNIQUE SCAN | PK_C_CLIENTES_BIT_03 | 1 | | |
------------------------------------------------------------------------------------------------

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

1 - access("HS_C_ESTADOS_CONTRATO_BIT_03"."CONTRATO_ID"="CONTRATOS"."CONTRATO_ID" AND
"HS_C_ESTADOS_CONTRATO_BIT_03"."ESTADO_ID"="HSCONTRATOS_ESTADOS"."ESTADO_ID")
2 - access("HS_C_ESTADOS_CONTRATO_BIT_03"."FEC_ESTADO_DT">=TO_DATE('2005-06-01 00:00:00',
'yyyy-mm-dd hh24:mi:ss') AND "HS_C_ESTADOS_CONTRATO_BIT_03"."ESTADO_ID"='BA' AND
"HS_C_ESTADOS_CONTRATO_BIT_03"."FEC_ESTADO_DT"<=TO_DATE('2005-06-30 23:59:59', 'yyyy-mm-dd
hh24:mi:ss'))
filter("HS_C_ESTADOS_CONTRATO_BIT_03"."ESTADO_ID"='BA')
6 - access("HSCONTRATOS_ESTADOS"."ESTADO_ID"='BA')
9 - access("BS_C_CLIENTES_BIT_03"."ABONADO_ID"="CONTRATOS"."ABONADO_ID")

Note: cpu costing is off

30 rows selected.



La ejecución ahora sólo tarda: 3 segundos.
El coste de análisis aumenta de 26 minutos a 32 minutos. La ganancia justifica el coste.

Muy bien. :-)

lunes, octubre 17, 2005

NOT IN vs. NOT EXISTS.

Para Fernando, por la que le espera. :-P


La siguiente consulta se ha cancelado tras 5 horas y 11 minutos en ejecutarse.

SELECT count(CLI.COD_ABONADO)
FROM BITOWN02.TM_C_CLIENTES_SAC_02 CLI,
BITOWN02.TE_ERRORES_BIT_02 TE
WHERE CLI.ROWID = TE.FILA_ID (+)
AND 'TM_C_CLIENTES_SAC_02' = TE.TABLA_DE (+)
AND TE.ERROR_ID IS NULL
AND CLI.COD_ABONADO NOT IN (
SELECT CU.ABONADO_id
FROM BITOWN03.BS_C_CONTRATOS_BIT_03 CONT, BITOWN03.BS_C_CUENTAS_BIT_03 CU
WHERE CU.CUENTA_ID=CONT.CUENTA_ID
);


con el siguiente plan de ejecución:
SQL> @c:\oracle\ora92\rdbms\admin\utlxpls

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

---------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost | Pstart| Pstop |
---------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 44 | 3343 | | |
| 1 | SORT AGGREGATE | | 1 | 44 | | | |
|* 2 | FILTER | | | | | | |
|* 3 | FILTER | | | | | | |
|* 4 | HASH JOIN OUTER | | | | | | |
| 5 | TABLE ACCESS FULL | TM_C_CLIENTES_SAC_02 | 74323 | 870K| 2407 | | |
| 6 | TABLE ACCESS FULL | TE_M_ERRORES_BIT_01 | 1 | 32 | 2 | 13 | 13 |
| 7 | NESTED LOOPS | | 1640K| 50M| 922 | | |
| 8 | PARTITION LIST ALL | | | | | 1 | 7 |
| 9 | TABLE ACCESS FULL | BS_C_CONTRATOS_BIT_03 | 1640K| 20M| 922 | 1 | 7 |
|* 10 | TABLE ACCESS BY INDEX ROWID| BS_C_CUENTAS_BIT_03 | 1 | 19 | | | |
|* 11 | INDEX UNIQUE SCAN | PK_C_CUENTAS_BIT_03 | 1 | | | | |
---------------------------------------------------------------------------------------------------------


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

2 - filter( NOT EXISTS (SELECT /*+ */ 0 FROM "BITOWN03"."BS_C_CUENTAS_BIT_03"
"CU","BITOWN03"."BS_C_CONTRATOS_BIT_03" "CONT" WHERE "CU"."CUENTA_ID"="CONT"."CUENTA_ID" AND
LNNVL("CU"."ABONADO_ID"<>:B1)))
3 - filter("TE_M_ERRORES_BIT_01"."ERROR_ID" IS NULL)
4 - access("SYS_ALIAS_1".ROWID="TE_M_ERRORES_BIT_01"."FILA_ID"(+))
10 - filter(LNNVL("CU"."ABONADO_ID"<>:B1))
11 - access("CU"."CUENTA_ID"="CONT"."CUENTA_ID")

Note: cpu costing is off

30 rows selected.


********************************************************************************
SOLUCIÓN al caso.
********************************************************************************

En este caso, la consulta con IN tiene un coste aceptable. No obstante, después de cinco horas de ejecución, es de sospechar que la cosa no va muy bien. La estrategia de ejecución es realizar dos FILTER: el primero para el OuterJoin de Clientes sin errores y el segundo para combinarlo (en un pesadísimo NestedLoops) con las cuentas con contratos.

Como la subconsulta está resultando más pesada incluso que la principal, es posible que sustituir IN por la cláusula EXISTS sea una buena estrategia.

Sí, también tengo cuidado que no haya códigos con valor NULL para resolver la consulta, ya que NOT IN y NOT EXISTS no son lo mismo.

Sustituyo NOT IN por NOT EXISTS y la consulta queda de este modo:



explain plan for
SELECT count(CLI.COD_ABONADO)
FROM BITOWN02.TM_C_CLIENTES_SAC_02 CLI,
BITOWN02.TE_ERRORES_BIT_02 TE
WHERE CLI.ROWID = TE.FILA_ID (+)
AND 'TM_C_CLIENTES_SAC_02' = TE.TABLA_DE (+)
AND TE.ERROR_ID IS NULL
AND not exists (
SELECT null
FROM BITOWN03.BS_C_CONTRATOS_BIT_03 CONT, BITOWN03.BS_C_CUENTAS_BIT_03 CU
WHERE CLI.COD_ABONADO=CU.ABONADO_ID AND
CU.CUENTA_ID=CONT.CUENTA_ID);



El plan de ejecución resultante parece ser similar al anterior, incluso su coste parece peor.



SQL> @c:\oracle\ora92\rdbms\admin\utlxpls

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

--------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost | Pstart| Pstop |
--------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 57 | | 4946 | | |
| 1 | SORT AGGREGATE | | 1 | 57 | | | | |
|* 2 | FILTER | | | | | | | |
|* 3 | HASH JOIN OUTER | | | | | | | |
|* 4 | HASH JOIN ANTI | | 1486K| 35M| 34M| 4269 | | |
| 5 | TABLE ACCESS FULL | TM_C_CLIENTES_SAC_02 | 1486K| 17M| | 2407 | | |
| 6 | VIEW | VW_SQ_1 | 1640K| 20M| | 922 | | |
| 7 | NESTED LOOPS | | 1640K| 50M| | 922 | | |
| 8 | PARTITION LIST ALL | | | | | | 1 | 7 |
| 9 | TABLE ACCESS FULL | BS_C_CONTRATOS_BIT_03 | 1640K| 20M| | 922 | 1 | 7 |
| 10 | TABLE ACCESS BY INDEX ROWID| BS_C_CUENTAS_BIT_03 | 1 | 19 | | | | |
|* 11 | INDEX UNIQUE SCAN | PK_C_CUENTAS_BIT_03 | 1 | | | | | |
| 12 | TABLE ACCESS FULL | TE_M_ERRORES_BIT_01 | 1 | 32 | | 2 | 13 | 13 |
--------------------------------------------------------------------------------------------------------------------

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

2 - filter("TE_M_ERRORES_BIT_01"."ERROR_ID" IS NULL)
3 - access("CLI".ROWID="TE_M_ERRORES_BIT_01"."FILA_ID"(+))
4 - access("CLI"."COD_ABONADO"="VW_SQ_1"."ABONADO_ID")
11 - access("CU"."CUENTA_ID"="CONT"."CUENTA_ID")

Note: cpu costing is off




Acabo de lanzar la ejecución: 28 segundos.
Très bien. :-)

jueves, octubre 13, 2005

Dílo de otra forma.

Para Manel Moreno, que no me ha dado ningún beso por ésto. :P


La siguiente consulta tardaba 11 horas en ejecutarse.

SELECT DISTINCT A.NODO_ID, B.NODO_B_ID NODO_EQ1
FROM BITOWN03.BS_R_NODOS_BIT_03 A,
BITOWN03.RE_R_CONEX_EXTERNAS_BIT_03 B,
BITOWN03.TMP_NODOS_OK_EST_BIT_03 C
WHERE A.NODO_ID = B.NODO_A_ID
AND B.NODO_B_ID = C.NODO_ID;


con el siguiente plan de ejecución:
SQL> @c:\oracle\ora92\rdbms\admin\utlxpls

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

-----------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
-----------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 5554 | 124K| 23 |
| 1 | SORT UNIQUE | | 5554 | 124K| 23 |
| 2 | NESTED LOOPS | | 5554 | 124K| 2 |
| 3 | MERGE JOIN CARTESIAN| | 5985M| 61G| 2 |
| 4 | TABLE ACCESS FULL | TMP_NODOS_OK_EST_BIT_03 | 1327 | 6635 | 2 |
| 5 | BUFFER SORT | | 4510K| 25M| |
| 6 | INDEX FULL SCAN | PK_R_NODOS_BIT_03 | 4510K| 25M| |
|* 7 | INDEX RANGE SCAN | IDX_NODO_CONEX_EXTERNA | 1 | 12 | |
-----------------------------------------------------------------------------------

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

7 - access("B"."NODO_B_ID"="C"."NODO_ID" AND "A"."NODO_ID"="B"."NODO_A_ID")

Note: cpu costing is off



********************************************************************************
SOLUCIÓN al caso.
********************************************************************************

Otro producto cartesiano. En este caso el cartesiano sabe muy bien lo que hace. Cruza casi 6.000 millones de filas (en total 61 gigas de información) y un coste mínimo. Vaya paradoja.

Un detalle para entender esta decisión: no existen restricciones de Primary Key, ni
Foreign Key, ni índices únicos, ni restricciones de Not Null.

A causa de ello, Oracle encuentra práctico combinar todos los resultados de una tabla (4,5 millones) sobre las 1400 filas de la otra tabla, en un "todos con todos".

No está mal. No obstante, hay información que Oracle, por mucho que analice las tablas, no va a poder obtener a priori. Únicamente nos interesan valores únicos de la tabla de relación, que existan en sus respectivas tablas relacionadas, pero, como digo, no existe nada que aporte a Oracle esa información.

Cambio algunos detalles de la consulta y la dejo así:



SELECT DISTINCT A.NODO_ID, B.NODO_B_ID NODO_EQ1
FROM BITOWN03.BS_R_NODOS_BIT_03 A,
(select distinct nodo_a_id, nodo_b_id from BITOWN03.RE_R_CONEX_EXTERNAS_BIT_03) B,
BITOWN03.TMP_NODOS_OK_EST_BIT_03 C
WHERE A.NODO_ID = B.NODO_A_ID
AND B.NODO_B_ID = C.NODO_ID;



Informo de dos cosas: que tengo interés en obtener los códigos distintos de la tabla de relación, y que además existan en las otras dos tablas.

El plan de ejecución cambia totalmente para ejecutarse tal como lo he dicho de otra forma. Ahora Oracle realiza este otro plan de ejecución.


SQL> @c:\oracle\ora92\rdbms\admin\utlxpls

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

---------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost |
---------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 5554 | 200K| | 13626 |
| 1 | NESTED LOOPS | | 5554 | 200K| | 13626 |
| 2 | NESTED LOOPS | | 5554 | 168K| | 13626 |
| 3 | VIEW | | 7159K| 177M| | 13626 |
| 4 | SORT UNIQUE | | 7159K| 81M| 273M| 13626 |
| 5 | TABLE ACCESS FULL| RE_R_CONEX_EXTERNAS_BIT_03 | 7159K| 81M| | 2589 |
|* 6 | INDEX UNIQUE SCAN | PK_TMP_MANEL_03 | 1 | 5 | | |
|* 7 | INDEX UNIQUE SCAN | PK_R_NODOS_BIT_03 | 1 | 6 | | |
---------------------------------------------------------------------------------------------

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

6 - access("B"."NODO_B_ID"="C"."NODO_ID")
7 - access("A"."NODO_ID"="B"."NODO_A_ID")





El coste ahora parece haberse disparado por completo. 13626 unidades de coste. Respecto al coste anterior, de sólo 23. Pero ahora no aparece el cartesiano y parece que la ejecución es más fiel a lo que queremos.

Acabo de lanzar la ejecución: un minuto con veinte segundos.
Bien.

martes, octubre 11, 2005

Mala Cardinalidad

Para Cristina Álvarez, quien confió en mi desde el primer momento y lo disimuló con toda su alma. :)


La siguiente consulta ha sido cancelada tras una hora y veinte minutos de ejecución.

SELECT N2.ENTIDAD_ID,
CLI.rowid row_id, cli.*,
MAX(REL.ABONADO_PADRE_ID) OVER (PARTITION BY ABONADO_HIJO_ID) REL_ABONADO_PADRE_ID
FROM BITOWN03.BS_V_MGEC_NODO_BIT_03 N1,
BITOWN03.BS_V_MGEC_NODO_BIT_03 N2,
BITOWN03.BS_V_MGEC_REL_NODOS_BIT_03 RN,
BITOWN02.TM_C_CLIENTES_SAC_02 CLI,
BITOWN03.RE_C_RELACIONES_ABONADO_BIT_03 REL
WHERE N1.ENTIDAD_ID= CLI.COD_DNICIF
AND N1.NODO_ID=RN.NODO_ID
AND RN.TIPO_RELACION_ID=2 AND RN.FECHA_FIN_DT IS NULL
AND RN.NODO_PADRE_ID=N2.NODO_ID
AND CLI.COD_ABONADO = REL.ABONADO_HIJO_ID (+);



con el siguiente plan de ejecución:

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

---------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
---------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 278 | 2496 |
| 1 | SORT UNIQUE | | 1 | 278 | 2496 |
| 2 | WINDOW SORT | | 1 | 278 | 2496 |
| 3 | NESTED LOOPS | | 1 | 278 | 2479 |
| 4 | NESTED LOOPS | | 1 | 261 | 2478 |
| 5 | NESTED LOOPS OUTER | | 1 | 244 | 2477 |
| 6 | MERGE JOIN CARTESIAN | | 1 | 232 | 2477 |
|* 7 | TABLE ACCESS FULL | BS_V_MGEC_REL_NODOS_BIT_03 | 1 | 22 | 70 |
| 8 | BUFFER SORT | | 1486K| 297M| 2407 |
| 9 | TABLE ACCESS FULL | TM_C_CLIENTES_SAC_02 | 1486K| 297M| 2407 |
|* 10 | INDEX FULL SCAN | PK_C_RELACIONES_ABONADO_BIT_03 | 1 | 12 | |
|* 11 | TABLE ACCESS BY INDEX ROWID| BS_V_MGEC_NODO_BIT_03 | 1 | 17 | 1 |
|* 12 | INDEX UNIQUE SCAN | PK_V_MGEC_NODO_BIT_03 | 1 | | |
| 13 | TABLE ACCESS BY INDEX ROWID | BS_V_MGEC_NODO_BIT_03 | 1 | 17 | 1 |
|* 14 | INDEX UNIQUE SCAN | PK_V_MGEC_NODO_BIT_03 | 1 | | |
---------------------------------------------------------------------------------------------------

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

7 - filter("RN"."TIPO_RELACION_ID"=2 AND "RN"."FECHA_FIN_DT" IS NULL)
10 - access("CLI"."COD_ABONADO"="REL"."ABONADO_HIJO_ID"(+))
filter("CLI"."COD_ABONADO"="REL"."ABONADO_HIJO_ID"(+))
11 - filter("N1"."ENTIDAD_ID"="CLI"."COD_DNICIF")
12 - access("N1"."NODO_ID"="RN"."NODO_ID")
14 - access("RN"."NODO_PADRE_ID"="N2"."NODO_ID")




********************************************************************************
SOLUCIÓN al caso.
********************************************************************************

Omitiendo el detalle que la cláusula distinct sobra. La mantenemos para que los planes resulten de ejecuciones similares.

Parece un plan bastante aceptable. No obstante, un producto cartesiano llama la atención... ¿únicamente se obtiene una fila? en ese caso, el cartesiano no hace daño. Vamos a consultar el total de filas de la tabla, si la tabla tiene generadas estadísticas (la columna num_rows de user_tables puede darnos una pista) y si el filtro de la operación 7 realmente resuelve una única fila.

SQL> select count(*) from bitown03.BS_V_MGEC_REL_NODOS_BIT_03;

COUNT(*)
----------
118907

SQL> select table_name, num_rows from dba_tables where
2 table_name='BS_V_MGEC_REL_NODOS_BIT_03';


TABLE_NAME NUM_ROWS
------------------------------ ----------
BS_V_MGEC_REL_NODOS_BIT_03 118907

SQL> select count(*) from bitown03.BS_V_MGEC_REL_NODOS_BIT_03
2 where TIPO_RELACION_ID=2 AND FECHA_FIN_DT IS NULL; --> FILTRO OPERACION 7

COUNT(*)
----------
12844


Pues parece que no... Se está produciendo un producto cartesiano de 1,5 millones de filas (operación 9 TABLE FULL SCAN) sobre 12844 elementos. Oracle está estimando mal la cardinalidad de las filas. Cree que con el filtro 7 únicamente obtendrá una fila y por eso el producto cartesiano no dispara el coste.

Graso error.

Es preciso analizar CORRECTAMENTE las tablas implicadas, indicando que se analicen también los valores de las columnas implicadas. Los comandos para realizar ese análisis son:

SQL> exec dbms_stats.gather_table_stats(OWNNAME=>'usuario',TABNAME=>'BS_V_MGEC_REL_NODOS_BIT_03',METHOD_OPT=>'for all columns');

PL/SQL procedure successfully completed.

Elapsed: 00:00:07.71
SQL> exec dbms_stats.gather_table_stats(OWNNAME=>'usuario',TABNAME=>'BS_V_MGEC_NODO_BIT_03',METHOD_OPT=>'for all columns');

PL/SQL procedure successfully completed.

Elapsed: 00:00:14.57


De esta forma, las estadísticas tienen también información sobre la cardinalidad de las columnas y el plan de ejecución cambia sustancialmente:



SQL> @c:\oracle\ora92\rdbms\admin\utlxpls

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

---------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost |
---------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 209K| 53M| | 12462 |
| 1 | SORT UNIQUE | | 209K| 53M| 112M| 12462 |
| 2 | WINDOW SORT | | 209K| 53M| 112M| 12462 |
| 3 | NESTED LOOPS OUTER | | 209K| 53M| | 4217 |
|* 4 | HASH JOIN | | 209K| 51M| | 4217 |
|* 5 | HASH JOIN | | 12841 | 589K| | 320 |
|* 6 | HASH JOIN | | 12841 | 388K| | 192 |
|* 7 | TABLE ACCESS FULL| BS_V_MGEC_REL_NODOS_BIT_03 | 12841 | 188K| | 70 |
| 8 | TABLE ACCESS FULL| BS_V_MGEC_NODO_BIT_03 | 128K| 2012K| | 112 |
| 9 | TABLE ACCESS FULL | BS_V_MGEC_NODO_BIT_03 | 128K| 2012K| | 112 |
| 10 | TABLE ACCESS FULL | TM_C_CLIENTES_SAC_02 | 1486K| 297M| | 2407 |
|* 11 | INDEX FULL SCAN | PK_C_RELACIONES_ABONADO_BIT_03 | 1 | 12 | | |
---------------------------------------------------------------------------------------------------

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

4 - access("N1"."ENTIDAD_ID"="CLI"."COD_DNICIF")
5 - access("RN"."NODO_PADRE_ID"="N2"."NODO_ID")
6 - access("N1"."NODO_ID"="RN"."NODO_ID")
7 - filter("RN"."TIPO_RELACION_ID"=2 AND "RN"."FECHA_FIN_DT" IS NULL)
11 - access("CLI"."COD_ABONADO"="REL"."ABONADO_HIJO_ID"(+))
filter("CLI"."COD_ABONADO"="REL"."ABONADO_HIJO_ID"(+))

Note: cpu costing is off


Efectivamente, el coste del plan es mayor, pero REAL.
Sólo ha tardado 23 segundos!!