Mis Presentaciones en el Oracle Open World 2014

Un post muy breve para informarles de las dos presentaciones que voy a realizar en el Oracle Open World 2014.

Session ID:         UGF4482
Session Title:     “Getting Started with SQL Pattern Matching in Oracle Database 12c
Venue / Room:  Moscone South – 301
Date and Time:  9/28/14, 13:30 – 14:15

Session ID:          CON4493
Session Title:      “Regular Expressions in Oracle Database 101”
Venue / Room:   Moscone South – 303
Date and Time:   10/2/14, 13:15 – 14:00

No olviden de confirmar que las presentaciones sean en los salones que indico porque a veces hacen cambios de última hora.

Espero verlos por allá!

Anuncios

Expresiones de Tablas Comunes Recursivas al Rescate

Traducción del post original publicado en Noviembre 27 2013.

Las expresiones de tablas comunes recursivas han estado disponibles en Oracle desde la versión 11g R2 que fue lanzada por allá en el año 2009. Siempre las había considerado como una alternativa a la cláusula connect by para poder escribir consultas jerárquicas que siguieran el estándar ANSI, así que nunca me había tomado la molestia de revisar su funcionalidad en detalle.

Durante Kscope13, tuve la oportunidad de asistir a una sesión de Dominic Delmolino donde él mostró que las expresiones de tablas comunes recursivas pueden ser utilizadas para resolver otros tipos de problemas. Uno de los casos justamente es cuando necesitamos “generar un valor” y luego queremos hacer referencia a dicho valor en el siguiente registro de nuestra consulta.

No hace mucho tuve que trabajar en una consulta con un requerimiento similar. La idea básica es que tenemos un número variable de registros por cada cliente y para cada registro debemos establecer o calcular un nuevo valor (a la cual vamos a llamar VAL). La parte interesante está en que para algunos registros, VAL debe ser determinado utilizando el VAL que se encuentra en el registro anterior.

En primer lugar voy a crear una tabla y voy a insertar unos registros que ayuden a visualizar el problema.


SQL> CREATE TABLE RSF_TEST
 2 (
 3 ID_CLIENT NUMBER NOT NULL,
 4 DT_ELIG DATE NOT NULL,
 5 TP NUMBER NOT NULL,
 6 SRV_GRP NUMBER NOT NULL,
 7 SRV_CODE NUMBER NOT NULL,
 8 DT_ADMIT DATE NOT NULL,
 9 DT_DISCHARGE DATE NOT NULL
 10 );

Table created

SQL> insert into RSF_TEST (ID_CLIENT, DT_ELIG, TP, SRV_GRP, SRV_CODE, DT_ADMIT, DT_DISCHARGE)
 2 values (525459313, to_date('01-11-2013', 'dd-mm-yyyy'), 13, 19, 12, to_date('01-05-2013', 'dd-mm-yyyy'), to_date('31-12-2013', 'dd-mm-yyyy'));
1 row inserted

SQL> insert into RSF_TEST (ID_CLIENT, DT_ELIG, TP, SRV_GRP, SRV_CODE, DT_ADMIT, DT_DISCHARGE)
 2 values (615086545, to_date('01-11-2013', 'dd-mm-yyyy'), 13, 19, 12, to_date('07-03-2013', 'dd-mm-yyyy'), to_date('31-03-2014', 'dd-mm-yyyy'));
1 row inserted

SQL> insert into RSF_TEST (ID_CLIENT, DT_ELIG, TP, SRV_GRP, SRV_CODE, DT_ADMIT, DT_DISCHARGE)
 2 values (525459313, to_date('01-10-2013', 'dd-mm-yyyy'), 13, 19, 12, to_date('01-05-2013', 'dd-mm-yyyy'), to_date('31-12-2013', 'dd-mm-yyyy'));
1 row inserted

SQL> insert into RSF_TEST (ID_CLIENT, DT_ELIG, TP, SRV_GRP, SRV_CODE, DT_ADMIT, DT_DISCHARGE)
 2 values (615086545, to_date('01-10-2013', 'dd-mm-yyyy'), 13, 19, 12, to_date('07-03-2013', 'dd-mm-yyyy'), to_date('31-03-2014', 'dd-mm-yyyy'));
1 row inserted

SQL> insert into RSF_TEST (ID_CLIENT, DT_ELIG, TP, SRV_GRP, SRV_CODE, DT_ADMIT, DT_DISCHARGE)
 2 values (615086545, to_date('01-09-2013', 'dd-mm-yyyy'), 13, 19, 12, to_date('07-03-2013', 'dd-mm-yyyy'), to_date('31-03-2014', 'dd-mm-yyyy'));
1 row inserted

SQL> insert into RSF_TEST (ID_CLIENT, DT_ELIG, TP, SRV_GRP, SRV_CODE, DT_ADMIT, DT_DISCHARGE)
 2 values (525459313, to_date('01-09-2013', 'dd-mm-yyyy'), 13, 19, 12, to_date('01-05-2013', 'dd-mm-yyyy'), to_date('31-12-2013', 'dd-mm-yyyy'));
1 row inserted

SQL> insert into RSF_TEST (ID_CLIENT, DT_ELIG, TP, SRV_GRP, SRV_CODE, DT_ADMIT, DT_DISCHARGE)
 2 values (525459313, to_date('01-08-2013', 'dd-mm-yyyy'), 13, 19, 12, to_date('01-05-2013', 'dd-mm-yyyy'), to_date('31-12-2013', 'dd-mm-yyyy'));
1 row inserted

SQL> insert into RSF_TEST (ID_CLIENT, DT_ELIG, TP, SRV_GRP, SRV_CODE, DT_ADMIT, DT_DISCHARGE)
 2 values (615086545, to_date('01-08-2013', 'dd-mm-yyyy'), 13, 19, 12, to_date('07-03-2013', 'dd-mm-yyyy'), to_date('31-03-2014', 'dd-mm-yyyy'));
1 row inserted

SQL> insert into RSF_TEST (ID_CLIENT, DT_ELIG, TP, SRV_GRP, SRV_CODE, DT_ADMIT, DT_DISCHARGE)
 2 values (615086545, to_date('01-07-2013', 'dd-mm-yyyy'), 13, 19, 12, to_date('07-03-2013', 'dd-mm-yyyy'), to_date('31-03-2014', 'dd-mm-yyyy'));
1 row inserted

SQL> insert into RSF_TEST (ID_CLIENT, DT_ELIG, TP, SRV_GRP, SRV_CODE, DT_ADMIT, DT_DISCHARGE)
 2 values (525459313, to_date('01-07-2013', 'dd-mm-yyyy'), 13, 19, 12, to_date('01-05-2013', 'dd-mm-yyyy'), to_date('31-12-2013', 'dd-mm-yyyy'));
1 row inserted

SQL> insert into RSF_TEST (ID_CLIENT, DT_ELIG, TP, SRV_GRP, SRV_CODE, DT_ADMIT, DT_DISCHARGE)
 2 values (525459313, to_date('01-06-2013', 'dd-mm-yyyy'), 13, 19, 12, to_date('01-05-2013', 'dd-mm-yyyy'), to_date('31-12-2013', 'dd-mm-yyyy'));
1 row inserted

SQL> insert into RSF_TEST (ID_CLIENT, DT_ELIG, TP, SRV_GRP, SRV_CODE, DT_ADMIT, DT_DISCHARGE)
 2 values (615086545, to_date('01-06-2013', 'dd-mm-yyyy'), 13, 19, 12, to_date('07-03-2013', 'dd-mm-yyyy'), to_date('31-03-2014', 'dd-mm-yyyy'));
1 row inserted

SQL> insert into RSF_TEST (ID_CLIENT, DT_ELIG, TP, SRV_GRP, SRV_CODE, DT_ADMIT, DT_DISCHARGE)
 2 values (525459313, to_date('01-05-2013', 'dd-mm-yyyy'), 13, 19, 12, to_date('01-05-2013', 'dd-mm-yyyy'), to_date('31-12-2013', 'dd-mm-yyyy'));
1 row inserted

SQL> insert into RSF_TEST (ID_CLIENT, DT_ELIG, TP, SRV_GRP, SRV_CODE, DT_ADMIT, DT_DISCHARGE)
 2 values (615086545, to_date('01-05-2013', 'dd-mm-yyyy'), 13, 19, 12, to_date('07-03-2013', 'dd-mm-yyyy'), to_date('31-03-2014', 'dd-mm-yyyy'));
1 row inserted

SQL> insert into RSF_TEST (ID_CLIENT, DT_ELIG, TP, SRV_GRP, SRV_CODE, DT_ADMIT, DT_DISCHARGE)
 2 values (525459313, to_date('01-04-2013', 'dd-mm-yyyy'), 13, 19, 12, to_date('12-12-2012', 'dd-mm-yyyy'), to_date('30-04-2013', 'dd-mm-yyyy'));
1 row inserted

SQL> insert into RSF_TEST (ID_CLIENT, DT_ELIG, TP, SRV_GRP, SRV_CODE, DT_ADMIT, DT_DISCHARGE)
 2 values (615086545, to_date('01-04-2013', 'dd-mm-yyyy'), 13, 19, 12, to_date('07-03-2013', 'dd-mm-yyyy'), to_date('31-03-2014', 'dd-mm-yyyy'));
1 row inserted

SQL> insert into RSF_TEST (ID_CLIENT, DT_ELIG, TP, SRV_GRP, SRV_CODE, DT_ADMIT, DT_DISCHARGE)
 2 values (615086545, to_date('01-03-2013', 'dd-mm-yyyy'), 13, 19, 12, to_date('07-03-2013', 'dd-mm-yyyy'), to_date('31-03-2014', 'dd-mm-yyyy'));
1 row inserted

SQL> insert into RSF_TEST (ID_CLIENT, DT_ELIG, TP, SRV_GRP, SRV_CODE, DT_ADMIT, DT_DISCHARGE)
 2 values (525459313, to_date('01-03-2013', 'dd-mm-yyyy'), 13, 19, 12, to_date('12-12-2012', 'dd-mm-yyyy'), to_date('30-04-2013', 'dd-mm-yyyy'));
1 row inserted

SQL> insert into RSF_TEST (ID_CLIENT, DT_ELIG, TP, SRV_GRP, SRV_CODE, DT_ADMIT, DT_DISCHARGE)
 2 values (525459313, to_date('01-02-2013', 'dd-mm-yyyy'), 13, 19, 12, to_date('12-12-2012', 'dd-mm-yyyy'), to_date('30-04-2013', 'dd-mm-yyyy'));
1 row inserted

SQL> insert into RSF_TEST (ID_CLIENT, DT_ELIG, TP, SRV_GRP, SRV_CODE, DT_ADMIT, DT_DISCHARGE)
 2 values (525459313, to_date('01-01-2013', 'dd-mm-yyyy'), 13, 19, 12, to_date('12-12-2012', 'dd-mm-yyyy'), to_date('30-04-2013', 'dd-mm-yyyy'));
1 row inserted

SQL> insert into RSF_TEST (ID_CLIENT, DT_ELIG, TP, SRV_GRP, SRV_CODE, DT_ADMIT, DT_DISCHARGE)
 2 values (525459313, to_date('01-12-2012', 'dd-mm-yyyy'), 13, 19, 12, to_date('12-12-2012', 'dd-mm-yyyy'), to_date('30-04-2013', 'dd-mm-yyyy'));
1 row inserted

SQL> insert into RSF_TEST (ID_CLIENT, DT_ELIG, TP, SRV_GRP, SRV_CODE, DT_ADMIT, DT_DISCHARGE)
 2 values (525459313, to_date('01-03-2012', 'dd-mm-yyyy'), 13, 1, 1, to_date('18-01-2012', 'dd-mm-yyyy'), to_date('29-03-2012', 'dd-mm-yyyy'));
1 row inserted

SQL> commit;

Commit complete

SQL>

A continuación tenemos una versión simplificada del problema. La consulta original contiene más predicados y las sentencias CASE son un poco más complejas ya que deben cubrir una serie de posibles combinaciones de las columnas srv_grp, srv_code. Traté de limitar el código de forma que únicamente incluya lo necesario para mostrar cómo determinar VAL para los registros que existen en mi tabla.


WITH
clients AS
(
 SELECT
   id_client,
   dt_elig,
   tp,
   srv_grp,
   srv_code,
   dt_admit,
   dt_discharge,
   row_number() OVER (PARTITION BY id_client ORDER BY id_client, dt_elig) rn
 FROM rsf_test
),
results (id_client, dt_elig, tp, srv_grp, srv_code, dt_admit, dt_discharge, rn, val) AS
(
 SELECT
   id_client,
   dt_elig,
   tp,
   srv_grp,
   srv_code,
   dt_admit,
   dt_discharge,
   rn,
   CASE
     WHEN tp <> 14 AND srv_grp = 19 AND srv_code = 12 AND dt_elig BETWEEN dt_admit AND dt_discharge THEN 3
     WHEN tp <> 14 AND srv_grp = 19 AND srv_code = 12 AND dt_elig NOT BETWEEN dt_admit AND dt_discharge THEN 0
     WHEN tp <> 14 AND srv_grp IN (1,10) AND srv_code IN (1,3) THEN 0
   END val
 FROM clients
 WHERE
   rn = 1
 UNION ALL
 SELECT
   c.id_client,
   c.dt_elig,
   c.tp,
   c.srv_grp,
   c.srv_code,
   c.dt_admit,
   c.dt_discharge,
   c.rn,
   CASE
     WHEN c.tp <> 14 AND c.srv_grp = 19 AND c.srv_code = 12 AND p.val = 3 AND c.dt_elig BETWEEN c.dt_admit AND c.dt_discharge THEN 3
     WHEN c.tp <> 14 AND c.srv_grp = 19 AND c.srv_code = 12 AND p.val < 3 AND c.dt_elig BETWEEN c.dt_admit AND c.dt_discharge THEN p.val + 1
     WHEN c.tp <> 14 AND c.srv_grp IN (1,10) AND c.srv_code IN (1,3) AND p.val = 3 THEN 3
     WHEN c.tp <> 14 AND c.srv_grp IN (1,10) AND c.srv_code IN (1,3) AND p.val < 3 THEN p.val
     WHEN c.tp <> 14 AND c.srv_grp = 19 AND c.srv_code = 12 AND p.val = 3 AND c.dt_elig NOT BETWEEN c.dt_admit AND c.dt_discharge THEN 3
     WHEN c.tp <> 14 AND c.srv_grp = 19 AND c.srv_code = 12 AND p.val < 3 AND c.dt_elig NOT BETWEEN c.dt_admit AND c.dt_discharge THEN p.val
   END val
 FROM
   clients c,
   results p
 WHERE
   c.id_client = p.id_client
   AND c.rn = p.rn + 1
)
SELECT
  r.id_client,
  r.dt_elig,
  r.tp,
  r.srv_grp,
  r.srv_code,
  r.dt_admit,
  r.dt_discharge,
  --r.rn,
  r.val
FROM results r
ORDER BY
  r.id_client,
  r.dt_elig;

Como pueden ver, las expresiones de tablas comunes recursivas contienen dos consultas: La consulta anchor que sirve para generar el primer registro que luego es procesado por la consulta recursiva. Las dos consultas son combinadas mediante el operador UNION ALL.

Ahora veamos el resultado una vez que se ejecuta la consulta:


 ID_CLIENT DT_ELIG           TP    SRV_GRP   SRV_CODE DT_ADMIT  DT_DISCHA        VAL
---------- --------- ---------- ---------- ---------- --------- --------- ----------
 525459313 01-MAR-12         13          1          1 18-JAN-12 29-MAR-12          0
 525459313 01-DEC-12         13         19         12 12-DEC-12 30-APR-13          0
 525459313 01-JAN-13         13         19         12 12-DEC-12 30-APR-13          1
 525459313 01-FEB-13         13         19         12 12-DEC-12 30-APR-13          2
 525459313 01-MAR-13         13         19         12 12-DEC-12 30-APR-13          3
 525459313 01-APR-13         13         19         12 12-DEC-12 30-APR-13          3
 525459313 01-MAY-13         13         19         12 01-MAY-13 31-DEC-13          3
 525459313 01-JUN-13         13         19         12 01-MAY-13 31-DEC-13          3
 525459313 01-JUL-13         13         19         12 01-MAY-13 31-DEC-13          3
 525459313 01-AUG-13         13         19         12 01-MAY-13 31-DEC-13          3
 525459313 01-SEP-13         13         19         12 01-MAY-13 31-DEC-13          3
 525459313 01-OCT-13         13         19         12 01-MAY-13 31-DEC-13          3
 525459313 01-NOV-13         13         19         12 01-MAY-13 31-DEC-13          3
 615086545 01-MAR-13         13         19         12 07-MAR-13 31-MAR-14          0
 615086545 01-APR-13         13         19         12 07-MAR-13 31-MAR-14          1
 615086545 01-MAY-13         13         19         12 07-MAR-13 31-MAR-14          2
 615086545 01-JUN-13         13         19         12 07-MAR-13 31-MAR-14          3
 615086545 01-JUL-13         13         19         12 07-MAR-13 31-MAR-14          3
 615086545 01-AUG-13         13         19         12 07-MAR-13 31-MAR-14          3
 615086545 01-SEP-13         13         19         12 07-MAR-13 31-MAR-14          3
 615086545 01-OCT-13         13         19         12 07-MAR-13 31-MAR-14          3
 615086545 01-NOV-13         13         19         12 07-MAR-13 31-MAR-14          3

22 rows selected.

En éste ejemplo en particular, la consulta anchor asigna ya sea un 0 o un 3 para VAL en el primer registro y de ahí la consulta recursiva evalúa si VAL debería incrementarse o permanecer estático en cada uno de los siguientes registros. La idea es tener acceso a VAL de la iteración previa para poder determinar el nuevo valor en la iteración actual.


RMOUG Training Days 2014

training_days

Un pequeño post para decirles que voy a presentar el Jueves 6 de Febrero. Aquí están los detalles de mi sesión:

SQL Pattern Matching in Oracle 12c

Salón 501, 11:15 – 12:15

El año pasado fue la primera vez que asistí a la conferencia y la pasé muy bien. Ya estoy contando los días para regresar a Denver.

Ahí nos vemos!

Actualizado 02/17/2014: La presentación se encuentra disponible en Slideshare


Tratando de validar direcciones de correo electrónico usando expresiones regulares

Traducción del post original publicado en Julio 10 2013.

Después de mi presentación en Kscope13, uno de los asistentes se me acercó a preguntarme si alguna vez había tratado de validar direcciones de correo electrónico utilizando expresiones regulares. Ésta persona no había escuchado sobre expresiones regulares antes de la presentación y me contó que en su trabajo estaba tratando de crear una función PL/SQL (la cual se estaba volviendo muy compleja) que se encargara de dicha funcionalidad. Le ofrecí ver si podía lograr algo y escribir una entrada en el blog.

Después de algunos intentos y de entender que no es una tarea sencilla, me decidí por la siguiente expresión regular: ^[a-z0-9._-]+@[a-z0-9.-]+\.[a-z]{2,3}$

En éste caso, yo considero que una dirección de correo electrónico es correcta si la misma empieza con una secuencia de caracteres que puede incluir cualquier letra o dígito, el punto, el guión o el guión bajo, a continuación el símbolo @ seguido por una secuencia de caracteres que puede incluir cualquier letra o dígito, el  punto o el guión y finalizando con un punto seguido de una secuencia de caracteres que contiene entre dos y tres letras.

Está claro que una dirección de correo electrónico puede contener otros caracteres; pero decidí utilizar aquellos que se ven con mayor frecuencia. No hay que olvidar que ésta expresión regular solamente considera letras minúsculas y que podemos pasar un parámetro a las funciones regexp en SQL para indicar que deseamos validar direcciones sin importar si las letras son mayúsculas o minúsculas.

Veamos cómo funciona todo utilizando datos que se encuentran en una tabla de clientes:

SQL> select * from clients;

NAME_CLIENT               PHONE                EMAIL
------------------------- -------------------- ------------------------------
FOUNTAIN,VICKIE L                              Vickie.L.Fountain@mail.com
ORTIZ,GENOVEVA M                               123ortiz.gmail.com
MALDONADO,TERESA                               teresa-maldonado@amazon.store
BARAJAS-RAMIREZ,AMY
SERRANO,GRACE
BALDA GALO                (512) 254-1694       galo@email.mydomain.net

6 rows selected.

Se puede observar que las direcciones de correo que siguen el patrón de la expresión regular pertenecen a los registros 1 y 6 y que las direcciones de correo en los registros 2 y 3 no siguen el patrón.

Podemos utilizar la siguiente sentencia SQL para encontrar los registros que siguen el patrón definido en la expresión regular:

SQL> select *
  2 from clients
  3 where regexp_like (email,'^[a-z0-9._-]+@[a-z0-9.-]+\.[a-z]{2,3}$','i');

NAME_CLIENT               PHONE                EMAIL
------------------------- -------------------- ------------------------------
FOUNTAIN,VICKIE L                              Vickie.L.Fountain@mail.com
BALDA GALO               (512) 254-1694        galo@email.mydomain.net

2 rows selected.

Se debe notar que la primera dirección de correo es considerada correcta a pesar de que no se definieron caracteres en mayúsculas en la expresión regular. Ésto ocurre debido al uso del parámetro ‘i’ en la función, lo que le indica que no importa si los caracteres están en mayúsculas o minúsculas.

Ahora busquemos los registros que no siguen el patrón definido en la expresión regular:

SQL> select *
  2 from clients
  3 where not regexp_like (email,'^[a-z0-9._-]+@[a-z0-9.-]+\.[a-z]{2,3}$','i');

NAME_CLIENT               PHONE                EMAIL
------------------------- -------------------- ------------------------------
ORTIZ,GENOVEVA M                               123ortiz.gmail.com
MALDONADO,TERESA                               teresa-maldonado@amazon.store

2 rows selected.

En éste caso, la primera dirección de correo no sigue el patrón definido en la expresión regular ya que falta la @. La segunda dirección utiliza un dominio formado por cuatro letras pero el patrón que estamos tratando de validar lo limita a dos o tres letras.

En resumen, éstos ejemplos muestran cómo empezar a realizar validaciones de direcciones de correo electrónico usando expresiones regulares. El patrón utilizado es bien básico, no es perfecto y puede ser mejorado; pero siempre dependerá del formato (o los formatos) de direcciones de correo electrónico que se desee validar y la complejidad de su representación.

Espero que les sea de ayuda.


Algo que no sabía: La Función REVERSE

Hace unos días estaba participando en el  pl/sql challenge y la pregunta consistía en identificar implementaciones que se ejecutaran correctamente para una función llamada “reverse_string” que recibía como parámetro una cadena de caracteres tipo varchar2 y retornaba como resultado otra cadena tipo varchar2 con los caracteres en orden inverso.

Una de las posibles respuestas era la siguiente función:

FUNCTION reverse_string (in_string VARCHAR2)
RETURN VARCHAR2
AS
  l_return   VARCHAR2 (100);
BEGIN
  SELECT REVERSE (in_string) INTO l_return FROM DUAL;

  RETURN l_return;
END reverse_string;

Al ver ese bloque de código, estaba muy seguro de que esa no podía ser una de las respuestas porque nunca había visto ninguna referencia sobre REVERSE en libros, manuales o documentación; pero por algún motivo me entró la desconfianza de que algo no estaba bien, así que me decidí a probar el código.

Abrí un sesión en SQL*PLUS y ejecuté el siguiente query:

SELECT reverse('hello world')
FROM dual;

Para mi sorpresa el query funcionó y mostró la cadena de caracteres en orden inverso: dlrow olleh.

Buscando en Internet encontré que REVERSE es una función que no está documentada y por lo tanto no tiene soporte oficial por parte de Oracle y que ha estado ahí por algún tiempo. Encontré referencias en blogs posts de hace más de siete años atrás, así que parece que he estado viviendo bajo una piedra durante todo este tiempo o a lo mejor nunca tuve un requerimiento para implementar este tipo de funcionalidad que me hubiera hecho encontrarla antes.

Para finalizar, no estoy sugiriendo que usen REVERSE en el código de sus aplicaciones, ya que como mencioné antes es una función que no tiene soporte oficial de Oracle y podría desaparecer en cualquier momento o su comportamiento podría ser modificado sin previo aviso. Otro inconveniente es que la función retorna resultados erróneos cuando se usa con caracteres que necesitan ser representados con múltiples bytes.