web-dev-qa-db-ger.com

Aufteilen eines Strings in mehrere Zeilen in Oracle

Ich weiß, dass dies bis zu einem gewissen Grad mit PHP und MYSQL beantwortet wurde, aber ich habe mich gefragt, ob mir jemand die einfachste Methode beibringen kann, einen String (durch Kommas getrennt) in mehrere Zeilen in Oracle 10g aufzuteilen ( vorzugsweise) und 11 g.

Die Tabelle sieht wie folgt aus:

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

Ich möchte Folgendes erstellen:

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

Ich habe ein paar mögliche Lösungen für den Stapel gesehen, die jedoch nur eine einzige Spalte ausmachten (die durch Kommas getrennte Zeichenfolge). Jede Hilfe wäre sehr dankbar.

96
marshalllaw

Die akzeptierte Antwort weist eine schlechte Leistung auf, wenn große Datensätze verwendet werden.

Dies kann ein verbesserter Weg sein (auch mit regexp und 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 [~ # ~] : Hier ist eine einfache (wie "nicht eingehende") Erklärung der Abfrage.

  1. length (regexp_replace(t.error, '[^,]+')) + 1 verwendet regexp_replace, um alles zu löschen, was nicht das Trennzeichen ist (in diesem Fall Komma), und length +1, um zu ermitteln, wie viele Elemente (Fehler) vorhanden sind.
  2. Die Funktion select level from dual connect by level <= (...) verwendet eine hierarchische Abfrage , um eine Spalte mit einer zunehmenden Anzahl gefundener Übereinstimmungen von 1 bis zur Gesamtzahl der Fehler zu erstellen .

    Vorschau:

    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)) führt ein Casting von Oracle-Typen durch.
    • Die Funktion cast(multiset(.....)) as sys.OdciNumberList transformiert mehrere Sammlungen (eine Sammlung für jede Zeile im Originaldatensatz) in eine einzige Sammlung von Zahlen, OdciNumberList.
    • Die Funktion table() wandelt eine Auflistung in eine Ergebnismenge um.
  4. FROM ohne Join erstellt einen Cross-Join zwischen Ihrem Dataset und dem Multiset. Infolgedessen wird eine Zeile im Datensatz mit 4 Übereinstimmungen viermal wiederholt (mit zunehmender Nummer in der Spalte "column_value").

    Vorschau:

    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)) verwendet den column_value als n-ten_Auftritt/Vorkommen Parameter für regexp_substr.
  6. Sie können einige andere Spalten aus Ihrem Datensatz (t.name, t.project Als Beispiel) zur einfachen Visualisierung hinzufügen.

Einige Verweise auf Oracle-Dokumente:

104
Nefreo

reguläre Ausdrücke sind eine wunderbare Sache :)

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
28
Andrey Khmelev

Es gibt einen großen Unterschied zwischen den folgenden beiden:

  • aufteilen einer einzelnen Zeichenfolge mit Trennzeichen
  • aufteilen von durch Trennzeichen getrennten Zeichenfolgen für mehrere Zeilen in einer Tabelle.

Wenn Sie die Zeilen nicht einschränken, erzeugt die CONNECT BY -Klausel mehrere Zeilen und wird nicht die gewünschte Ausgabe geben.

Abgesehen von regulären Ausdrücken verwenden einige andere Alternativen:

  • XMLTable
  • [~ # ~] Modell [~ # ~] Klausel

Setup

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>

Verwenden von [~ # ~] 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>

Verwenden der [~ # ~] Modell [~ # ~] -Klausel:

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>
28
Lalit Kumar B

Ein paar weitere Beispiele dafür:

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
/

Verwenden Sie möglicherweise auch 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

8
Art

Ich möchte einen anderen Ansatz mit einer PIPELINED-Tabellenfunktion vorschlagen. Dies ähnelt in gewisser Weise der XMLTABLE-Technik, mit der Ausnahme, dass Sie Ihre eigene benutzerdefinierte Funktion zum Teilen der Zeichenfolge bereitstellen:

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

Ergebnisse:

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

Das Problem bei dieser Art von Ansatz ist, dass der Optimierer die Kardinalität der Tabellenfunktion häufig nicht kennt und eine Vermutung anstellen muss. Dies kann potenziell schädlich für Ihre Ausführungspläne sein. Daher kann diese Lösung erweitert werden, um Ausführungsstatistiken für das Optimierungsprogramm bereitzustellen.

Sie können diese Optimierungsschätzung anzeigen, indem Sie in der obigen Abfrage einen EXPLAIN PLAN ausführen:

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

Obwohl die Auflistung nur 3 Werte enthält, schätzt das Optimierungsprogramm 8168 Zeilen (Standardwert). Dies mag zunächst irrelevant erscheinen, aber es kann ausreichen, dass sich der Optimierer für einen suboptimalen Plan entscheidet.

Die Lösung besteht darin, die Optimierererweiterungen zu verwenden, um Statistiken für die Sammlung bereitzustellen:

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

Testen des resultierenden Ausführungsplans:

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

Wie Sie sehen können, ist die Kardinalität im obigen Plan nicht mehr der 8196-Schätzwert. Es ist immer noch nicht korrekt, da wir eine Spalte anstelle eines String-Literal an die Funktion übergeben.

Einige Anpassungen am Funktionscode wären notwendig, um in diesem speziellen Fall eine genauere Schätzung vorzunehmen, aber ich denke, das Gesamtkonzept wird hier ziemlich genau erklärt.

Die in dieser Antwort verwendete Funktion str2tbl wurde ursprünglich von Tom Kyte entwickelt: https://asktom.Oracle.com/pls/asktom/f?p=100:11:0:::P11_QUESTION_ID:110612348061 =

Weitere Informationen zum Zuordnen von Statistiken zu Objekttypen finden Sie in diesem Artikel: http://www.Oracle-developer.net/display.php?id=427

Die hier beschriebene Technik funktioniert in 10g +.

6

Ich denke, die beste Art und Weise, wie ich mich verbinde, und regexp-Funktion

   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

SOURCE

4
SüniÚr

REGEXP_COUNT wurde erst mit Oracle 11i hinzugefügt. Hier ist eine Oracle 10g-Lösung, die von Art's Lösung übernommen wurde.

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

Hier ist eine alternative Implementierung mit XMLTABLE, die das Umwandeln in verschiedene Datentypen ermöglicht:

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

... oder wenn Ihre durch Trennzeichen getrennten Zeichenfolgen in einer oder mehreren Zeilen einer Tabelle gespeichert sind:

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

Ab Oracle 12c können Sie JSON_TABLE und JSON_ARRAY:

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

Und fragen Sie:

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;

Ausgabe:

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

2
Lukasz Szozda

Ohne connect by oder regexp zu verwenden:

    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;
2
Ilya Kharlamov

Ich möchte eine andere Methode hinzufügen. Dieser verwendet rekursive Abfragen, was ich in den anderen Antworten nicht gesehen habe. Es wird von Oracle seit 11gR2 unterstützt.

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 ist sehr flexibel mit dem Splitting-Charakter. Ändern Sie es einfach in den INSTR Aufrufen.

1

Ich hatte das gleiche Problem und xmltable hat mir geholfen:

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

1
Volkov Maxim