División de cadena en varias filas en Oracle

104

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 varias 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 representaron una sola columna (que es la cadena delimitada por comas). Cualquier ayuda será muy apreciada.

Ley marcial
fuente
2
Para ver ejemplos de uso de la cláusula REGEXP, XMLTABLEy MODEL, consulte Dividir cadenas delimitadas por comas en una tabla con Oracle SQL
Lalit Kumar B

Respuestas:

121

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.

  1. length (regexp_replace(t.error, '[^,]+')) + 1usa regexp_replacepara borrar cualquier cosa que no sea el delimitador (coma en este caso) y length +1para obtener cuántos elementos (errores) hay.
  2. El select level from dual connect by level <= (...)utiliza un consulta jerárquica para crear una columna con un número creciente de partidos encontró, 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
    
  3. table(cast(multiset(.....) as sys.OdciNumberList)) hace algunos tipos de oráculo.
    • Los cast(multiset(.....)) as sys.OdciNumberListtransforma múltiples colecciones (uno de recogida para cada fila en el conjunto de datos original) en una sola colección de números, OdciNumberList.
    • La table()función transforma una colección en un conjunto de resultados.
  4. FROMsin una combinació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
    
  5. trim(regexp_substr(t.error, '[^,]+', 1, levels.column_value))usa column_valuecomo parámetro nth_appearance / ocurrence para regexp_substr.
  6. Puede agregar algunas otras columnas de su conjunto de datos ( t.name, t.projectcomo ejemplo) para una fácil visualización.

Algunas referencias a documentos de Oracle:

Nefreo
fuente
7
¡Tener cuidado! Una expresión regular del formato '[^,]+'para analizar cadenas no devuelve el elemento correcto si hay un elemento nulo en la lista. Consulte aquí para obtener más información: stackoverflow.com/questions/31464275/…
Gary_W
13
desde 11g se puede usar en regexp_count(t.error, ',')lugar de length (regexp_replace(t.error, '[^,]+')), lo que puede traer otra mejora de rendimiento
Štefan Oravec
1
485 segundos con CONNECT BY "normal". 0.296 segundos de esta manera. ¡Tú Molas! Ahora todo lo que tengo que hacer es entender cómo funciona. :-)
Bob Jarvis - Reincorpora a Monica
@BobJarvis agregó una edición para explicar lo que hace. Se aceptan correcciones de ortografía / gramática.
Nefreo
"La respuesta aceptada tiene un rendimiento deficiente": ¿cuál es la respuesta aceptada en este tema? Utilice los enlaces para hacer referencia a la otra publicación.
0xdb
28

las expresiones regulares son algo maravilloso :)

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
Andrey Khmelev
fuente
1
hola, ¿puede aclararme por qué la consulta anterior da filas duplicadas si no utilicé una palabra clave distinta en la consulta?
Jagadeesh G
2
Esa consulta no se puede utilizar debido a @JagadeeshG, especialmente en tablas enormes.
Michael-O
3
Extremadamente lento, hay una mejor respuesta a continuación
MoreCoffee
La razón de la lentitud es que todas las combinaciones de Names están conectadas, lo que se puede ver si elimina distinct. Desafortunadamente, agregar and Name = prior Namea la connect bycláusula causa ORA-01436: CONNECT BY loop in user data.
mik
Puede evitar el ORA-01436error agregando AND name = PRIOR name(o cualquiera que sea la clave principal) y AND PRIOR SYS_GUID() IS NOT NULL
David Faber
28

Hay una gran diferencia entre los dos siguientes:

  • dividir una sola cadena delimitada
  • dividir cadenas delimitadas para varias filas en una tabla.

Si no restringe las filas, la cláusula CONNECT BY producirá varias filas y no dará el resultado deseado.

Además de las expresiones regulares , se utilizan algunas otras alternativas:

  • 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 MODEL :

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>
Lalit Kumar B
fuente
1
¿Puede dar más detalles, por qué tiene que ('"' || REPLACE(text, ',', '","') || '"')haberlos y no se pueden quitar los corchetes? Los documentos de Oracle ([ docs.oracle.com/database/121/SQLRF/functions268.htm ) no me quedan claros. ¿Es XQuery_string?
Betlista
@Betlista es una expresión de XQuery.
Lalit Kumar B
La solución XMLTABLE por alguna razón falla constantemente al generar la última entrada para filas de longitud mixta. P.ej. fila1: 3 palabras; fila2: 2 palabras, fila3: 1 palabra; fila4: 2 palabras, fila5: 1 palabra: no generará la última palabra. El orden de las filas no importa.
Gnudiff
7

Un par de ejemplos más 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

Arte
fuente
Tenga en cuenta que comma_to_table()solo funciona con tokens que se ajustan a las convenciones de nomenclatura de objetos de base de datos de Oracle. Lanzará una cuerda como, '123,456,789'por ejemplo.
APC
7

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 perjudicial 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 EXPLICAR PLAN 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 tiene solo 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 un plan subóptimo.

La solución es utilizar 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 anterior ya no es el valor estimado de 8196. Todavía no es correcto porque estamos pasando una columna en lugar de una cadena literal a la función.

Sería necesario hacer algunos ajustes en el código de 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 +.

Daniela Petruzalek
fuente
4

REGEXP_COUNT no se agregó hasta Oracle 11i. Aquí hay una solución 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;
durette
fuente
¿Cómo puedo agregar un filtro para esto? Digamos que quiero filtrar solo con name = '108'. Intenté agregar un where después de la cláusula from pero terminé con duplicados.
DRTauli
4

A partir de Oracle 12c, puede usar JSON_TABLEy 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 
└──────┴─────────┴──────────────────┴──────┘

db <> demostración de violín

Lukasz Szozda
fuente
1
Admito que este es un truco inteligente pero, francamente, me desconcertaría si lo encontrara en una base de código.
APC
@APC Esto es solo una muestra de lo que es posible con SQL. Si tengo que usar dicho código en mi base de código, definitivamente lo incluiría en una función o dejaría un comentario extendido :)
Lukasz Szozda
Por supuesto. Es solo que este hilo es uno de los éxitos más populares para la tokenización de cadenas con Oracle, por lo que creo que deberíamos incluir advertencias en las soluciones más exóticas, para proteger a los inocentes de sí mismos :)
APC
3

Aquí hay una implementación alternativa que usa XMLTABLE que permite la conversión 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 sus cadenas delimitadas se almacenan 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
;
Silentsurfer
fuente
Creo que esta solución funciona para Oracle 11.2.0.3 y versiones posteriores.
APC
2

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 cámbielo en las INSTRllamadas.

Thomas Tschernich
fuente
2

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;
Ilya Kharlamov
fuente
1

Tuve el mismo problema y xmltable me ayudó:

SELECT id, recortar (COLUMN_VALUE) texto de t, xmltable (('"' || REPLACE (text, ',', '", "') || '"'))

Volkov Maxim
fuente
0

En Oracle 11g y versiones posteriores, puede usar una subconsulta recursiva y funciones de cadena simples (que pueden ser más rápidas que las expresiones regulares y las subconsultas jerárquicas correlacionadas):

Configuración de Oracle :

CREATE TABLE table_name ( name, project, error ) as
 select 108, 'test',  'Err1, Err2, Err3' from dual union all
 select 109, 'test2', 'Err1'             from dual;

Consulta :

WITH table_name_error_bounds ( name, project, error, start_pos, end_pos ) AS (
  SELECT name,
         project,
         error,
         1,
         INSTR( error, ', ', 1 )
  FROM   table_name
UNION ALL
  SELECT name,
         project,
         error,
         end_pos + 2,
         INSTR( error, ', ', end_pos + 2 )
  FROM   table_name_error_bounds
  WHERE  end_pos > 0
)
SELECT name,
       project,
       CASE end_pos
       WHEN 0
       THEN SUBSTR( error, start_pos )
       ELSE SUBSTR( error, start_pos, end_pos - start_pos )
       END AS error
FROM   table_name_error_bounds

Salida :

NOMBRE | PROYECTO | ERROR
---: | : ------ | : ----
 108 | prueba | Err1
 109 | test2 | Err1
 108 | prueba | Err2
 108 | prueba | Err3

db <> violín aquí

MT0
fuente
-1

había usado la función DBMS_UTILITY.comma_to _table en realidad está funcionando 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 mi propia tabla y nombres de columna

Smart003
fuente
5
Tenga en cuenta que comma_to_table()solo funciona con tokens que se ajustan a las convenciones de nomenclatura de objetos de base de datos de Oracle. Lanzará una cuerda como, '123,456,789'por ejemplo.
APC
¿Podemos implementar usando tablas temporales?
Smart003
1
Umm, dadas todas las otras soluciones viables, ¿por qué querríamos usar tablas temporales que vienen con una sobrecarga masiva de materializar los datos?
APC