Divisão de string em várias linhas no Oracle

104

Eu sei que isso foi respondido em algum grau com PHP e MYSQL, mas eu queria saber se alguém poderia me ensinar a abordagem mais simples para dividir uma string (delimitada por vírgulas) em várias linhas no Oracle 10g (de preferência) e 11g.

A tabela é a seguinte:

Name | Project | Error 
108    test      Err1, Err2, Err3
109    test2     Err1

Eu quero criar o seguinte:

Name | Project | Error
108    Test      Err1
108    Test      Err2 
108    Test      Err3 
109    Test2     Err1

Eu vi algumas soluções potenciais em torno da pilha, no entanto, elas representavam apenas uma única coluna (sendo a string delimitada por vírgulas). Qualquer ajuda seria muito apreciada.

marshalllaw
fonte
2
Para exemplos que usam REGEXP, XMLTABLEe MODELcláusula, consulte Dividir vírgula strings delimitadas em uma tabela usando o Oracle SQL
Lalit Kumar B

Respostas:

121

Esta pode ser uma maneira melhorada (também com regexp e conectar por):

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

EDIT : Aqui está uma explicação simples (como em "não em profundidade") da consulta.

  1. length (regexp_replace(t.error, '[^,]+')) + 1usa regexp_replacepara apagar qualquer coisa que não seja o delimitador (vírgula neste caso) e length +1para obter quantos elementos (erros) existem.
  2. O select level from dual connect by level <= (...)usa uma consulta hierárquica para criar uma coluna com um número crescente de correspondências encontradas, de 1 ao número total de erros.

    Antevisão:

    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)) faz alguma conversão de tipos de oráculo.
    • O cast(multiset(.....)) as sys.OdciNumberListtransforma várias coleções (uma coleção para cada linha no conjunto de dados original) em uma única coleção de números, OdciNumberList.
    • A table()função transforma uma coleção em um conjunto de resultados.
  4. FROMsem uma junção cria uma junção cruzada entre seu conjunto de dados e o multiset. Como resultado, uma linha no conjunto de dados com 4 correspondências se repetirá 4 vezes (com um número crescente na coluna chamada "valor_coluna").

    Antevisão:

    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 o column_valuecomo o parâmetro nth_appearance / ocurrence para regexp_substr.
  6. Você pode adicionar algumas outras colunas de seu conjunto de dados ( t.name, t.projectcomo um exemplo) para facilitar a visualização.

Algumas referências aos documentos da Oracle:

Nefreo
fonte
7
Cuidado! Um regex do formato '[^,]+'para analisar strings não retorna o item correto se houver um elemento nulo na lista. Veja aqui para mais informações: stackoverflow.com/questions/31464275/…
Gary_W
13
desde 11g você pode usar em regexp_count(t.error, ',')vez de length (regexp_replace(t.error, '[^,]+')), o que pode trazer outra melhoria de desempenho
Štefan Oravec
1
485 segundos com CONNECT BY "normal". 0,296 segundos desta forma. Você é demais! Agora tudo o que preciso fazer é entender como funciona. :-)
Bob Jarvis - Reintegrar Monica
@BobJarvis adicionou uma edição para explicar o que ele faz. Correções de ortografia / gramática são bem-vindas.
Nefreo de
"A resposta aceita tem baixo desempenho" - qual é a resposta aceita neste tópico? Por favor, use os links para referenciar a outra postagem.
0xdb
28

expressões regulares são uma coisa maravilhosa :)

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
fonte
1
oi, você pode me esclarecer por que a consulta acima fornece linhas duplicadas se eu não usei palavras-chave distintas na consulta
Jagadeesh G
2
Essa consulta é inutilizável devido a @JagadeeshG, especialmente em tabelas enormes.
Michael-O
3
Extremamente lento, há uma resposta melhor abaixo
MoreCoffee
A razão da lentidão é que todas as combinações de Names estão conectadas, o que pode ser visto se você remover distinct. Infelizmente, acrescentando and Name = prior Nameàs connect bycausas da cláusula ORA-01436: CONNECT BY loop in user data.
mik
Você pode evitar o ORA-01436erro adicionando AND name = PRIOR name(ou qualquer que seja a chave primária) e AND PRIOR SYS_GUID() IS NOT NULL
David Faber
28

Há uma grande diferença entre os dois abaixo:

  • dividindo uma única string delimitada
  • divisão de strings delimitadas para várias linhas em uma tabela.

Se você não restringir as linhas, a cláusula CONNECT BY produzirá várias linhas e não dará a saída desejada.

Além das expressões regulares , algumas outras alternativas estão usando:

  • XMLTable
  • Cláusula MODEL

Configuração

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 a 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
fonte
1
Você pode elaborar mais, por que tem que haver ('"' || REPLACE(text, ',', '","') || '"')e os colchetes não podem ser removidos? Documentos Oracle ([ docs.oracle.com/database/121/SQLRF/functions268.htm ) não são claros para mim. É isso XQuery_string?
Betlista
@Betlista é uma expressão XQuery.
Lalit Kumar B
A solução XMLTABLE, por algum motivo, falha constantemente em produzir a última entrada para linhas de comprimento misto. Por exemplo. linha1: 3 palavras; linha2: 2 palavras, linha3: 1 palavra; linha 4: 2 palavras, linha 5: 1 palavra - não produzirá a última palavra. A ordem das linhas não importa.
Gnudiff
7

Mais alguns exemplos do mesmo:

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
/

Além disso, pode 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
fonte
Esteja ciente de que comma_to_table()só funciona com tokens que se enquadram nas convenções de nomenclatura de objetos de banco de dados Oracle. Vai arremessar em uma corda, '123,456,789'por exemplo.
APC
7

Eu gostaria de propor uma abordagem diferente usando uma função de tabela PIPELINED. É um pouco semelhante à técnica de XMLTABLE, exceto que você está fornecendo sua própria função personalizada para dividir a sequência 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

O problema com esse tipo de abordagem é que muitas vezes o otimizador não sabe a cardinalidade da função da tabela e terá que fazer uma estimativa. Isso pode ser potencialmente prejudicial aos seus planos de execução, portanto, essa solução pode ser estendida para fornecer estatísticas de execução para o otimizador.

Você pode ver essa estimativa do otimizador executando um PLANO DE EXPLICAÇÃO na consulta acima:

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 |
----------------------------------------------------------------------------------------------

Mesmo que a coleção tenha apenas 3 valores, o otimizador estimou 8.168 linhas para ela (valor padrão). Isso pode parecer irrelevante no início, mas pode ser o suficiente para o otimizador decidir por um plano abaixo do ideal.

A solução é usar as extensões do otimizador para fornecer estatísticas para a coleção:

-- 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;

Testando o plano de execução 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 você pode ver, a cardinalidade no plano acima não é mais o valor estimado de 8196. Ainda não está correto porque estamos passando uma coluna em vez de um literal de string para a função.

Alguns ajustes no código da função seriam necessários para fornecer uma estimativa mais próxima neste caso específico, mas acho que o conceito geral é explicado muito bem aqui.

A função str2tbl usada nesta resposta foi desenvolvida originalmente por Tom Kyte: https://asktom.oracle.com/pls/asktom/f?p=100:11:0:::::P11_QUESTION_ID:110612348061

O conceito de associação de estatísticas com tipos de objeto pode ser explorado posteriormente lendo este artigo: http://www.oracle-developer.net/display.php?id=427

A técnica descrita aqui funciona em 10g +.

Daniela Petruzalek
fonte
4

REGEXP_COUNT não foi adicionado até o Oracle 11i. Aqui está uma solução Oracle 10g, adotada da solução 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
fonte
Como posso adicionar um filtro para isso, digamos que eu queira filtrar apenas com nome = '108'. Tentei adicionar um where após a cláusula from, mas acabei com duplicatas.
DRTauli
4

A partir do Oracle 12c, você pode usar JSON_TABLEe JSON_ARRAY:

CREATE TABLE tab(Name, Project, Error) AS
SELECT 108,'test' ,'Err1, Err2, Err3' FROM dual UNION 
SELECT 109,'test2','Err1'             FROM dual;

E 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;

Resultado:

┌──────┬─────────┬──────────────────┬──────┐
 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 <> demonstração de violino

Lukasz Szozda
fonte
1
Admito que é um truque inteligente, mas, francamente, ficaria intrigado se o encontrasse em uma base de código.
APC
@APC Isso é apenas uma amostra do que é possível com SQL. Se eu tiver que usar esse código em minha base de código, eu definitivamente iria envolvê-lo em uma função ou deixar um comentário estendido :)
Lukasz Szozda
Claro. É que este tópico é um dos hits mais populares para tokenização de string com Oracle, então acho que devemos incluir advertências sobre as soluções mais exóticas, para proteger os inocentes de si mesmos :)
APC
3

Aqui está uma implementação alternativa usando XMLTABLE que permite a conversão para diferentes tipos de dados:

select 
  xmltab.txt
from xmltable(
  'for $text in tokenize("a,b,c", ",") return $text'
  columns 
    txt varchar2(4000) path '.'
) xmltab
;

... ou se suas strings delimitadas forem armazenadas em uma ou mais linhas de uma tabela:

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
fonte
Acho que essa solução funciona para Oracle 11.2.0.3 e versões posteriores.
APC
2

Eu gostaria de adicionar outro método. Este usa querys recursivas, algo que não vi nas outras respostas. É suportado pela 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

É bastante flexível com o caráter de divisão. Basta alterá-lo nas INSTRchamadas.

Thomas Tschernich
fonte
2

Sem usar conectar por ou 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
fonte
1

Eu tive o mesmo problema, e xmltable me ajudou:

SELECT id, trim (COLUMN_VALUE) text FROM t, xmltable (('"' || REPLACE (text, ',', '", "') || '"'))

Volkov Maxim
fonte
0

No Oracle 11g e posterior, você pode usar uma subconsulta recursiva e funções de string simples (que podem ser mais rápidas do que expressões regulares e subconsultas hierárquicas correlacionadas):

Configuração do 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

Produto :

NAME | PROJETO | ERRO
---: | : ------ | : ----
 108 teste | Err1
 109 test2 | Err1
 108 teste | Err2
 108 teste | Err3

db <> fiddle aqui

MT0
fonte
-1

Eu usei a função DBMS_UTILITY.comma_to _table, na verdade, está funcionando no código a seguir

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; 

eu tinha usado minha própria tabela e nomes de coluna

Smart003
fonte
5
Esteja ciente de que comma_to_table()só funciona com tokens que se enquadram nas convenções de nomenclatura de objetos de banco de dados Oracle. Vai arremessar em uma corda, '123,456,789'por exemplo.
APC
podemos implementar usando tabelas temporárias?
Smart003
1
Umm, dadas todas as outras soluções viáveis, por que deveríamos usar tabelas temporárias que vêm com uma sobrecarga enorme de materializar os dados?
APC