Divisione della stringa in più righe in Oracle


104

So che a questo è stato risposto in una certa misura con PHP e MYSQL, ma mi chiedevo se qualcuno potesse insegnarmi l'approccio più semplice per dividere una stringa (delimitata da virgole) in più righe in Oracle 10g (preferibilmente) e 11g.

La tabella è la seguente:

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

Voglio creare quanto segue:

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

Ho visto alcune potenziali soluzioni intorno allo stack, tuttavia rappresentavano solo una singola colonna (essendo la stringa delimitata da virgole). Qualsiasi aiuto sarebbe molto apprezzato.


2
Per esempi che utilizzano REGEXP, XMLTABLEe MODELla clausola, vedi Spalato virgole stringhe delimitate in una tabella utilizzando Oracle SQL
Lalit Kumar B

Risposte:


121

Questo potrebbe essere un modo migliore (anche con regexp e 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

EDIT : Ecco una semplice (come in, "non in profondità") spiegazione della query.

  1. length (regexp_replace(t.error, '[^,]+')) + 1usa regexp_replaceper cancellare tutto ciò che non è il delimitatore (virgola in questo caso) e length +1per ottenere quanti elementi (errori) ci sono.
  2. Il select level from dual connect by level <= (...)utilizza una query gerarchica per creare una colonna con un numero crescente di corrispondenza trovata, da 1 al numero totale di errori.

    Anteprima:

    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)) fa alcuni casting di tipi di oracolo.
    • Le cast(multiset(.....)) as sys.OdciNumberListtrasforma le più raccolte (una raccolta per ogni riga del set di dati originale) in un unico insieme di numeri, OdciNumberList.
    • La table()funzione trasforma una raccolta in un gruppo di risultati.
  4. FROMsenza join crea un cross join tra il set di dati e il multiset. Di conseguenza, una riga nel set di dati con 4 corrispondenze verrà ripetuta 4 volte (con un numero crescente nella colonna denominata "column_value").

    Anteprima:

    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_valuecome parametro nth_appearance / ocurrence per regexp_substr.
  6. Puoi aggiungere altre colonne dal tuo set di dati ( t.name, t.projectcome esempio) per una facile visualizzazione.

Alcuni riferimenti a documenti Oracle:


7
Attenzione! Una regex del formato '[^,]+'per analizzare le stringhe non restituisce l'elemento corretto se nell'elenco è presente un elemento nullo. Vedi qui per maggiori info: stackoverflow.com/questions/31464275/...
Gary_W

13
da 11g puoi usare al regexp_count(t.error, ',')posto di length (regexp_replace(t.error, '[^,]+')), il che potrebbe portare un altro miglioramento delle prestazioni
Štefan Oravec

1
485 secondi con CONNECT BY "normale". 0,296 secondi in questo modo. Sei forte! Adesso non mi resta che capire come funziona. :-)
Bob Jarvis - Ripristina Monica il

@BobJarvis ha aggiunto una modifica per spiegare cosa fa. Le correzioni ortografiche / grammaticali sono benvenute.
Nefreo

"La risposta accettata ha prestazioni scadenti": qual è la risposta accettata in questo argomento? Si prega di utilizzare i collegamenti per fare riferimento all'altro post.
0xdb

28

le espressioni regolari sono una cosa meravigliosa :)

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

1
ciao, puoi chiarirmi perché la query sopra fornisce righe duplicate se non ho usato una parola chiave distinta nella query
Jagadeesh G

2
Quella query è inutilizzabile a causa di @JagadeeshG, specialmente su tabelle enormi.
Michael-O

3
Estremamente lento, c'è una risposta migliore di seguito
MoreCoffee

Il motivo della lentezza è che ogni combinazione di Names è connessa, cosa che può essere vista se rimuovi distinct. Purtroppo aggiungendo and Name = prior Namealla connect byclausola cause ORA-01436: CONNECT BY loop in user data.
mik

Puoi evitare l' ORA-01436errore aggiungendo AND name = PRIOR name(o qualunque sia la chiave primaria) e AND PRIOR SYS_GUID() IS NOT NULL
David Faber

28

C'è un'enorme differenza tra i due seguenti:

  • divisione di una singola stringa delimitata
  • suddivisione di stringhe delimitate per più righe in una tabella.

Se non si limitano le righe, la clausola CONNECT BY produrrà più righe e non darà l'output desiderato.

Oltre alle espressioni regolari , alcune altre alternative utilizzano:

  • XMLTable
  • Clausola MODELLO

Impostare

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>

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

Utilizzando la clausola 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>

1
Puoi approfondire, perché deve esserci ('"' || REPLACE(text, ',', '","') || '"')e le parentesi non possono essere rimosse? I documenti Oracle ([ docs.oracle.com/database/121/SQLRF/functions268.htm ) non mi sono chiari. È vero XQuery_string?
Betlista

@Betlista è un'espressione XQuery.
Lalit Kumar B

La soluzione XMLTABLE per qualche motivo non riesce costantemente a produrre l'ultima voce per righe di lunghezza mista. Per esempio. riga1: 3 parole; riga2: 2 parole, riga3: 1 parola; riga4: 2 parole, riga5: 1 parola - non produrrà l'ultima parola. L'ordinamento delle righe non ha importanza.
Gnudiff

7

Un paio di altri esempi dello stesso:

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
/

Inoltre, può utilizzare 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


Tieni presente che comma_to_table()funziona solo con token che si adattano alle convenzioni di denominazione degli oggetti del database di Oracle. Scaglierà su una stringa come '123,456,789'per esempio.
APC

7

Vorrei proporre un approccio diverso utilizzando una funzione di tabella PIPELINED. È in qualche modo simile alla tecnica di XMLTABLE, tranne per il fatto che stai fornendo la tua funzione personalizzata per dividere la stringa di caratteri:

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

risultati:

      NAME PROJECT    ERROR
---------- ---------- --------------------
       108 test       Err1
       108 test       Err2
       108 test       Err3
       109 test2      Err1

Il problema con questo tipo di approccio è che spesso l'ottimizzatore non conosce la cardinalità della funzione tabella e dovrà fare un'ipotesi. Questo potrebbe essere potenzialmente dannoso per i tuoi piani di esecuzione, quindi questa soluzione può essere estesa per fornire statistiche di esecuzione per l'ottimizzatore.

Puoi vedere questa stima dell'ottimizzatore eseguendo un EXPLAIN PLAN sulla query sopra:

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

Anche se la raccolta ha solo 3 valori, l'ottimizzatore ha stimato 8168 righe per essa (valore predefinito). All'inizio può sembrare irrilevante, ma potrebbe essere sufficiente che l'ottimizzatore decida per un piano non ottimale.

La soluzione è utilizzare le estensioni dell'ottimizzatore per fornire statistiche per la raccolta:

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

Testare il piano di esecuzione risultante:

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

Come puoi vedere la cardinalità sul piano sopra non è più il valore indovinato 8196. Non è ancora corretto perché stiamo passando una colonna invece di una stringa letterale alla funzione.

Sarebbe necessario apportare alcune modifiche al codice della funzione per fornire una stima più accurata in questo caso particolare, ma penso che il concetto generale sia praticamente spiegato qui.

La funzione str2tbl utilizzata in questa risposta è stata originariamente sviluppata da Tom Kyte: https://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:110612348061

Il concetto di associazione delle statistiche con i tipi di oggetti può essere ulteriormente esplorato leggendo questo articolo: http://www.oracle-developer.net/display.php?id=427

La tecnica qui descritta funziona in 10g +.


4

REGEXP_COUNT non è stato aggiunto fino a Oracle 11i. Ecco una soluzione Oracle 10g, adottata dalla soluzione di 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;

Come posso aggiungere un filtro per questo, diciamo che voglio filtrare solo con name = '108'. Ho provato ad aggiungere un dove dopo la clausola from, ma ho finito con i duplicati.
DR Tauli

4

A partire da Oracle 12c potresti usare 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 interroga:

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;

Produzione:

┌──────┬─────────┬──────────────────┬──────┐
 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 <> fiddle demo


1
Ammetto che questo è un trucco intelligente, ma francamente mi sorprenderebbe se lo trovassi in una base di codice.
APC

@APC Questa è solo una dimostrazione di ciò che è possibile fare con SQL. Se devo utilizzare tale codice nella mia base di codice, lo inserirò sicuramente in una funzione o lascerei un commento esteso :)
Lukasz Szozda

Ovviamente. È solo che questo thread è uno dei successi più popolari per la tokenizzazione di stringhe con Oracle, quindi penso che dovremmo includere avvertimenti sulle soluzioni più esotiche, per proteggere gli innocenti da se stessi :)
APC

3

Ecco un'implementazione alternativa che utilizza XMLTABLE che consente il casting a diversi tipi di dati:

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

... o se le tue stringhe delimitate sono archiviate in una o più righe di una tabella:

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
;

Penso che questa soluzione funzioni per Oracle 11.2.0.3 e versioni successive.
APC

2

Vorrei aggiungere un altro metodo. Questo utilizza query ricorsive, qualcosa che non ho visto nelle altre risposte. È supportato da Oracle sin da 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

È abbastanza flessibile con il carattere di scissione. Basta cambiarlo nelle INSTRchiamate.


2

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

1

Ho avuto lo stesso problema e xmltable mi ha aiutato:

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


0

In Oracle 11g e versioni successive, è possibile utilizzare una sottoquery ricorsiva e semplici funzioni stringa (che potrebbero essere più veloci delle espressioni regolari e delle sottoquery gerarchiche correlate):

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

Query :

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

Uscita :

NOME | PROGETTO | ERRORE
---: | : ------ | : ----
 108 | test | ERR1
 109 | test2 | ERR1
 108 | test | Err2
 108 | test | Err3

db <> fiddle qui


-1

avevo usato la funzione DBMS_UTILITY.comma_to _table in realtà funziona il codice come segue

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; 

avevo usato i miei nomi di tabella e colonna


5
Tieni presente che comma_to_table()funziona solo con token che si adattano alle convenzioni di denominazione degli oggetti del database di Oracle. Scaglierà su una stringa come '123,456,789'per esempio.
APC

possiamo implementare utilizzando tabelle temporanee?
Smart003

1
Umm, date tutte le altre soluzioni praticabili, perché dovremmo utilizzare tabelle temporanee che comportano un enorme sovraccarico di materializzazione dei dati?
APC
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.