web-dev-qa-db-ja.com

Oracleで文字列を複数の行に分割する

これはPHPとMYSQLである程度答えられていますが、Oracle 10gで文字列(カンマ区切り)を複数の行に分割する最も簡単な方法を誰かに教えてもらえればと思いました。 11g。

表は以下のとおりです。

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

以下を作成したいです。

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

私はスタックの周りにいくつかの潜在的な解決策を見ました、しかしそれらは単一の列(カンマで区切られた文字列であること)だけを説明しました。任意の助けは大歓迎です。

95
marshalllaw

大規模なデータセットを使用する場合、受け入れられた回答はパフォーマンスが良くありません。

これは(regexpや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:これはクエリの簡単な説明です(「深くない」など)。

  1. length (regexp_replace(t.error, '[^,]+')) + 1regexp_replaceを使用して区切り文字以外のもの(この場合はコンマ)を消去し、length +1を使用して要素(エラー)の数を調べます。
  2. select level from dual connect by level <= (...)は、階層クエリを使用して、1からエラーの総数まで、一致数が増加する列を作成します。

    プレビュー:

    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))は、Oracle型のキャストをいくつか行います。
    • cast(multiset(.....)) as sys.OdciNumberListは、複数のコレクション(元のデータセットの各行に1つのコレクション)を単一の数字のコレクションOdciNumberListに変換します。
    • table()関数はコレクションを結果セットに変換します。
  4. 結合なしのFROMは、データセットとマルチセットの間に交差結合を作成します。結果として、4つの一致があるデータセット内の行は4回繰り返されます( "column_value"という名前の列に番号が増えていきます)。

    プレビュー:

    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))は、column_valuenth_appearance/ocurrenceパラメーターとしてregexp_substrを使用します。
  6. 簡単に視覚化するために、データセットから他の列(例としてt.name, t.project)を追加することができます。

Oracleドキュメントへの参照

103
Nefreo

正規表現は素晴らしいことです:)

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

以下の2つの間には大きな違いがあります。

  • 単一の区切り文字列を分割する
  • テーブル内の複数行の区切り文字列を分割する。

行を制限しない場合は、CONNECT BY句を使用すると、複数行が生成されます希望する出力が得られません。

正規表現とは別に、他のいくつかの方法があります。

  • XMLTable
  • MODEL

設定

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>

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>

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

同じ例をいくつか挙げます。

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
/

また、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

PIPELINED表関数を使用して別のアプローチを提案したいと思います。文字列を分割するための独自のカスタム関数を提供している点を除けば、XMLTABLEの手法と多少似ています。

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

結果:

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

この種のアプローチの問題点は、オプティマイザが表関数の基数を知らないことが多く、推測しなければならないことです。これは実行計画に有害になる可能性があるため、オプティマイザの実行統計を提供するようにこのソリューションを拡張することができます。

上記のクエリでEXPLAIN PLANを実行すると、このオプティマイザの見積もりを確認できます。

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

コレクションには3つの値しかありませんが、オプティマイザはそれに対して8168行を見積もりました(デフォルト値)。これは最初は無関係に思えるかもしれませんが、オプティマイザが次善の計画を決定するのに十分かもしれません。

解決策は、オプティマイザ拡張機能を使用してコレクションの統計を提供することです。

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

実行計画をテストします。

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

あなたが見ることができるように上記の計画の基数はもう8196推測された値ではありません。文字列リテラルの代わりに列を関数に渡しているため、まだ正しくありません。

この特定のケースでは、関数コードを少し調整してより正確に見積もる必要がありますが、ここでは全体的な概念について説明します。

この回答で使用したstr2tbl関数は、もともとTom Kyteによって開発されました。 https://asktom.Oracle.com/pls/asktom/f?p = 100:11:0 :::: P11_QUESTION_ID:110612348061

統計とオブジェクト型の関連付けの概念は、この記事を読んでさらに詳しく調べることができます。 http://www.Oracle-developer.net/display.php?id=427

ここで説明している手法は10g以降で動作します。

6

私は私が接続し、機能を正規表現する最善の方法だと思う

   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は、Oracle 11iまで追加されませんでした。これは、Artのソリューションから採用されたOracle 10gのソリューションです。

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

これはXMLTABLEを使った別の実装で、さまざまなデータ型にキャストできます。

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

...または区切り文字列がテーブルの1つ以上の行に格納されている場合

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

Oracle 12c以降では、JSON_TABLEJSON_ARRAYを使用できます。

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

そしてクエリ:

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;

出力:

┌──────┬─────────┬──────────────────┬──────┐
│ 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 <>フィドルデモ

2
Lukasz Szozda

connect byまたは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

別の方法を追加したいのですが。これは再帰的なクエリを使用します。これは他の回答では見たことがありません。 11gR2以降、Oracleでサポートされています。

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

それは分割文字で非常に柔軟です。 INSTRの呼び出しで変更するだけです。

1

私は同じ問題を抱えていました、そしてxmltableは私を助けました:

SELECT ID、トリム(COLUMN_VALUE)テキストFROM t、xmltable(( '' '|| REPLACE(text、'、 '、' "、" ')||' "'))

1
Volkov Maxim