Archivo de la etiqueta: connect by level

Extraer valores de una lista contenida en una columna

En ciertas ocasiones nos vemos en la necesidad de almacenar una serie de valores distintos en una misma columna. Especialmente cuando estamos tratando con datos de configuración que pueden variar en el número y valores posibles con el tiempo. Explotar esta información desde SQL suele presentar muchos inconvenientes puesto que se trata de información sin normalizar y que requiere tratar con cadenas de texto de alta variabilidad.

Gracias a la implantación de expresiones regulares dentro de SQL podemos mejorar las alternativas a la hora de consultar este tipo de datos.

Tomemos, a modo de prueba, las siguientes cadenas de texto:

TYPE=String#LENGTH=18#DEFAULT=empty
LENGTH=1#TYPE=Number#DECIMALS=0#DEFAULT=0
LENGTH=10#DEFAULT=1.00#TYPE=Number#DECIMALS=2

Mediante el uso de las funciones “regexp_instr” y “regexp_substr” dentro de una consulta SELECT podemos extraer los distintos componentes de cada una de las cadenas y, por ejemplo, mostrarlos como columnas:

with test_tab as (
    select 1 cfg, 'TYPE=String#LENGTH=18#DEFAULT=empty' config from dual
    union all
    select 2 cfg, 'LENGTH=1#TYPE=Number#DECIMALS=0#DEFAULT=0' config from dual
    union all
    select 3 cfg, 'LENGTH=10#DEFAULT=1.00#TYPE=Number#DECIMALS=2' config from dual)
select cfg,
       decode((regexp_instr(config, '(^|#)TYPE=(.+)($|#)')),     0, '', (regexp_substr(config, '[^#]+', (regexp_instr(config, '(^|#)TYPE=(.+)($|#)')),     1))) cfg_TYPE,
       decode((regexp_instr(config, '(^|#)LENGTH=(.+)($|#)')),   0, '', (regexp_substr(config, '[^#]+', (regexp_instr(config, '(^|#)LENGTH=(.+)($|#)')),   1))) cfg_LENGTH,
       decode((regexp_instr(config, '(^|#)DECIMALS=(.+)($|#)')), 0, '', (regexp_substr(config, '[^#]+', (regexp_instr(config, '(^|#)DECIMALS=(.+)($|#)')), 1))) cfg_DECIMALS,
       decode((regexp_instr(config, '(^|#)DEFAULT=(.+)($|#)')),  0, '', (regexp_substr(config, '[^#]+', (regexp_instr(config, '(^|#)DEFAULT=(.+)($|#)')),  1))) cfg_DEFAULT,
       config
from test_tab;

Resultado:

CFG	CFG_TYPE	CFG_LENGTH	CFG_DECIMALS	CFG_DEFAULT	CONFIG
---	-----------	----------	-------------	-----------	----------------------------------------
1	TYPE=String	LENGTH=18	DEFAULT=empty			TYPE=String#LENGTH=18#DEFAULT=empty
2	TYPE=Number	LENGTH=1	DECIMALS=0	DEFAULT=0	LENGTH=1#TYPE=Number#DECIMALS=0#DEFAULT=0
3	TYPE=Number	LENGTH=10	DECIMALS=2	DEFAULT=1.00	LENGTH=10#DEFAULT=1.00#TYPE=Number#DECIMALS=2

Estas mismas funciones nos pueden servir para filtrar los datos en la cláusula WHERE o para modificar los datos de la cadena a través del operador SET en una operación UPDATE.

Por último, mediante la cláusula CONNECT BY, podemos listar todos los datos de una de las listas y mostrarlos como filas distintas en el resultado de la consulta:

with test_tab as (select 1 cfg, 'TYPE=String#LENGTH=18#DEFAULT=empty' config from dual)
select cfg, config, regexp_substr(config, '[^#]+', 1, level) key_value
from test_tab
connect by regexp_substr(config, '[^#]+', 1, level) is not null;

Resultado:

CFG	CONFIG					KEY_VALUE
---	-----------------------------------	-------------
1	TYPE=String#LENGTH=18#DEFAULT=empty	TYPE=String
1	TYPE=String#LENGTH=18#DEFAULT=empty	LENGTH=18
1	TYPE=String#LENGTH=18#DEFAULT=empty	DEFAULT=empty

Select sobre colecciones PL/SQL correlacionadas

Oracle permite acceder a los valores de colecciones PL/SQL desde sentencias SQL. Esta propiedad del comando SELECT se llama table_collection_expression y muestra el contenido de una colección como si de una tabla se tratara.

Para que el proceso funcione correctamente es necesario que la colección esté declarada en el esquema, no dentro de un paquete:

-- Create PL/SQL collection
create or replace type ty_number is table of number;

-- Select from one PL/SQL collection
select * from table(ty_number(10,20,30,50,40));

La sentencia daría como resultado:

COLUMN_VALUE
------------
10
20
30
50
40

Esta propiedad puede resultarnos muy útil cuando estamos programando en PL/SQL y, por ejemplo, necesitamos extraer datos de una consulta a través de un proceso bulk collection y compararlos con registros de otras tablas.

Pero, ¿qué ocurre si necesitamos cruzar datos de dos colecciones PL/SQL y datos de tablas? ¿Cómo vamos a correlacionar los elementos de varias colecciones entre sí? En estos casos podemos recurrir a una sentencia join en combinación con la cláusula connect by level para generar los índices que nos permitirán recorrer las colecciones:

-- Select from two correlated PL/SQL collections
select coll_index, coll_1_value, coll_2_value
from (select level coll_index from dual connect by level <= 5) gen_idx -- Generate index for iterate collections
     join (select rownum rn, column_value coll_1_value from table(ty_number(10,20,30,50,40))) coll_1 on gen_idx.coll_index = coll_1.rn -- Collection 1
     join (select rownum rn, column_value coll_2_value from table(ty_number(11,21,31,51,41))) coll_2 on gen_idx.coll_index = coll_2.rn; -- Collection 2

La ejecución de la sentencia produce el siguiente resultado:

COLL_INDEX	COLL_1_VALUE	COLL_2_VALUE
----------	------------	------------
1		10		11
2		20		21
3		30		31
4		50		51
5		40		41

Como puede apreciarse al fijarnos en las filas 4 y 5, los elementos de las colecciones mantienen el orden original y la correlación entre sus elementos.