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.
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.
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.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
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.table()
transforme une collection en un ensemble de résultats.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
trim(regexp_substr(t.error, '[^,]+', 1, levels.column_value))
utilise le column_value
comme paramètre nth_appearance/ocurrence pour regexp_substr
.t.name, t.project
par exemple) pour faciliter la visualisation.Quelques références à la documentation Oracle:
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
Il y a une énorme différence entre les deux ci-dessous:
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:
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>
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
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 +.
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
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;
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
;
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 │
└──────┴─────────┴──────────────────┴──────┘
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;
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
.
J'ai eu le même problème et xmltable m'a aidé:
SELECT id, rognage (COLUMN_VALUE) texte de t, xmltable (('' '' || REPLACE (text, ',', '","') || ""))