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


104

私はこれが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

スタックに関するいくつかの潜在的な解決策を見てきましたが、それらは単一の列(カンマ区切りの文字列)しか占めていません。どんな助けでも大歓迎です。


回答:


121

これは改善された方法である可能性があります(これも正規表現で接続します)。

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

編集:クエリの簡単な(「深さではない」など)説明です。

  1. length (regexp_replace(t.error, '[^,]+')) + 1用途regexp_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)) オラクル型のいくつかのキャストを行います。
    • は、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ドキュメントへの参照:


7
注意してください!'[^,]+'文字列を解析する形式の正規表現は、リストにnull要素がある場合、正しい項目を返しません。:詳細はこちらを参照してくださいstackoverflow.com/questions/31464275/...
Gary_W

13
あなたが使用することができ11gの以来のregexp_count(t.error, ',')代わりにlength (regexp_replace(t.error, '[^,]+'))別のパフォーマンスの向上をもたらす可能性がある、
ステファンOravec

1
「通常の」CONNECT BYで485秒。この方法で0.296秒。あなたはロック!今、私がしなければならないのは、それがどのように機能するかを理解することだけです。:-)
ボブ・ジャービス-モニカを

@BobJarvisが編集内容を追加しました。スペル/文法の修正は大歓迎です。
Nefreo

「承認された回答はパフォーマンスが低い」-このトピックで承認された回答は何ですか?リンクを使用して他の投稿を参照してください。
0xdb

28

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

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

1
こんにちは、クエリで個別のキーワードを使用しなかった場合に、上記のクエリが重複する行を
返す

2
@JagadeeshGにより、特に巨大なテーブルでは、そのクエリは使用できません。
Michael-O

3
非常に遅いですが、以下のより良い答えがあります
MoreCoffee

速度が遅いのは、のすべての組み合わせNameが接続されているためdistinctです。これは、を削除すると確認できます。残念ながらand Name = prior Name、このconnect by句を追加すると、が発生しORA-01436: CONNECT BY loop in user dataます。
mik 10/24

あなたは避けることができますORA-01436追加することによって、エラーをAND name = PRIOR name(または主キーがあるかもしれないものは何でも) AND PRIOR SYS_GUID() IS NOT NULL
デヴィッド・フェーバー

28

以下の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>

1
もう少し詳しく説明してもらえますか、なぜ('"' || REPLACE(text, ',', '","') || '"')ブラケットが必要で、ブラケットを削除できないのですか?Oracleのドキュメント([ docs.oracle.com/database/121/SQLRF/functions268.htm))がわかりません。ですよXQuery_stringね?
Betlista 2015

@BetlistaこれはXQuery式です。
Lalit Kumar B

XMLTABLEソリューションは、何らかの理由で、長さが混在する行の最後のエントリを出力できない場合があります。例えば。row1:3ワード。row2:2ワード、row3:1ワード。row4:2ワード、row5:1ワード-最後のワードを出力しません。行の順序は関係ありません。
グヌーディフ2018年

7

同じもののいくつかの例:

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


は、comma_to_table()Oracleのデータベースオブジェクトの命名規則に適合するトークンでのみ機能することに注意してください。それは'123,456,789'例えばのように文字列に投げつけるでしょう。
APC

7

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以上で機能します。


4

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;

これにフィルターを追加するにはどうすれば、名前= '108'のみでフィルターをかけたいとしましょう。from句の後にwhereを追加しようとしましたが、重複してしまいました。
DRTauli 2014年

4

オラクル部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 <> fiddleデモ


1
これは巧妙なトリックだと思いますが、正直なところ、コードベースでこれに遭遇すると困惑します。
APC

@APCこれは、SQLで何ができるかを示すだけです。私のコードベースでそのようなコードを使用する必要がある場合、私は間違いなくそれを関数でラップするか、拡張コメントを残します:)
Lukasz Szozda

もちろん。これは、このスレッドがOracleでの文字列トークン化で最も人気のあるヒットの1つであるため、よりエキゾチックなソリューションについての警告を含めて、無実の人を彼らから保護する必要があると思います:)
APC

3

さまざまなデータ型にキャストできる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
;

このソリューションはOracle 11.2.0.3以降のバージョンで機能すると思います。
APC

2

別のメソッドを追加したいのですが。これは再帰クエリを使用していますが、これは他の回答では見たことがないものです。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呼び出しでそれを変更します。


2

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;

1

同じ問題があり、xmltableが役に立ちました。

SELECT id、trim(COLUMN_VALUE)text FROM t、xmltable(( '"' || REPLACE(text、 '、'、 '"、 "')|| '"'))


0

Oracle 11g以降では、再帰的なサブクエリと単純な文字列関数を使用できます(正規表現や相関する階層的なサブクエリよりも高速な場合があります)。

Oracleのセットアップ

CREATE TABLE table_name ( name, project, error ) as
 select 108, 'test',  'Err1, Err2, Err3' from dual union all
 select 109, 'test2', 'Err1'             from dual;

クエリ

WITH table_name_error_bounds ( name, project, error, start_pos, end_pos ) AS (
  SELECT name,
         project,
         error,
         1,
         INSTR( error, ', ', 1 )
  FROM   table_name
UNION ALL
  SELECT name,
         project,
         error,
         end_pos + 2,
         INSTR( error, ', ', end_pos + 2 )
  FROM   table_name_error_bounds
  WHERE  end_pos > 0
)
SELECT name,
       project,
       CASE end_pos
       WHEN 0
       THEN SUBSTR( error, start_pos )
       ELSE SUBSTR( error, start_pos, end_pos - start_pos )
       END AS error
FROM   table_name_error_bounds

出力

NAME | プロジェクト| エラー
---:| :------ | :----
 108 | テスト| Err1
 109 | test2 | Err1
 108 | テスト| エラー2
 108 | テスト| Err3

ここに db <> fiddle


-1

DBMS_UTILITY.comma_to _table関数を実際に使用しましたが、実際には次のようにコードが機能しています

declare
l_tablen  BINARY_INTEGER;
l_tab     DBMS_UTILITY.uncl_array;
cursor cur is select * from qwer;
rec cur%rowtype;
begin
open cur;
loop
fetch cur into rec;
exit when cur%notfound;
DBMS_UTILITY.comma_to_table (
     list   => rec.val,
     tablen => l_tablen,
     tab    => l_tab);
FOR i IN 1 .. l_tablen LOOP
    DBMS_OUTPUT.put_line(i || ' : ' || l_tab(i));
END LOOP;
end loop;
close cur;
end; 

自分のテーブル名と列名を使用していた


5
は、comma_to_table()Oracleのデータベースオブジェクトの命名規則に適合するトークンでのみ機能することに注意してください。それは'123,456,789'例えばのように文字列に投げつけるでしょう。
APC

一時テーブルを使用して実装できますか?
Smart003

1
ええと、他のすべての実行可能なソリューションを考えると、なぜデータを具体化するという大きなオーバーヘッドが伴う一時テーブルを使用したいのでしょうか。
APC
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.