Archivo de la etiqueta: base de datos

Conversión de fechas RFC3339 a Oracle timestamp

La consulta siguiente muestra la conversión del formato de fechas RFC3339, muy común en servicios REST y objetos JSON, al timpo de dato timestamp nativo en Oracle.

select TO_TIMESTAMP_TZ('2020-12-15T15:20:28Z', 'YYYY-MM-DD"T"HH24:MI:SS.FF9 TZH:TZM') as RFC3339_to_timestamp_zulu,
       TO_TIMESTAMP_TZ('2020-12-15T15:20:28Z', 'YYYY-MM-DD"T"HH24:MI:SS.FF9 TZH:TZM') AT TIME ZONE 'Europe/Madrid' as RFC3339_to_timestamp_madrid
from dual;
RFC3339_TO_TIMESTAMP_ZULU         RFC3339_TO_TIMESTAMP_MADRID                
--------------------------------- -------------------------------------------
15/12/2020 15:20:28,000000000 GMT 15/12/2020 16:20:28,000000000 EUROPE/MADRID
Anuncio publicitario

12 Things Developers Will Love About Oracle Database 12c Release 2 (All Things SQL)

La nueva versión de Oracle 12cR2 (o 12.2) ya se encuentra disponible en Oracle Cloud. En el siguiente artículo se revisan las principales novedades: 12 Things Developers Will Love About Oracle Database 12c Release 2 (All Things SQL)

  • Easier, Better, Faster, Stronger JSON
    • JSON from SQL
    • JSON in PL/SQL
  • Looooooooooong Names
  • Robust Code using Constants for Data Type Lengths
  • Listagg Improved On Overflow
  • Lightning Fast SQL with Real Time Materialized Views
  • Fast Estimates with Approximate Query Enhancements
  • Verify Data Type Conversions
  • Handle Casting Conversion Errors
  • Single Statement Table Partitioning
  • Automatic List Partitioning
  • Mark Old Code as «Not for Use»
  • PL/SQL Code Coverage

Oracle 12.2 Cool New Features

En el último Oracle Open World se ha anunciado el lanzamiento de Oracle 12cR2 (o 12.2). En el siguiente artículo se repasan algunas de las novedades más importantes: Oracle 12.2 Cool New Features

  • Partitioning
    • Alter table to partitioned table
    • Create table for exchange
  • In-Memory
    • Standby Database: In-Memory option
    • Dynamic resizing
    • Fast start
  • Security
    • Online encryption and re-key operations
    • Internal tablespace encryption
    • DB vault simulation
  • Dictionary
    • Long identifiers
  • PDB
    • Resource management
    • Hot clone
    • Local undo
    • Flashback
    • Characterset
  • Others
    • Index Monitoring
    • Materialized Views – real time refresh
    • External Tables – partitions

Select random rows

Para seleccionar una o varias filas de una tabla de manera aleatoria existen distintos enfoques. En este ejemplo se intenta obtener una fila de la tabla EMP tomando una muestra del 25% de los registros totales sobre el que posteriormente se aplica una ordenación utilizando la función DBMS_RANDOM.random. De esta forma podemos aplicar una ordenación aleatoria sobre los registros de la tabla evitando el alto coste de ejecutar la función random en tablas que sean muy grandes.

-- Select 1 random row from EMP table
select *
from ( select *
    from emp sample(25) -- 25% total rows
    order by dbms_random.random)
where rownum = 1;

El resultado:

EMPNO	ENAME	JOB	MGR	HIREDATE	SAL	COMM	DEPTNO
7698	BLAKE	MANAGER	7839	01-MAY-81	2850	-	30

GitHub: Oracle Database Sample Schemas

A través de GitHub – oracle/db-sample-schemas Oracle pone a disposición de la comunidad de desarrolladores y diseñadores de bases de datos los scripts para generar los siguientes schemas:

  • HR: Human Resources
  • OE: Order Entry
  • PM: Product Media
  • IX: Information Exchange
  • SH: Sales History
  • BI: Business Intelligence

HEXTOBLOB

Implementación de la función HEXTORAW para dar soporte a CLOBS:

create or replace
procedure hextoblob(
                    src_clob  in       clob,
                    dest_blob in out   nocopy blob
				   )
is
    v_pos    number := 1;
    v_amount number := 1024;
    v_buffer varchar2(1024);
    v_raw    raw(2048);
begin

    while v_pos < dbms_lob.getlength(src_clob)
    loop
        dbms_lob.read(
            lob_loc => src_clob, 
            amount  => v_amount, 
            offset  => v_pos, 
            buffer  => v_buffer);
        
        v_raw := hextoraw(v_buffer);

        dbms_lob.writeappend(
            lob_loc => dest_blob, 
            amount  => utl_raw.length(v_raw), 
            buffer  => v_raw);
        
        v_pos := v_pos + v_amount;
    end loop;
    
end hextoblob;

Para probar el procedimiento podemos ejecutar el siguinte código:

declare
  cl_test   clob := '4041424344';
  bl_result blob;
begin

  dbms_lob.createtemporary(
    lob_loc => bl_result,
    cache   => false,
    dur     => dbms_lob.call);

  hextoblob(
    src_clob  => cl_test,
    dest_blob => bl_result);

  dbms_output.put_line('Result: '||
                        utl_raw.cast_to_varchar2( bl_result ));

  dbms_lob.freetemporary( bl_result );

end;

El resultado debe ser:

Result: @ABCD

SQL vs R

Introducción

Vamos a realizar una breve comparativa entre SQL y R. Para ello nos centraremos en los aspectos básicos del acceso a datos, mostrando ejemplos de las mismas operaciones en ambos lenguajes.

En junio de 2015 Oracle publicó el documento SQL – the natural language for analysis en el que se describen los fundamentos del lenguaje SQL y las ventajas que ofrece en el campo del acceso y análisis de datos:

http://www.oracle.com/technetwork/database/bi-datawarehousing/wp-sqlnaturallanguageanalysis-2431343.pdf

Éste documento será nuestra guía en la pequeña comparativa que sigue a continucaión.

Preparación del entorno

El juego de datos que se utiliza en los ejemplos del documento son las conocidas tablas EMP y DEPT, que tradicionalmente han acompañado a Oracle. Desde R se puede acceder a una gran variedad de fuentes de datos, entre las que se incluyen bases de datos, ficheros, recursos web, etc.

En este caso, para acceder a los datos de las tablas Oracle desde R, primero los volcaremos a un fichero con estructura CSV y los cargaremos utilizando la librería de R «DPLYR».

Las siguientes sentencias SQL nos pueden servir para generar la estructura CVS:

select '"EMPNO","ENAME","JOB","MGR","HIREDATE","SAL","COMM","DEPTNO"' from dual
union all
select EMPNO||',"'||ENAME||'","'||JOB||'",'||MGR||','||HIREDATE||','||SAL||','||COMM||','||DEPTNO
from emp;
select '"DEPTNO","DNAME","LOC"' from dual
union all
select DEPTNO||',"'||DNAME||'","'||LOC||'"'
from dept;

Después de guardar los datos de EMP y DEPT en disco con formato CSV será necesario copiar ambos ficheros en una ruta a la que se tenga acceso desde el entorno de ejecución de R (por ejemplo, un subdirectorio «./data» dentro del espacionde trabajo) y ejecutar los siguientes comandos:

library(dplyr)
emp <- tbl_dt(read.csv("./data/emp.csv",stringsAsFactors = FALSE))
dept <- tbl_dt(read.csv("./data/dept.csv",stringsAsFactors = FALSE))

Principios de SQL

El lenguaje SQL se compone de cuatro operadores básicos:

  • Projection
  • Filter
  • Join
  • Aggregate

Projection

El operador projection permite indicar las columnas que queremos recuperar de una tabla. En su forma básica, se indican las columnas existentes por su nombre o posición en la tabla.

SELECT ename, job, hiredate
FROM emp;
ENAME	JOB		HIREDATE
-----	---------	--------------------
KING	PRESIDENT	17-NOV-1981 00:00:00
BLAKE	MANAGER		01-MAY-1981 00:00:00
CLARK	MANAGER		09-JUN-1981 00:00:00
JONES	MANAGER		02-APR-1981 00:00:00
-- Next lines were deleted
emp %>%
select(ENAME, JOB, HIREDATE)
Source: local data table [14 x 3]

    ENAME       JOB             HIREDATE
    (chr)     (chr)                (chr)
1    KING PRESIDENT 17-NOV-1981 00:00:00
2   BLAKE   MANAGER 01-MAY-1981 00:00:00
3   CLARK   MANAGER 09-JUN-1981 00:00:00
4   JONES   MANAGER 02-APR-1981 00:00:00
# Next lines were deleted

La proyección, en su forma extendida, permite generar columnas dinámicas aplicando operaciones que son calculadas en tiempo de ejecución.

SELECT ename, sal, comm, comm/sal*100 as COMM_RATE
FROM emp;
ENAME	SAL	COMM	COMM_RATE
-----	----	----	---------
KING	5000	- 	-
BLAKE	2850	- 	-
CLARK	2450	- 	-
JONES	2975	- 	-
SCOTT	3000	- 	-
FORD	3000	- 	-
SMITH	800	- 	-
ALLEN	1600	300	18.75
WARD	1250	500	40
MARTIN	1250	1400	112
TURNER	1500	0	0
ADAMS	1100	- 	-
JAMES	950	- 	-
MILLER	1300	- 	- 
emp %>%
mutate(COMM_RATE = COMM/SAL*100) %>%
select(ENAME,SAL,COMM,COMM_RATE)
Source: local data table [14 x 4]

    ENAME   SAL  COMM COMM_RATE
    (chr) (int) (int)     (dbl)
1    KING  5000    NA        NA
2   BLAKE  2850    NA        NA
3   CLARK  2450    NA        NA
4   JONES  2975    NA        NA
5   SCOTT  3000    NA        NA
6    FORD  3000    NA        NA
7   SMITH   800    NA        NA
8   ALLEN  1600   300     18.75
9    WARD  1250   500     40.00
10 MARTIN  1250  1400    112.00
11 TURNER  1500     0      0.00
12  ADAMS  1100    NA        NA
13  JAMES   950    NA        NA
14 MILLER  1300    NA        NA

Filter

El operador filter permite indicar una serie de reglas que deben cumplir los datos almacenados para ser recuperados en la consulta. En su forma más sencilla comparan los valores de la tabla con una expresión.

SELECT ename, job, hiredate
FROM emp
WHERE job = 'CLERK';
ENAME	JOB	HIREDATE
------	-----	--------------------
SMITH	CLERK	17-DEC-1980 00:00:00
ADAMS	CLERK	12-JAN-1983 00:00:00
JAMES	CLERK	03-DEC-1981 00:00:00
MILLER	CLERK	23-JAN-1982 00:00:00
emp %>%
filter(JOB == "CLERK") %>%
select(ENAME, JOB, HIREDATE)
Source: local data table [4 x 3]

   ENAME   JOB             HIREDATE
   (chr) (chr)                (chr)
1  SMITH CLERK 17-DEC-1980 00:00:00
2  ADAMS CLERK 12-JAN-1983 00:00:00
3  JAMES CLERK 03-DEC-1981 00:00:00
4 MILLER CLERK 23-JAN-1982 00:00:00

Join

El operador join relaciona distintas tablas a través de la comparación de uno o varios de sus campos. Existen varios tipos de join dependiendo de la forma en la que va a realizar el cruzado de los datos: inner, full outer, outer left, outer right y cross.

SELECT d.deptno, d.dname, e.empno, e.ename
FROM dept d INNER JOIN emp e
 ON (e.deptno = d.deptno);
DEPTNO	DNAME		EMPNO	ENAME
------	----------	-----	------
10	ACCOUNTING	7782	CLARK
10	ACCOUNTING	7934	MILLER
10	ACCOUNTING	7839	KING
20	RESEARCH	7902	FORD
20	RESEARCH	7788	SCOTT
-- Next lines were deleted
emp %>%
inner_join(dept, by = c("DEPTNO"="DEPTNO")) %>%
select(DEPTNO,DNAME,EMPNO,ENAME)
Source: local data table [14 x 4]

   DEPTNO      DNAME EMPNO  ENAME
    (int)      (chr) (int)  (chr)
1      10 ACCOUNTING  7839   KING
2      10 ACCOUNTING  7782  CLARK
3      10 ACCOUNTING  7934 MILLER
4      20   RESEARCH  7566  JONES
5      20   RESEARCH  7788  SCOTT
# Next lines were deleted

Aggregate

El operador aggregate ejecuta operaciones sobre grupos, o sets, de datos.

SELECT SUM(sal) AS total_salary
FROM emp;
TOTAL_SALARY
------------
29025
emp %>%
summarise(total_salary = sum(SAL))
Source: local data table [1 x 1]

  total_salary
         (int)
1        29025

Este operador es especialmente importante en el análisis de datos porque permite acotar las operaciones a rangos o agrupaciones de los datos sobre los que estamos trabajando.

SELECT deptno,
       COUNT(empno) AS no_of_employees,
       SUM(sal) AS total_salary,
       AVG(sal) AS average_salary
FROM emp
GROUP BY deptno;
DEPTNO	NO_OF_EMPLOYEES	TOTAL_SALARY	AVERAGE_SALARY
------	---------------	------------	----------------------------------------
30	6		9400		1566.66666666666666666666666666666666667
20	5		10875		2175
10	3		8750		2916.66666666666666666666666666666666667
emp %>%
group_by(DEPTNO) %>%
summarise(no_of_employees = length(EMPNO),total_salary = sum(SAL), average_salary = mean(SAL))
Source: local data table [3 x 4]

  DEPTNO no_of_employees total_salary average_salary
   (int)           (int)        (int)          (dbl)
1     10               3         8750       2916.667
2     30               6         9400       1566.667
3     20               5        10875       2175.000

Generar SHA de una cadena (PL/SQL + Java)

El paquete DBMS_CRYPTO nos permite generar hash de una cadena de texto utilizando distintos algoritmos como MD4, MD5 y SHA1. Pero si queremos generar un hash mediante SHA-256, SHA-384 o SHA-512 tenemos que apoyarnos en Java. Los siguientes bloques de código incorporan la posibilidad de generar estos hash desde SQL o PL/SQL.

Los algoritmos disponibles son los siguientes:

  • MD5
  • SHA-1
  • SHA-256
  • SHA-384
  • SHA-512

El primer paso es crear la clase Java que nos permita generar hash más complejos y compilarla en la base de datos:

create or replace and compile java source named encoder as
package com.test;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import sun.misc.BASE64Encoder;

public class Encoder {

    public static String encode(String message, String algorithm) {
        
        String encodedMessage;
        
        try {
            MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
            byte[] hash = messageDigest.digest(message.getBytes("UTF-8"));
            encodedMessage = (new BASE64Encoder()).encode(hash);
        
        } catch (Exception e) {
            encodedMessage = null;
        }
        
        return encodedMessage;
    }
} 

Una vez compilada la clase Java en nuestra base de datos, es necesario crear una función PL/SQL de enlace para poder invocar la clase desde una sentencia SQL o desde código PL/SQL:

create or replace function encoder(p_string    in varchar2,
                                   p_algorithm in varchar2) return varchar2 as
language java name 'com.test.Encoder.encode(java.lang.String, java.lang.String) return String';

Por último, un ejemplo de uso:

select Encoder('Hello', 'SHA-256') "hash_Hello",
       Encoder('HELLO', 'SHA-256') "hash_HELLO"
from dual;
hash_Hello
--------------------------------------------
GF+NsyJx/iX1Yab8k4suJkMG7DBO2lGAB9F2SCY4GWk=

hash_HELLO
--------------------------------------------
NzPNl3/46xi5hzV+Is7Zn0YJfzHssjnoeK5jdg6D5NU=

Ver también Generar SH1 de una cadena y Generar el HASH de una cadena

Buscar intervalos – Lead analytic function

¿Cómo encontrar intervalos libres en una secuencia temporal?

Ésta fue la pregunta que surgió en el transcurso de un proyecto en el que los usuarios tenían que dar de alta periodos temporales con una duración variable y que no tenían por qué seguir un orden. Estos periodos temporales se registraron en una tabla general de periodos, en la que se asociaba a cada usuario los periodos que iba dando de alta con las fechas de inicio y final de periodo. Cada vez que un usuario iba a registrar un nuevo periodo el sistema debía comprobar si este periodo existía ya o era necesario darlo de alta. Pero, ¿cómo buscar si el nuevo periodo de tiempo estaba ya comprendido entre los registrados anteriormente por el usuario?

La respuesta que finalmente encontramos fue utilizar las funciones analíticas MAX y LEAD. Gracias a estas dos funciones, una simple SELECT nos podía devolver los intervalos libres entre los periodos ya registrados en la tabla.

Para demostrar su funcionamiento, primero vamos a crear un pequeño escenario de pruebas. En la tabla «lead_test» vamos a insertar una serie de registros con fechas de inicio y final que nos permitan probar la solución:

-- Clean environment
drop table lead_test;

-- Create test environment
create table lead_test (id number, date_start date, date_end date);

insert into lead_test values(1, to_date('01/01/2014 12:00:01', 'dd/mm/yyyy hh24:mi:ss'), to_date('01/02/2014 12:00:00', 'dd/mm/yyyy hh24:mi:ss'));
insert into lead_test values(3, to_date('01/03/2014 12:00:01', 'dd/mm/yyyy hh24:mi:ss'), to_date('01/04/2014 12:00:00', 'dd/mm/yyyy hh24:mi:ss'));
insert into lead_test values(4, to_date('15/03/2014 22:00:00', 'dd/mm/yyyy hh24:mi:ss'), to_date('01/05/2014 12:00:00', 'dd/mm/yyyy hh24:mi:ss'));
insert into lead_test values(5, to_date('01/05/2014 12:00:01', 'dd/mm/yyyy hh24:mi:ss'), to_date('01/06/2014 12:00:00', 'dd/mm/yyyy hh24:mi:ss'));

commit;

-- Alter default date format
alter session set nls_date_format = 'dd/mm/yyyy hh24:mi:ss';

Una vez ejecutadas las sentencias que preparan el entorno de pruebas vamos a comprobar cómo queda nuestra tabla «lead_test:

-- Show test data
select * from lead_test;

        ID DATE_START          DATE_END
---------- ------------------- -------------------
         1 01/01/2014 12:00:01 01/02/2014 12:00:00
         3 01/03/2014 12:00:01 01/04/2014 12:00:00
         4 15/03/2014 22:00:00 01/05/2014 12:00:00
         5 01/05/2014 12:00:01 01/06/2014 12:00:00

Estos datos nos proporcionan una casuística variada:

  • Hemos omitido el registro con ID = 2 para dejar el intervalo libre.
  • Los registros con ID 3 y 4 se solapan en el tiempo.
  • Los registros con ID 4 y 5 empiezan y acaban en el mismo momento, justo uno a continuación del otro pero sin solapamiento.

En una primera aproximación, se elaboró la siguiente consulta que nos permitía obtener los intervalos que no se encontraban cubiertos por ninguno de los periodos registrados en la tabla:

-- Search All Gaps
select *
from (
      select max(l.date_end) over (order by l.date_start) gap_start,
             lead(l.date_start) over (order by l.date_start) gap_end
      from lead_test l
     )
where gap_start < gap_end;

Como resultado obtendremos el intervalo que existe entre los registros con ID 1 y 3. Pero también el intervalo entre los registros con ID 4 y 5, puesto que no hay solapamiento:

GAP_START           GAP_END
------------------- -------------------
01/02/2014 12:00:00 01/03/2014 12:00:01
01/05/2014 12:00:00 01/05/2014 12:00:01

Con una pequeña modificación en la consulta podemos regular la duración mínima de los intervalos que queremos encontrar. Y, de este modo, podemos descartar aquellos intervalos que resulten demasiado pequeños para nuestros requerimientos:

-- Search Gaps > 1 sec.
select *
from (
      select max(l.date_end) over (order by l.date_start) gap_start,
             lead(l.date_start) over (order by l.date_start) gap_end
      from lead_test l
     )
where gap_start < gap_end
      and (gap_end - gap_start) > (1/24/60/60);

Al incluir como condición que el tiempo entre final de un intervalo y el inicio del intervalo que le sigue sea mayor a 1 segundo (la fórmula 1/24/60/60 representa un segundo en operaciones con fechas en las consultas SQL de Oracle) obtendremos solo el primer intervalo, entre los periodos con ID 1 y 3:

GAP_START           GAP_END
------------------- -------------------
01/02/2014 12:00:00 01/03/2014 12:00:01

Generar SH1 de una cadena

El paquete DBMS_CRYPTO nos permite, entre otras cosas, generar SHA1 de cualquier cadena de texto. Admite los siguientes parámetros:

  • src – [RAW|BLOB|CLOB]: datos a los que aplicar el hash
  • typ – PLS_INTEGER: algoritmo hash a aplicar

Los algoritmos disponibles son los siguientes:

  • HASH_MD4 := 1
  • HASH_MD5 := 2
  • HASH_SH1 := 3

Nota: Es necesario conceder permisos de ejecución sobre este paquete a los usuarios que vayan a utilizarlo.

select DBMS_CRYPTO.hash(utl_raw.cast_to_raw ('Hello'),
                        3) "hash_Hello",
       DBMS_CRYPTO.hash(utl_raw.cast_to_raw ('HELLO'),
                        3) "hash_HELLO"
from dual;
hash_Hello
----------------------------------------
F7FF9E8B7BB2E09B70935A5D785E0CC5D9D0ABF0

hash_HELLO
----------------------------------------
C65F99F8C5376ADADDDC46D5CBCF5762F9E55EB7

Ver también Generar el HASH de una cadena