Mostrando entradas con la etiqueta SQL. Mostrar todas las entradas
Mostrando entradas con la etiqueta SQL. Mostrar todas las entradas

lunes, 17 de enero de 2011

Un ejemplo de como realizar calculos con fechas con sql

Hace unos dias me llegó un mail de esos tipicos mails cadena que supuestamente traen suerte si se lo envias a 20 personas mas. Claramente estos mails no tienen sustento real en general y actuan por ingenieria social como mecanismo de generación de tráfico basura. Rara vez alcanzo a leer mas de 2 lineas antes de borrarlos, pero en este caso me llamó la atención y despertó mi curiosidad, ya que probar su veracidad con una consulta sql seria muy facil. Claramente esto no tiene ninguna utilidad mas que mostrarles como resolver cuestiones y relaciones de fechas usando, en este caso Oracle, como una
calculadora extremadamente costosa.

El mail decia algo asi: "Este año el mes de julio tiene 5 viernes, 5 sabados y 5 domingos, esto se da cada 823 años, esto se denomina saco de dinero, envia esto a 20 amigos... bla bla bla". Cuando lo leí, me dí cuenta que no podia ser real ya que tener esa periodicidad tan exacta y extensa, tratandose de fechas, no era posible. Para probarlo prolijamente, realicé una consulta de forma tal de generar fechas automaticamente, comenzando desde una fecha bien lejana (500000 años hacia atrás) y sumando cada vez que el mes fuera Julio (07) los dias viernes, sabado y domingo (6,7,1). Si la suma es 15 entonces en ese año se da que lo que reza el mail en cuestión. A continuación les muestro la consulta y el resultado:


select to_char(dt,'YYYY') dt,count(1)
from (select sysdate-500000+rownum dt
from dual
connect by rownum <= 500000)
where to_char(dt,'MM') = '07'
and to_char(dt,'d') in (6,7,1)
group by to_char(dt,'YYYY')
having count(1) > 14
order by to_number(dt) desc

1 2005 15
2 1994 15
3 1988 15
4 1983 15
5 1977 15
6 1966 15
7 1960 15
8 1955 15
9 1949 15
10 1938 15
11 1932 15
12 1927 15
13 1921 15
14 1910 15
15 1904 15
16 1898 15
17 1892 15
18 1887 15
19 1881 15
20 1870 15

Como se ve, en el 2005 se dió por ultima vez la relación citada, pasaron solo 6 años para que se repita y no 823!!!.

miércoles, 15 de diciembre de 2010

Cuando una consulta utiliza un indice, pero no el mejor indice posible

Muchas veces me preguntaron por que una consulta no responde en un tiempo adecuado cuando el plan de ejecución muestra que se esta utilizando un indice. La respuesta en ciertos casos es muy sencilla y se debea a que el indice no es lo mas selectivo posible, no filtra todo lo que podria filtrar. Decir que una consulta usa un plan que accede por un indice no es suficiente para asegurar que se haya encontrado el camino mas eficiente. Para mostrarles un poco de que estoy hablando voy a armar un caso, un tanto trivial pero no por eso menos ilustrativo, para que se entienda la idea.

Voy a crear una tabla T con 3 columnas. La columna COL1 va a tener 1000 valores distintos y la columna COL2 va a tener 10 valores posibles. Además agrego la columna COL3 de relleno



create table t
as
select mod(rownum,1000) col1,
mod(rownum,100000) col2,
dbms_random.string('a',50) col3
from dual
connect by rownum <= 1000000


Una vez creada la tabla voy a crear un indice por COL1 y recolecto las estadisticas para la tabla y para el indice:

create index t_idx on t (col1)

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


Ejecuto la siguiente consulta y extraigo el plan usando DBMS_XPLAN.DISPLAY_CURSOR para que el plan sea mas detallado (en una nota futura voy a usar el mismo método para mostrar como ver como se "confunde" el optimizador cuando no se dispone de las estadisticas adecuadas):


select /*+ gather_plan_statistics */ * from t
where col1 = 9
and col2 = 9

select * from table (dbms_xplan.display_cursor(null,null,'ALLSTATS LAST'));

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
SQL_ID 25p2md7bszhj6, child number 0
-------------------------------------
select /*+ gather_plan_statistics */ * from t where col1 = 9 and col2
= 9

Plan hash value: 1020776977

-----------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 10 |00:00:00.01 | 1006 |
|* 1 | TABLE ACCESS BY INDEX ROWID| T | 1 | 1 | 10 |00:00:00.01 | 1006 |
|* 2 | INDEX RANGE SCAN | T_IDX | 1 | 1000 | 1000 |00:00:00.01 | 6 |
-----------------------------------------------------------------------------------------------

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

1 - filter("COL2"=9)
2 - access("COL1"=9)

En el paso 2 del plan se observa que la estimación fue buena (E-Rows=A-Rows) pero la cantidad filtrada fue 1ooo filas, cuando realmente se deberian haber filtrado 10, como se ve en el paso 1 (A-Rows)

(Aclaración: A-Rows es la cantidad de filas reales y E-Rows es la cantidad de filas estimada)

Ahora voy a eliminar el indice T_IDX y voy a crear otro con el mismo nombre pero indexando por COL1 y COL2. Veamos el nuevo plan:

drop index t_idx

create index t_idx on t (col1,col2)

select /*+ gather_plan_statistics 2 */ * from t
where col1 = 9
and col2 = 9

select * from table (dbms_xplan.display_cursor(null,null,'ALLSTATS LAST'));

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
SQL_ID 25p2md7bszhj6, child number 0
-------------------------------------
select /*+ gather_plan_statistics */ * from t where col1 = 9 and col2
= 9

Plan hash value: 1020776977

-----------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 10 |00:00:00.01 | 14 |
| 1 | TABLE ACCESS BY INDEX ROWID| T | 1 | 10 | 10 |00:00:00.01 | 14 |
|* 2 | INDEX RANGE SCAN | T_IDX | 1 | 10 | 10 |00:00:00.01 | 4 |
-----------------------------------------------------------------------------------------------

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

2 - access("COL1"=9 AND "COL2"=9)

Ahora el paso 2 retorna al paso 1 solo 10 filas, que es la cantidad de filas total retornada por la consulta. Observar que la cantidad de buffers utilizado es bastante inferior al caso anterior.
Esto demuestra que el nuevo indice fue 100 veces mas selectivo y por lo tanto se necesitaron menos recursos, en consecuencia menos tiempo de procesamiento, para obtener la misma salida.

La moraleja es que nunca hay que conformarse con solo verificar que la sentencia accede por un indice y profundizar en el análisis sobre el esquema actual de indexación comprobando si es el mejor indice posible o si se puede buscar alguna otra combinación mas eficiente.

miércoles, 24 de noviembre de 2010

Como realizar update/delete masivos en forma efectiva

En esta nota voy a mostrarles un método efectivo para modificar o eliminar una gran cantidad de filas sobre una tabla grande. En general las tablas voluminosas se encuentran particionadas para lograr escalar en forma natural. El particionamiento principalmente provee 3 tipos de beneficios: 1) mejora la performance, 2) facilita la administración y mantenimiento y 3) incrementa la disponibilidad de los datos. Resolver una consulta usando como tabla subyacente particionada puede verse de la misma forma que resolver un problema dividiendolo en partes. La conocida premisa: divide y conquistaras es el principal objetivo detrás de particionar.

Desde que se introdujo el feature de partitioning (Oracle 8) se ha ampliado notablemente el set de operaciones posibles sobre tablas e indices para dar soporte y manejar las tablas/indices particionados. Con cada nuevo release se fueron agregando distintas opciones, métodos de particionado y operaciones para manipulación de segmentos. Los distintos features introducidas en cada release son:


Oracle 8 (1997)
  • Partition Pruning (*)
  • Range Partitioning (incluye operaciones ADD, DROP, RENAME, TRUNCATE, MODIFY, MOVE, SPLIT y EXCHANGE)

Oracle 8i (1999)
  • Particionamiento Hash
  • Particionamiento compuesto: range/hash
  • Se agregó la operación MERGE

Oracle 9i R2 (2002)
  • List Partitioning
  • Particionamiento compuesto: Range/List
  • Cláusula UPDATE GLOBAL INDEXES

Oracle 10g R1 (2004)
  • Indices globales particionados por Hash y List

Oracle 10g R2 (2005)
  • Se incremento el limite de particiones/subparticiones de 65k a 4M

Oracle 11g R1 (2007)

  • Particionamiento compuesto: range-range, list-range, list-list y list-hash.
  • Se agregó particionamiento por intervalo, por referencia y de sistema.

Oracle 11g R2 (2009)

  • Columnas virtuales como primary key para tablas particionadas referenciadas.
  • Indices particionados por sistema para tablas particionadas por lista.


Como se puede ver, practicamente en cada nuevo release hubo algún agregado de nueva funcionalidad. Sin embargo, a mi entender, el principal feature existe desde el primer release con partitioning (1997). Me refiero al partition pruning o poda de partición, que posibilita que el optimizador (siempre hablando de CBO) elija en forma automática, precisa y transparente la partición o particiones donde se encuentra los datos requeridos. Esto permite segmentar los datos y solo procesar los que nos interesan, sin tener que agregar ninguna inteligencia adicional en el código de aplicación.

Con respecto a las operaciones, la gran mayoria existen desde Oracle 8, solo se agregó tiempo después el MERGE. Una operación muy interensante es EXCHANGE, con la cual se puede intercambiar una tabla sin particionar con una partición. Justamente es esta la operación que voy a usar para proponer una alternativa rapida para cambiar o borrar gran cantidad de filas sobre tablas particionadas. A continuación, somo suelo hacer, voy a mostrar los pasos en detalle y comparar los tiempos y uso de recursos:

Voy a crear una tabla T particionada por lista con 3 particiones

create table t(c1 int,c2 varchar2(10),
c3 date,
c4 char(1))
partition by list (c4)
(
partition t_a values ('A') ,
partition t_b values ('B') ,
partition t_c values ('C')
);

Ahora voy a insertar 10M de filas distribuidas en forma arbitraria sobre las particiones:

insert into t
select rownum,
dbms_random.string('a',10),
sysdate-dbms_random.value(-100,100),
chr(trunc(dbms_random.value(65,68)))
from dual
connect by rownum <= 10000000;

Inserto 5M de filas sobre la partición en la que voy a trabajar para tener mas filas:

insert into t
select rownum+10000000,
dbms_random.string('a',10),
sysdate-dbms_random.value(-100,100),
'A' from dual
connect by rownum <= 5000000;

Luego de cargados todos los valores se confirman (commit) y luego se recolectan estadisticas.
Veamos el plan para una consulta que cuenta filas sobre la partición 1 (t_a):

explain plan for
select count(1)
from t where c2 > 'R' and c4 = 'A';

select * from table(dbms_xplan.display);

PLAN_TABLE_OUTPUT
---------------------------------------------------------------------------------------------------
Plan hash value: 2901716037

-----------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop |
-----------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 13 | 8455 (3)| 00:03:04 | | |
| 1 | SORT AGGREGATE | | 1 | 13 | | | | |
| 2 | PARTITION LIST SINGLE| | 5588K| 69M| 8455 (3)| 00:03:04 | 1 | 1 |
|* 3 | TABLE ACCESS FULL | T | 5588K| 69M| 8455 (3)| 00:03:04 | 1 | 1 |
-----------------------------------------------------------------------------------------------

Claramente se observa que el optimizador solo accedió la partición 1. Ejecutando la consulta vemos que la estimación del optimizador fue buena:


select count(1)
from t
where c4 = 'A' and c2 > 'R';



COUNT(1)
----------
5610297

El total de filas de la partición es:

select count(1)
from t
where c4 = 'A' ;

COUNT(1)
----------
8333946

En este punto, ya tenemos una partición con mas de 8.3M de filas de las cuales vamos a modificar 5.6M, lo cual es mas del 67%.
Primero voy a testear un update normal sobre la tabla T para luego realizar la comparativa con la misma modificación pero usando otro enfoque mas eficiente.


update t
set c3 = c3+1
where c4 = 'A'
and c2 > 'R'

5610297 filas actualizadas.

Transcurrido: 00:04:45.37

La modificación demoró 4' 45". Pensemos que la base de datos debe mantener la consistencia para garantizar la lectura consistente (mediante el UNDO) y persistir los cambios para poder recuperarse si un evento de falla ocurre durante la modificación (REDO). Estos mecanismos provocan que los tiempos se incrementen y se genere información adicional.

Revisemos cuanto espacio de UNDO y REDO se necesitó para realizar el update:

select 'REDO_SIZE',
round(ms.value/1024/1024) value
from v$mystat ms,
v$statname sn
where ms.STATISTIC# = sn.STATISTIC#
and sn.NAME = 'redo size'
union all
SELECT 'UNDO_SIZE',
t.used_ublk*8/1024 value
FROM v$transaction t, v$session s
WHERE t.addr = s.taddr
AND s.audsid = userenv('sessionid')

REDO_SIZE 2489 Mb
UNDO_SIZE 885 Mb

Para modificar 5.3M se necesitaron 2489Mb de redo y 885Mb de undo!!!. En el ejemplo, la tabla no tiene indices. Si tuviera indices y la columna modificada sea parte de las columnas de indexación se generaría mas redo y undo, y además la sentencia tendría que actualizar los indices por cada fila modificada lo cual provocaría que el update demore bastante mas. Si el procesamiento masivo fuera un delete en lugar de un update, se generará mas undo (el delete es la operación dml que mas undo genera) y se tendrá que mantener balanceados los indices, lo cual implica mas tiempo de procesamiento.

Existe una forma mas sencilla de realizar el update usando la operación estrella de partitioning: EXCHANGE. Antes de usar el exchange tenemos que crear una tabla auxiliar (T_A) y para acelerar la creación configuro la tabla como nologging e inserto en forma directa usando el hint APPEND.
create table t_a_aux nologging as
select /*+ APPEND */
c1,
c2,
case when (c2>'R') then c3+1
else c3 end c3,
c4
from t
where c4 = 'A'

Transcurrido: 00:00:22.04

Solo se necesitaron 22" para insertar la filas en la tabla auxiliar. Con la función DECODE o CASE realizo el cambio simulando el update. Ahora solo resta realizar el intercambio entre la tabla auxiliar y la partición t_a con la operación EXCHANGE:

ALTER TABLE t
EXCHANGE PARTITION t_a
WITH table t_a_aux ;

Transcurrido: 00:00:11.46

El exchange se realizó en casi 12". Sumando la creación de la tabla auxiliar y el exchange, todo demoró solo 44"!!!, es decir mas de 6 veces mas rapido que el update tradicional.
Ejecutando la consulta para obtener el espacio de redo y undo generado se obtiene:
REDO_SIZE     1 Mb
UNDO_SIZE 0 Mb
Practicamente no hubo alocación de undo/redo. Por lo cual, para ciertos casos resulta muy util usar este metodo para actualizar dado que los tiempos de procesamiento se reducen sensiblemente y ademas los requerimientos de undo y redo son minimizados casi por completo.

Para eliminar (delete) en forma masiva, la creación de la tabla auxiliar solo deberá llenarse con las filas que no se borran. Si se necesitara borrar muchas filas de una tabla no particionada se podrá utilizar el mismo enfoque, es decir reemplazar el delete por un insert en una tabla nueva, recrear los indices y renombrar.

viernes, 26 de marzo de 2010

Reporte de Perfil de Carga de Trabajo Historica en la Base (Historical Load Profile)

La sentencia que copié más abajo permite realizar un reporte historico de la actividad general o carga de trabajo de una base de datos Oracle. Ya que la consulta obtiene información del AWR la cantidad de historia disponible dependerá de la retención definida en el repositorio (por default es de 7 dias aunque yo siempre aconsejo cambiarlo a 30 dias). La información proporcionada es lo que se muestra en un reporte AWR en la sección LOAD PROFILE, que esta al principio del reporte. Todos aquellos que hayan analizado performance mirando reportes de awr (ejecutando el script awrrpt.sql o bien usando una herramienta gráfica como el TOAD), sabrán que la sección de "Load Profile" junto con los 5 eventos tops son la primera "foto" del "estado de salud" general de la base. Sin embargo esa "foto" no sirve de mucho si no se conoce bien de antemano la actividad y tipo de carga o si no se ve alguna métrica con valores notoriamente grandes.

Para poder realizar un análisis efectivo hay que sacar varios reportes awr y compararlos para establecer diferencias. En esos casos, yo prefiero tener toda la historia disponible de un patallazo para así comparar mas facilmente, ver si alguna métrica esta en valores no habituales, poder exportar la salida del query a una planilla y realizar un gráfico historico para confeccionar un informe.

Antes de ejecutar el query es importante aclarar que los valores reportados están en unidades por segundo (primera columna del load profile de awr) y que funciona en bases 10g o superiores.


with intervals
as
(select snap,
extract(second from int)+
extract(minute from int)*60+
extract(hour from int)*60*60 int_sec
from
(select end_interval_time snap,
end_interval_time-lead(end_interval_time)
over (partition by startup_time order by snap_id desc) int
from dba_hist_snapshot))
select to_char(snap_time,'YYYY/MM/DD HH24') snap_time,
round((redo_size-lead(redo_size)
over (partition by startup_time order by snap_time desc))/
int.int_sec,2) "Redo Size",
round((logical_reads-lead(logical_reads)
over (partition by startup_time order by snap_time desc))/
int.int_sec,2) "Logical Reads",
round((db_block_changes-lead(db_block_changes)
over (partition by startup_time order by snap_time desc))/
int.int_sec,2) "Block Changes",
round((physical_reads-lead(physical_reads)
over (partition by startup_time order by snap_time desc))/
int.int_sec,2) "Physical Reads",
round((physical_writes-lead(physical_writes)
over (partition by startup_time order by snap_time desc))/
int.int_sec,2) "Physical Writes",
round((user_calls-lead(user_calls)
over (partition by startup_time order by snap_time desc))/
int.int_sec,2) "User Calls",
round((parses-lead(parses)
over (partition by startup_time order by snap_time desc))/
int.int_sec,2) "Parses",
round((parses_hard-lead(parses_hard)
over (partition by startup_time order by snap_time desc))/
int.int_sec,2) "Parses Hard",
round((sorts-lead(sorts)
over (partition by startup_time order by snap_time desc))/
int.int_sec,2) "Sorts",
round((logons-lead(logons)
over (partition by startup_time order by snap_time desc))/
int.int_sec,2) "Logons",
round((executes-lead(executes)
over (partition by startup_time order by snap_time desc))/
int.int_sec,2) "Exectutes",
round((user_rollbacks-lead(user_rollbacks)
over (partition by startup_time order by snap_time desc)+
user_commits-lead(user_commits)
over (partition by startup_time order by snap_time desc))/
int.int_sec,2) "Transactions"
from
(select s.end_interval_time snap_time,
s.startup_time startup_time,
max(decode(ss.stat_name,'redo size',value,null)) redo_size,
max(decode(ss.stat_name,'user rollbacks',value,null)) user_rollbacks,
max(decode(ss.stat_name,'user commits',value,null)) user_commits,
max(decode(ss.stat_name,'session logical reads',value,null)) logical_reads,
max(decode(ss.stat_name,'db block changes',value,null)) db_block_changes,
max(decode(ss.stat_name,'physical reads',value,null)) physical_reads,
max(decode(ss.stat_name,'physical writes',value,null)) physical_writes,
max(decode(ss.stat_name,'user calls',value,null)) user_calls,
max(decode(ss.stat_name,'parse count (total)',value,null)) parses,
max(decode(ss.stat_name,'parse count (hard)',value,null)) parses_hard,
max(decode(ss.stat_name,'sorts (memory)',value,null)) sorts,
max(decode(ss.stat_name,'logons cumulative',value,null)) logons,
max(decode(ss.stat_name,'execute count',value,null)) executes
from dba_hist_sysstat ss,
dba_hist_snapshot s
where s.snap_id = ss.snap_id
and ss.stat_name in ('user rollbacks','user commits','session logical reads',
'db block changes','physical reads','physical writes','user calls',
'parse count (total)','parse count (hard)','sorts (memory)','logons cumulative',
'execute count','redo size')
group by s.end_interval_time,s.startup_time) t,
intervals int
where t.snap_time = int.snap
order by snap_time desc

viernes, 22 de enero de 2010

Concatenar valores de una columna en 11g R2 (usando la función LISTAGG)

Varias veces me han preguntado como hacer para concatenar los valores de una columna en una sola fila agrupados por otra cierta columna. Para eso se necesita un operador de concatenación tal como existe el operador SUM() o el AVG() para sumar o sacar el promedio de un conjunto de columnas con cierto criterio de agrupamiento. Para solucionar esto lo que siempre sugeria era crear el operador STRAGG, que es una función que pueden encontrar en la pagina asktom.oracle.com. Abajo voy a crear la función para mostrarles como funciona:


rop@ROP92> create or replace type string_agg_type as object
2 (
3 total varchar2(4000),
4
5 static function
6 ODCIAggregateInitialize(sctx IN OUT string_agg_type )
7 return number,
8
9 member function
10 ODCIAggregateIterate(self IN OUT string_agg_type ,
11 value IN varchar2 )
12 return number,
13
14 member function
15 ODCIAggregateTerminate(self IN string_agg_type,
16 returnValue OUT varchar2,
17 flags IN number)
18 return number,
19
20 member function
21 ODCIAggregateMerge(self IN OUT string_agg_type,
22 ctx2 IN string_agg_type)
23 return number
24 );
25 /

Type created.


rop@ROP92> create or replace type body string_agg_type
2 is
3
4 static function ODCIAggregateInitialize(sctx IN OUT string_agg_type)
5 return number
6 is
7 begin
8 sctx := string_agg_type( null );
9 return ODCIConst.Success;
10 end;
11
12 member function ODCIAggregateIterate(self IN OUT string_agg_type,
13 value IN varchar2 )
14 return number
15 is
16 begin
17 self.total := self.total || ',' || value;
18 return ODCIConst.Success;
19 end;
20
21 member function ODCIAggregateTerminate(self IN string_agg_type,
22 returnValue OUT varchar2,
23 flags IN number)
24 return number
25 is
26 begin
27 returnValue := ltrim(self.total,',');
28 return ODCIConst.Success;
29 end;
30
31 member function ODCIAggregateMerge(self IN OUT string_agg_type,
32 ctx2 IN string_agg_type)
33 return number
34 is
35 begin
36 self.total := self.total || ctx2.total;
37 return ODCIConst.Success;
38 end;
39 end;
40 /

Type body created.

rop@ROP92>
rop@ROP92> CREATE or replace
2 FUNCTION stragg(input varchar2 )
3 RETURN varchar2
4 PARALLEL_ENABLE AGGREGATE USING string_agg_type;
5 /

Function created.

rop@ROP92>
rop@ROP92> select deptno, stragg(ename)
2 from scott.emp
3 group by deptno
4 /

DEPTNO STRAGG(ENAME)
-------------------------------
10 CLARK,KING,MILLER
20 SMITH,FORD,ADAMS,SCOTT,JONES
30 ALLEN,BLAKE,MARTIN,TURNER,JAMES,WARD

En el ejemplo concatené los empleados en una sola fila agrupados por el departamento al cual pertenecen. Si tuvieran que concatenar mas valores y no le alcanza con varchar2 tambien pueden encontrar en el sitio de Tom una variante para usar CLOB, aunque es bastante mas lenta.

Ahora bien, en 11g R2 hubo una actualizacion importante de la funciones analiticas, tambien llamadas Analytic Functions II, y una de estas nuevas funciones (LISTAGG)se puede utilizar para hacer lo mismo que stragg en forma nativa evitando tener que crear el tipo stragg y demás. Les muestro un ejemplito para listar los tipos de trabajo que hay en cada departamento:


rop@ROP112> select deptno, listagg(job,',') within group (order by job) jobs
2 from (select distinct deptno, job from scott.emp)
3 group by deptno
4 order by deptno
5 /

DEPTNO JOBS
---------- ------------------------------
10 CLERK,MANAGER,PRESIDENT
20 ANALYST,CLERK,MANAGER
30 CLERK,MANAGER,SALESMAN


Como siempre digo, es importante leer los manuales cada vez que se libera una nueva versión, sobre todo el "Oracle New Features" que es un resumen de las nuevas caracteriticas. Me sucede a menudo que veo codigos o formas de administración antiguas que insumen muchas horas para hacer lo mismo que ya esta resuelto en forma nativa o mas simple a partir de una nueva versión.

martes, 15 de diciembre de 2009

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

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


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

Tabla creada.

rop@DESA11G> create sequence t_seq;

Secuencia creada.



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


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



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


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

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


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

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

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

domingo, 26 de julio de 2009

Convertir segundos en años, meses, días, horas, minutos y segundos

Hace unos años un desarrollador me preguntó como podia hacer para convertir una columna de segundos acumulados en horas, minutos y segundos ya que se necesitaba mostrar esa información en un reporte para una compania de celulares. La pregunta me resultó muy interesante y la pude resolver de una forma bastante sencilla, utilizando solo sentencias sql (había encontrado otras soluciones pero utilizaban código VB o PL/SQL).
Un tiempo compartí la solución un sitio de Oracle y una semanas despues la publicaron en una sección de códigos utiles: "Convert Seconds to Hours, Minutes, and Seconds"
Viendo de armar una nota sobre seguridad de passwords en Oracle, necesitaba ver las cantidad de tiempo que demandaría crackear una password que requeria mostrar el tiempo necesario en romper una password medido en años, meses, dias, horas, minutos y segundos. Extendiendo el código referenciado arriba armé la siguiente función:

rop@DESA10G> create or replace function f_get_duration (p_sec number) return varchar2
2  is
3   l_dur varchar2(50);
4  begin
5   l_dur := to_char(extract(year from numtoyminterval(months_between(sysdate+(p_sec/60/60/24),sysdate),'month')),'0009') || ' ' ||
6            to_char(extract(month from numtoyminterval(months_between(sysdate+(p_sec/60/60/24),sysdate),'month')),'09')|| ' ' ||
7            to_char(extract(day from numtodsinterval ((sysdate+(p_sec/60/60/24))-add_months(sysdate,trunc(months_between(sysdate+
    (p_sec/60/60/24),sysdate))),'day' )),'009')|| ' ' ||
8            to_char(trunc(mod(p_sec,86400)/60/60),'09') || ' ' ||
9            to_char(trunc(mod(p_sec,3600)/60),'09') || ' ' ||
10           to_char(mod(mod(p_sec,3600),60),'09');
11   return l_dur;
12  end;
13  /

Función creada.
rop@DESA10G>

Abajo les paso unos ejemplos de uso:

Probemos con la cantidad de segundos en 22 horas:
rop@DESA10G> select f_get_duration(60*60*22) "YY MM DD HH MI SS" from dual;

YY MM DD HH MI SS
----------------------------------------------------------------------------------------------------
0000  00  000  22  00  00

Ahora con los segundos en 1 año y 22 horas:
rop@DESA10G> select f_get_duration((60*60*24*365)+(60*60*22))"YY MM DD HH MI SS" from dual;

YY MM DD HH MI SS
----------------------------------------------------------------------------------------------------
0001  00  000  22  00  00

Por ultimo con los segundos en 5 años y alrededor de 5 meses (puede variar porque tomo meses de 30 dias y dependerá del momento en que se corra)
rop@DESA10G>  select f_get_duration((60*60*24*365*5)+(60*60*24*30*5)) "YY MM DD HH MI SS" from dual;

YY MM DD HH MI SS
----------------------------------------------------------------------------------------------------
0005  04  026  00  00  00

rop@DESA10G>

Vemos que dió 5 años, 4 meses y 26 dias.

En una futura nota sobre algoritmos de fuerza bruta para "crackear" passwords les voy a mostrar usando la función extendida F_GET_DURATION cuanto tiempo se demoraría en romper una password de n caracteres.