sql - transponer - División de cadena en múltiples filas en Oracle
pivot oracle columns to rows (14)
A partir de Oracle 12c, puede usar JSON_TABLE
y JSON_ARRAY
:
CREATE TABLE tab(Name, Project, Error) AS
SELECT 108,''test'' ,''Err1, Err2, Err3'' FROM dual UNION
SELECT 109,''test2'',''Err1'' FROM dual;
Y consulta:
SELECT *
FROM tab t
OUTER APPLY (SELECT TRIM(p) AS p
FROM JSON_TABLE(REPLACE(JSON_ARRAY(t.Error), '','', ''","''),
''$[*]'' COLUMNS (p VARCHAR2(4000) PATH ''$''))) s;
Salida:
┌──────┬─────────┬──────────────────┬──────┐
│ Name │ Project │ Error │ P │
├──────┼─────────┼──────────────────┼──────┤
│ 108 │ test │ Err1, Err2, Err3 │ Err1 │
│ 108 │ test │ Err1, Err2, Err3 │ Err2 │
│ 108 │ test │ Err1, Err2, Err3 │ Err3 │
│ 109 │ test2 │ Err1 │ Err1 │
└──────┴─────────┴──────────────────┴──────┘
Sé que esto ha sido respondido hasta cierto punto con PHP y MYSQL, pero me preguntaba si alguien podría enseñarme el enfoque más simple para dividir una cadena (delimitada por comas) en múltiples filas en Oracle 10g (preferiblemente) y 11g.
La tabla es la siguiente:
Name | Project | Error
108 test Err1, Err2, Err3
109 test2 Err1
Quiero crear lo siguiente:
Name | Project | Error
108 Test Err1
108 Test Err2
108 Test Err3
109 Test2 Err1
He visto algunas soluciones potenciales alrededor de la pila, sin embargo, solo representaban una sola columna (siendo la cadena delimitada por comas). Cualquier ayuda sería muy apreciada.
Aquí hay una implementación alternativa que utiliza XMLTABLE que permite enviar datos a diferentes tipos de datos:
select
xmltab.txt
from xmltable(
''for $text in tokenize("a,b,c", ",") return $text''
columns
txt varchar2(4000) path ''.''
) xmltab
;
... o si tus cadenas delimitadas están almacenadas en una o más filas de una tabla:
select
xmltab.txt
from (
select ''a;b;c'' inpt from dual union all
select ''d;e;f'' from dual
) base
inner join xmltable(
''for $text in tokenize($input, ";") return $text''
passing base.inpt as "input"
columns
txt varchar2(4000) path ''.''
) xmltab
on 1=1
;
Creo que la mejor manera de conectarme y la función de expresión regular
with temp as (
select 108 Name, ''test'' Project, ''Err1, Err2, Err3'' Error from dual
union all
select 109, ''test2'', ''Err1'' from dual
)
SELECT distinct Name, Project, trim(regexp_substr(str, ''[^,]+'', 1, level)) str
FROM (SELECT Name, Project, Error str FROM temp) t
CONNECT BY instr(str, '','', 1, level - 1) > 0
order by Name
Hay una gran diferencia entre los dos siguientes:
- dividiendo una sola cadena delimitada
- dividir cadenas delimitadas para múltiples filas en una tabla.
Si no restringe las filas, entonces la cláusula CONNECT BY produciría múltiples filas y no dará el resultado deseado.
- Para una cadena delimitada individual, mire la cadena delimitada por comas simples en filas
- Para dividir cadenas delimitadas en una tabla, observe las cadenas delimitadas por comas en una tabla
Además de las expresiones regulares , algunas otras alternativas están utilizando:
- XMLTable
- Cláusula MODELO
Preparar
SQL> CREATE TABLE t (
2 ID NUMBER GENERATED ALWAYS AS IDENTITY,
3 text VARCHAR2(100)
4 );
Table created.
SQL>
SQL> INSERT INTO t (text) VALUES (''word1, word2, word3'');
1 row created.
SQL> INSERT INTO t (text) VALUES (''word4, word5, word6'');
1 row created.
SQL> INSERT INTO t (text) VALUES (''word7, word8, word9'');
1 row created.
SQL> COMMIT;
Commit complete.
SQL>
SQL> SELECT * FROM t;
ID TEXT
---------- ----------------------------------------------
1 word1, word2, word3
2 word4, word5, word6
3 word7, word8, word9
SQL>
Usando XMLTABLE :
SQL> SELECT id,
2 trim(COLUMN_VALUE) text
3 FROM t,
4 xmltable((''"''
5 || REPLACE(text, '','', ''","'')
6 || ''"''))
7 /
ID TEXT
---------- ------------------------
1 word1
1 word2
1 word3
2 word4
2 word5
2 word6
3 word7
3 word8
3 word9
9 rows selected.
SQL>
Usando la cláusula MODELO :
SQL> WITH
2 model_param AS
3 (
4 SELECT id,
5 text AS orig_str ,
6 '',''
7 || text
8 || '','' AS mod_str ,
9 1 AS start_pos ,
10 Length(text) AS end_pos ,
11 (Length(text) - Length(Replace(text, '',''))) + 1 AS element_count ,
12 0 AS element_no ,
13 ROWNUM AS rn
14 FROM t )
15 SELECT id,
16 trim(Substr(mod_str, start_pos, end_pos-start_pos)) text
17 FROM (
18 SELECT *
19 FROM model_param MODEL PARTITION BY (id, rn, orig_str, mod_str)
20 DIMENSION BY (element_no)
21 MEASURES (start_pos, end_pos, element_count)
22 RULES ITERATE (2000)
23 UNTIL (ITERATION_NUMBER+1 = element_count[0])
24 ( start_pos[ITERATION_NUMBER+1] = instr(cv(mod_str), '','', 1, cv(element_no)) + 1,
25 end_pos[iteration_number+1] = instr(cv(mod_str), '','', 1, cv(element_no) + 1) )
26 )
27 WHERE element_no != 0
28 ORDER BY mod_str ,
29 element_no
30 /
ID TEXT
---------- --------------------------------------------------
1 word1
1 word2
1 word3
2 word4
2 word5
2 word6
3 word7
3 word8
3 word9
9 rows selected.
SQL>
La respuesta aceptada tiene un rendimiento deficiente cuando se usan grandes conjuntos de datos.
Esta puede ser una forma mejorada (también con regexp y connect by):
with temp as
(
select 108 Name, ''test'' Project, ''Err1, Err2, Err3'' Error from dual
union all
select 109, ''test2'', ''Err1'' from dual
)
select distinct
t.name, t.project,
trim(regexp_substr(t.error, ''[^,]+'', 1, levels.column_value)) as error
from
temp t,
table(cast(multiset(select level from dual connect by level <= length (regexp_replace(t.error, ''[^,]+'')) + 1) as sys.OdciNumberList)) levels
order by name
EDITAR : Aquí hay una explicación simple (como en, "no en profundidad") de la consulta.
-
length (regexp_replace(t.error, ''[^,]+'')) + 1
usaregexp_replace
para borrar cualquier cosa que no sea el delimitador (coma en este caso) y lalength +1
para obtener cuántos elementos (errores) hay. El
select level from dual connect by level <= (...)
usa una consulta jerárquica para crear una columna con un número creciente de coincidencias encontradas, desde 1 hasta el número total de errores.Avance:
select level, length (regexp_replace(''Err1, Err2, Err3'', ''[^,]+'')) + 1 as max from dual connect by level <= length (regexp_replace(''Err1, Err2, Err3'', ''[^,]+'')) + 1
-
table(cast(multiset(.....) as sys.OdciNumberList))
hace algo de fundición de tipos de oráculo.- El
cast(multiset(.....)) as sys.OdciNumberList
transforma múltiples colecciones (una colección para cada fila en el conjunto de datos original) en una única colección de números, OdciNumberList. - La función
table()
transforma una colección en un conjunto de resultados.
- El
FROM
sin una unión crea una combinación cruzada entre su conjunto de datos y el conjunto múltiple. Como resultado, una fila en el conjunto de datos con 4 coincidencias se repetirá 4 veces (con un número creciente en la columna denominada "column_value").Avance:
select * from temp t, table(cast(multiset(select level from dual connect by level <= length (regexp_replace(t.error, ''[^,]+'')) + 1) as sys.OdciNumberList)) levels
-
trim(regexp_substr(t.error, ''[^,]+'', 1, levels.column_value))
utiliza elcolumn_value
como el parámetro nth_appearance / ocurrence pararegexp_substr
. - Puede agregar algunas otras columnas de su conjunto de datos (
t.name, t.project
como ejemplo) para una fácil visualización.
Algunas referencias a documentos de Oracle:
Me gustaría agregar otro método. Este usa consultas recursivas, algo que no he visto en las otras respuestas. Es compatible con Oracle desde 11gR2.
with cte0 as (
select phone_number x
from hr.employees
), cte1(xstr,xrest,xremoved) as (
select x, x, null
from cte0
union all
select xstr,
case when instr(xrest,''.'') = 0 then null else substr(xrest,instr(xrest,''.'')+1) end,
case when instr(xrest,''.'') = 0 then xrest else substr(xrest,1,instr(xrest,''.'') - 1) end
from cte1
where xrest is not null
)
select xstr, xremoved from cte1
where xremoved is not null
order by xstr
Es bastante flexible con el carácter de división. Simplemente INSTR
en las llamadas INSTR
.
Me gustaría proponer un enfoque diferente utilizando una función de tabla PIPELINED. Es algo similar a la técnica de XMLTABLE, excepto que está proporcionando su propia función personalizada para dividir la cadena de caracteres:
-- Create a collection type to hold the results
CREATE OR REPLACE TYPE typ_str2tbl_nst AS TABLE OF VARCHAR2(30);
/
-- Split the string according to the specified delimiter
CREATE OR REPLACE FUNCTION str2tbl (
p_string VARCHAR2,
p_delimiter CHAR DEFAULT '',''
)
RETURN typ_str2tbl_nst PIPELINED
AS
l_tmp VARCHAR2(32000) := p_string || p_delimiter;
l_pos NUMBER;
BEGIN
LOOP
l_pos := INSTR( l_tmp, p_delimiter );
EXIT WHEN NVL( l_pos, 0 ) = 0;
PIPE ROW ( RTRIM( LTRIM( SUBSTR( l_tmp, 1, l_pos-1) ) ) );
l_tmp := SUBSTR( l_tmp, l_pos+1 );
END LOOP;
END str2tbl;
/
-- The problem solution
SELECT name,
project,
TRIM(COLUMN_VALUE) error
FROM t, TABLE(str2tbl(error));
Resultados:
NAME PROJECT ERROR
---------- ---------- --------------------
108 test Err1
108 test Err2
108 test Err3
109 test2 Err1
El problema con este tipo de enfoque es que a menudo el optimizador no conocerá la cardinalidad de la función de tabla y tendrá que adivinar. Esto podría ser potencialmente dañino para sus planes de ejecución, por lo que esta solución se puede ampliar para proporcionar estadísticas de ejecución para el optimizador.
Puede ver esta estimación del optimizador ejecutando un PLAN EXPLICACIÓN en la consulta anterior:
Execution Plan
----------------------------------------------------------
Plan hash value: 2402555806
----------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 16336 | 366K| 59 (0)| 00:00:01 |
| 1 | NESTED LOOPS | | 16336 | 366K| 59 (0)| 00:00:01 |
| 2 | TABLE ACCESS FULL | T | 2 | 42 | 3 (0)| 00:00:01 |
| 3 | COLLECTION ITERATOR PICKLER FETCH| STR2TBL | 8168 | 16336 | 28 (0)| 00:00:01 |
----------------------------------------------------------------------------------------------
Aunque la colección solo tiene 3 valores, el optimizador estimó 8168 filas para ella (valor predeterminado). Esto puede parecer irrelevante al principio, pero puede ser suficiente para que el optimizador decida por un plan subóptimo.
La solución es usar las extensiones del optimizador para proporcionar estadísticas para la colección:
-- Create the optimizer interface to the str2tbl function
CREATE OR REPLACE TYPE typ_str2tbl_stats AS OBJECT (
dummy NUMBER,
STATIC FUNCTION ODCIGetInterfaces ( p_interfaces OUT SYS.ODCIObjectList )
RETURN NUMBER,
STATIC FUNCTION ODCIStatsTableFunction ( p_function IN SYS.ODCIFuncInfo,
p_stats OUT SYS.ODCITabFuncStats,
p_args IN SYS.ODCIArgDescList,
p_string IN VARCHAR2,
p_delimiter IN CHAR DEFAULT '','' )
RETURN NUMBER
);
/
-- Optimizer interface implementation
CREATE OR REPLACE TYPE BODY typ_str2tbl_stats
AS
STATIC FUNCTION ODCIGetInterfaces ( p_interfaces OUT SYS.ODCIObjectList )
RETURN NUMBER
AS
BEGIN
p_interfaces := SYS.ODCIObjectList ( SYS.ODCIObject (''SYS'', ''ODCISTATS2'') );
RETURN ODCIConst.SUCCESS;
END ODCIGetInterfaces;
-- This function is responsible for returning the cardinality estimate
STATIC FUNCTION ODCIStatsTableFunction ( p_function IN SYS.ODCIFuncInfo,
p_stats OUT SYS.ODCITabFuncStats,
p_args IN SYS.ODCIArgDescList,
p_string IN VARCHAR2,
p_delimiter IN CHAR DEFAULT '','' )
RETURN NUMBER
AS
BEGIN
-- I''m using basically half the string lenght as an estimator for its cardinality
p_stats := SYS.ODCITabFuncStats( CEIL( LENGTH( p_string ) / 2 ) );
RETURN ODCIConst.SUCCESS;
END ODCIStatsTableFunction;
END;
/
-- Associate our optimizer extension with the PIPELINED function
ASSOCIATE STATISTICS WITH FUNCTIONS str2tbl USING typ_str2tbl_stats;
Probando el plan de ejecución resultante:
Execution Plan
----------------------------------------------------------
Plan hash value: 2402555806
----------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 23 | 59 (0)| 00:00:01 |
| 1 | NESTED LOOPS | | 1 | 23 | 59 (0)| 00:00:01 |
| 2 | TABLE ACCESS FULL | T | 2 | 42 | 3 (0)| 00:00:01 |
| 3 | COLLECTION ITERATOR PICKLER FETCH| STR2TBL | 1 | 2 | 28 (0)| 00:00:01 |
----------------------------------------------------------------------------------------------
Como puede ver, la cardinalidad en el plan de arriba ya no es el valor calculado 8196. Todavía no es correcto porque estamos pasando una columna en lugar de un literal de cadena a la función.
Sería necesario hacer algunos ajustes al código de la función para dar una estimación más cercana en este caso particular, pero creo que el concepto general se explica bastante aquí.
La función str2tbl utilizada en esta respuesta fue desarrollada originalmente por Tom Kyte: https://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:110612348061
El concepto de asociar estadísticas con tipos de objetos se puede explorar más a fondo leyendo este artículo: http://www.oracle-developer.net/display.php?id=427
La técnica descrita aquí funciona en 10g +.
REGEXP_COUNT no se agregó hasta Oracle 11i. Aquí hay una solución de Oracle 10g, adoptada de la solución de Art.
SELECT trim(regexp_substr(''Err1, Err2, Err3'', ''[^,]+'', 1, LEVEL)) str_2_tab
FROM dual
CONNECT BY LEVEL <=
LENGTH(''Err1, Err2, Err3'')
- LENGTH(REPLACE(''Err1, Err2, Err3'', '','', ''''))
+ 1;
Sin usar connect by o regexp :
with mytable as (
select 108 name, ''test'' project, ''Err1,Err2,Err3'' error from dual
union all
select 109, ''test2'', ''Err1'' from dual
)
,x as (
select name
,project
,'',''||error||'','' error
from mytable
)
,iter as (SELECT rownum AS pos
FROM all_objects
)
select x.name,x.project
,SUBSTR(x.error
,INSTR(x.error, '','', 1, iter.pos) + 1
,INSTR(x.error, '','', 1, iter.pos + 1)-INSTR(x.error, '','', 1, iter.pos)-1
) error
from x, iter
where iter.pos < = (LENGTH(x.error) - LENGTH(REPLACE(x.error, '',''))) - 1;
Tuve el mismo problema, y xmltable me ayudó:
SELECT id, trim(COLUMN_VALUE) text
FROM t, xmltable((''"'' || REPLACE(text, '','', ''","'') || ''"''))
Un par de más ejemplos de lo mismo:
SELECT trim(regexp_substr(''Err1, Err2, Err3'', ''[^,]+'', 1, LEVEL)) str_2_tab
FROM dual
CONNECT BY LEVEL <= regexp_count(''Err1, Err2, Err3'', '','')+1
/
SELECT trim(regexp_substr(''Err1, Err2, Err3'', ''[^,]+'', 1, LEVEL)) str_2_tab
FROM dual
CONNECT BY LEVEL <= length(''Err1, Err2, Err3'') - length(REPLACE(''Err1, Err2, Err3'', '','', ''''))+1
/
Además, puede usar DBMS_UTILITY.comma_to_table & table_to_comma: http://www.oracle-base.com/articles/9i/useful-procedures-and-functions-9i.php#DBMS_UTILITY.comma_to_table
Yo había usado la función DBMS_UTILITY.comma_to _table en realidad está trabajando el código de la siguiente manera
declare
l_tablen BINARY_INTEGER;
l_tab DBMS_UTILITY.uncl_array;
cursor cur is select * from qwer;
rec cur%rowtype;
begin
open cur;
loop
fetch cur into rec;
exit when cur%notfound;
DBMS_UTILITY.comma_to_table (
list => rec.val,
tablen => l_tablen,
tab => l_tab);
FOR i IN 1 .. l_tablen LOOP
DBMS_OUTPUT.put_line(i || '' : '' || l_tab(i));
END LOOP;
end loop;
close cur;
end;
había usado mis propios nombres de tabla y columna
expresiones regulares es una cosa maravillosa :)
with temp as (
select 108 Name, ''test'' Project, ''Err1, Err2, Err3'' Error from dual
union all
select 109, ''test2'', ''Err1'' from dual
)
SELECT distinct Name, Project, trim(regexp_substr(str, ''[^,]+'', 1, level)) str
FROM (SELECT Name, Project, Error str FROM temp) t
CONNECT BY instr(str, '','', 1, level - 1) > 0
order by Name
CREATE FUNCTION dbo.BreakStringIntoRows (@CommadelimitedString varchar(1000))
RETURNS @Result TABLE (Column1 VARCHAR(100))
AS
BEGIN
DECLARE @IntLocation INT
WHILE (CHARINDEX('','', @CommadelimitedString, 0) > 0)
BEGIN
SET @IntLocation = CHARINDEX('','', @CommadelimitedString, 0)
INSERT INTO @Result (Column1)
--LTRIM and RTRIM to ensure blank spaces are removed
SELECT RTRIM(LTRIM(SUBSTRING(@CommadelimitedString, 0, @IntLocation)))
SET @CommadelimitedString = STUFF(@CommadelimitedString, 1, @IntLocation, '''')
END
INSERT INTO @Result (Column1)
SELECT RTRIM(LTRIM(@CommadelimitedString))--LTRIM and RTRIM to ensure blank spaces are removed
RETURN
END
GO
--Using the UDF to convert comma separated values into rows
SELECT * FROM dbo.BreakStringIntoRows(''Apple,Banana,Orange'')
SELECT * FROM dbo.BreakStringIntoRows(''Apple , Banana, Orange'')