web-dev-qa-db-fra.com

Fractionner une chaîne en plusieurs lignes dans Oracle

Je sais que des réponses ont été apportées à PHP et à MYSQL, mais je me demandais si quelqu'un pourrait m'apprendre la méthode la plus simple pour fractionner une chaîne (délimitée par des virgules) en plusieurs lignes dans Oracle 10g (de préférence) et 11g.

Le tableau est comme suit:

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

Je veux créer ce qui suit:

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

J'ai vu quelques solutions potentielles autour de la pile, mais elles ne représentaient qu'une seule colonne (la chaîne délimitée par des virgules). Toute aide serait grandement appréciée.

96
marshalllaw

La réponse acceptée présente de faibles performances lors de l'utilisation de jeux de données volumineux.

Cela peut être un moyen amélioré (également avec regexp et 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: Voici une explication simple (comme dans "pas en profondeur") de la requête.

  1. length (regexp_replace(t.error, '[^,]+')) + 1 utilise regexp_replace pour effacer tout ce qui n'est pas le délimiteur (virgule dans ce cas) et length +1 pour obtenir le nombre d'éléments (erreurs) qui s'y trouvent.
  2. La select level from dual connect by level <= (...) utilise une requête hiérarchique pour créer une colonne avec un nombre croissant de correspondances trouvées, de 1 au nombre total d'erreurs.

    Aperçu:

    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)) effectue quelques transtypages de types Oracle.
    • cast(multiset(.....)) as sys.OdciNumberList transforme plusieurs collections (une collection pour chaque ligne du jeu de données d'origine) en une seule collection de nombres, OdciNumberList.
    • La fonction table() transforme une collection en un ensemble de résultats.
  4. FROM sans jointure crée une jointure croisée entre votre jeu de données et le multiset. Par conséquent, une ligne du jeu de données contenant 4 correspondances sera répétée 4 fois (avec un nombre croissant dans la colonne intitulée "column_value").

    Aperçu:

    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)) utilise le column_value comme paramètre nth_appearance/ocurrence pour regexp_substr.
  6. Vous pouvez ajouter d'autres colonnes de votre ensemble de données (t.name, t.project par exemple) pour faciliter la visualisation.

Quelques références à la documentation Oracle:

104
Nefreo

les expressions régulières est une chose merveilleuse :)

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

Il y a une énorme différence entre les deux ci-dessous:

  • division d'une seule chaîne délimitée
  • diviser des chaînes délimitées pour plusieurs lignes dans une table.

Si vous ne limitez pas les lignes, la clause CONNECT BY produira plusieurs lignes et ne donnera pas le résultat souhaité.

Outre les expressions rationnelles , quelques autres alternatives utilisent:

  • XMLTable
  • CLAUSE _ MODEL

Configuration

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>

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

Utilisation de la clause 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>
28
Lalit Kumar B

Quelques autres exemples de la même chose:

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
/

Vous pouvez également utiliser 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

Je voudrais proposer une approche différente en utilisant une fonction de table PIPELINED. C'est un peu similaire à la technique de XMLTABLE, sauf que vous fournissez votre propre fonction personnalisée pour scinder la chaîne de caractères:

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

Résultats:

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

Le problème avec ce type d’approche est que souvent l’optimiseur ne saura pas la cardinalité de la fonction table et devra deviner. Cela pourrait être potentiellement dommageable pour vos plans d'exécution. Cette solution peut donc être étendue pour fournir des statistiques d'exécution pour l'optimiseur.

Vous pouvez voir cette estimation de l'optimiseur en exécutant un EXPLAIN PLAN sur la requête ci-dessus:

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

Bien que la collection ne contienne que 3 valeurs, l'optimiseur a estimé 8168 lignes à sa place (valeur par défaut). Cela peut sembler peu pertinent au début, mais cela peut suffire à l'optimiseur pour décider d'un plan sous-optimal.

La solution consiste à utiliser les extensions de l'optimiseur pour fournir des statistiques sur la collection:

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

Test du plan d'exécution résultant:

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

Comme vous pouvez le constater, la cardinalité sur le plan ci-dessus n’est plus la valeur 8196 supposée. Ce n'est toujours pas correct car nous passons une colonne au lieu d'un littéral de chaîne à la fonction.

Quelques ajustements au code de fonction seraient nécessaires pour donner une estimation plus précise dans ce cas particulier, mais je pense que le concept global est expliqué en détail ici.

La fonction str2tbl utilisée dans cette réponse a été développée par Tom Kyte: https://asktom.Oracle.com/pls/asktom/f?p=100:11:::::P11_QUESTION_ID:110612348061 =

Le concept d'association de statistiques à des types d'objet peut être approfondi en lisant cet article: http://www.Oracle-developer.net/display.php?id=427

La technique décrite ici fonctionne en 10g +.

6
Daniela Petruzalek

Je pense que la meilleure façon je me connecte par et fonction regexp

   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 n'a pas été ajouté avant Oracle 11i. Voici une solution Oracle 10g, issue de la solution d'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;
4
durette

Voici une implémentation alternative utilisant XMLTABLE qui permet de transtyper différents types de données:

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

... ou si vos chaînes délimitées sont stockées dans une ou plusieurs lignes d'une table:

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

A partir d'Oracle 12c, vous pouvez utiliser JSON_TABLE et JSON_ARRAY:

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

Et requête:

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;

Sortie:

┌──────┬─────────┬──────────────────┬──────┐
│ 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 <> démo fiddle

2
Lukasz Szozda

Sans utiliser connect by 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;
2
Ilya Kharlamov

J'aimerais ajouter une autre méthode. Celui-ci utilise des requêtes récursives, quelque chose que je n'ai pas vu dans les autres réponses. Il est pris en charge par Oracle depuis 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

C'est assez flexible avec le personnage qui se dédouble. Changez-le simplement dans les appels INSTR.

1
Thomas Tschernich

J'ai eu le même problème et xmltable m'a aidé:

SELECT id, rognage (COLUMN_VALUE) texte de t, xmltable (('' '' || REPLACE (text, ',', '","') || ""))

1
Volkov Maxim