web-dev-qa-db-fra.com

Comment convertir des valeurs séparées par des virgules en lignes dans Oracle?

Voici le DDL -

create table tbl1 (
   id number,
   value varchar2(50)
);

insert into tbl1 values (1, 'AA, UT, BT, SK, SX');
insert into tbl1 values (2, 'AA, UT, SX');
insert into tbl1 values (3, 'UT, SK, SX, ZF');

Notez que la valeur est séparée par des virgules string.

Mais, nous avons besoin d'un résultat comme suit-

ID VALUE
-------------
1  AA
1  UT
1  BT
1  SK
1  SX
2  AA
2  UT
2  SX
3  UT
3  SK
3  SX
3  ZF

Comment écrit-on SQL pour cela?

4
Samiul Al Hossaini

Je suis d’accord pour dire que c’est un très mauvais design… .. Essayez ceci si vous ne pouvez pas changer ce design:

select distinct id, trim(regexp_substr(value,'[^,]+', 1, level) ) value, level
  from tbl1
   connect by regexp_substr(value, '[^,]+', 1, level) is not null
   order by id, level;

OUPUT

id value level
1   AA  1
1   UT  2
1   BT  3
1   SK  4
1   SX  5
2   AA  1
2   UT  2
2   SX  3
3   UT  1
3   SK  2
3   SX  3
3   ZF  4

Crédits à this

Éliminer les doublons de manière plus élégante et efficace (crédit de @mathguy)

select id, trim(regexp_substr(value,'[^,]+', 1, level) ) value, level
  from tbl1
   connect by regexp_substr(value, '[^,]+', 1, level) is not null
      and PRIOR id =  id 
      and PRIOR SYS_GUID() is not null  
   order by id, level;

Si vous voulez une approche "ANSIer", optez pour un CTE:

with t (id,res,val,lev) as (
           select id, trim(regexp_substr(value,'[^,]+', 1, 1 )) res, value as val, 1 as lev
             from tbl1
            where regexp_substr(value, '[^,]+', 1, 1) is not null
            union all           
            select id, trim(regexp_substr(val,'[^,]+', 1, lev+1) ) res, val, lev+1 as lev
              from t
              where regexp_substr(val, '[^,]+', 1, lev+1) is not null
              )
select id, res,lev
  from t
order by id, lev;

SORTIE

id  val lev
1   AA  1
1   UT  2
1   BT  3
1   SK  4
1   SX  5
2   AA  1
2   UT  2
2   SX  3
3   UT  1
3   SK  2
3   SX  3
3   ZF  4

Une autre approche récursive de MT0 mais sans regex:

WITH t ( id, value, start_pos, end_pos ) AS
  ( SELECT id, value, 1, INSTR( value, ',' ) FROM tbl1
  UNION ALL
  SELECT id,
    value,
    end_pos                    + 1,
    INSTR( value, ',', end_pos + 1 )
  FROM t
  WHERE end_pos > 0
  )
SELECT id,
  SUBSTR( value, start_pos, DECODE( end_pos, 0, LENGTH( value ) + 1, end_pos ) - start_pos ) AS value
FROM t
ORDER BY id,
  start_pos;

J'ai essayé 3 approches avec un ensemble de données de 30000 lignes et 118104 lignes renvoyées et j'ai obtenu les résultats moyens suivants:

  • Mon approche récursive: 5 secondes
  • Approche MT0: 4 secondes
  • Approche Mathguy: 16 secondes
  • Approche récursive MT0 sans regex: 3,45 secondes

@Mathguy a également testé avec un plus grand ensemble de données:

Dans tous les cas, la requête récursive (je n’ai testé que celle avec les méthodes régulières Substr et instr) est plus efficace, d’un facteur 2 à 5. Voici le combinaisons de nombre de chaînes/jetons par chaîne et exécution de CTAS temps pour hiérarchique vs récursif, hiérarchique en premier. Tous les temps en secondes

  • 30 000 x 4: 5/1. 
  • 30 000 x 10: 15/3.
  • 30 000 x 25: 56/37.
  • 5 000 x 50: 33/14.
  • 5000 x 100: 160/81.
  • 10 000 x 200: 1 924/772
11
vercelli

Cela donnera les valeurs sans que vous ayez à supprimer les doublons ou à utiliser un hack consistant à inclure SYS_GUID() ou DBMS_RANDOM.VALUE() dans le CONNECT BY:

SELECT t.id,
       v.COLUMN_VALUE AS value
FROM   TBL1 t,
       TABLE(
         CAST(
           MULTISET(
             SELECT TRIM( REGEXP_SUBSTR( t.value, '[^,]+', 1, LEVEL ) )
             FROM   DUAL
             CONNECT BY LEVEL <= REGEXP_COUNT( t.value, '[^,]+' )
           )
           AS SYS.ODCIVARCHAR2LIST
         )
       ) v

Mettre à jour :

Renvoyer l'index de l'élément dans la liste:

Option 1 - Renvoyer un UDT:

CREATE TYPE string_pair IS OBJECT( lvl INT, value VARCHAR2(4000) );
/

CREATE TYPE string_pair_table IS TABLE OF string_pair;
/

SELECT t.id,
       v.*
FROM   TBL1 t,
       TABLE(
         CAST(
           MULTISET(
             SELECT string_pair( level, TRIM( REGEXP_SUBSTR( t.value, '[^,]+', 1, LEVEL ) ) )
             FROM   DUAL
             CONNECT BY LEVEL <= REGEXP_COUNT( t.value, '[^,]+' )
           )
           AS string_pair_table
         )
       ) v;

Option 2 - Utiliser ROW_NUMBER():

SELECT t.id,
       v.COLUMN_VALUE AS value,
       ROW_NUMBER() OVER ( PARTITION BY id ORDER BY ROWNUM ) AS lvl
FROM   TBL1 t,
       TABLE(
         CAST(
           MULTISET(
             SELECT TRIM( REGEXP_SUBSTR( t.value, '[^,]+', 1, LEVEL ) )
             FROM   DUAL
             CONNECT BY LEVEL <= REGEXP_COUNT( t.value, '[^,]+' )
           )
           AS SYS.ODCIVARCHAR2LIST
         )
       ) v;
4
MT0

Une autre méthode consiste à définir une fonction PL/SQL simple:

CREATE OR REPLACE FUNCTION split_String(
  i_str    IN  VARCHAR2,
  i_delim  IN  VARCHAR2 DEFAULT ','
) RETURN SYS.ODCIVARCHAR2LIST DETERMINISTIC
AS
  p_result       SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST();
  p_start        NUMBER(5) := 1;
  p_end          NUMBER(5);
  c_len CONSTANT NUMBER(5) := LENGTH( i_str );
  c_ld  CONSTANT NUMBER(5) := LENGTH( i_delim );
BEGIN
  IF c_len > 0 THEN
    p_end := INSTR( i_str, i_delim, p_start );
    WHILE p_end > 0 LOOP
      p_result.EXTEND;
      p_result( p_result.COUNT ) := SUBSTR( i_str, p_start, p_end - p_start );
      p_start := p_end + c_ld;
      p_end := INSTR( i_str, i_delim, p_start );
    END LOOP;
    IF p_start <= c_len + 1 THEN
      p_result.EXTEND;
      p_result( p_result.COUNT ) := SUBSTR( i_str, p_start, c_len - p_start + 1 );
    END IF;
  END IF;
  RETURN p_result;
END;
/

Alors le SQL devient très simple:

SELECT t.id,
       v.column_value AS value
FROM   TBL1 t,
       TABLE( split_String( t.value ) ) v
1
MT0

Vercelli a posté une réponse correcte. Cependant, avec plus d'une chaîne à scinder, connect by générera un nombre de lignes en croissance exponentielle, avec beaucoup, beaucoup de doublons. (Essayez simplement la requête sans distinct.) Cela détruira les performances sur des données de taille non triviale.

Un moyen courant de résoudre ce problème consiste à utiliser une condition prior et une vérification supplémentaire pour éviter les cycles dans la hiérarchie. Ainsi:

select id, trim(regexp_substr(value,'[^,]+', 1, level) ) value, level
  from tbl1
   connect by regexp_substr(value, '[^,]+', 1, level) is not null
          and prior id = id
          and prior sys_guid() is not null
   order by id, level;

Voir, par exemple, cette discussion sur OTN: https://community.Oracle.com/thread/2526535

1
mathguy