在Oracle中将字符串拆分为多行


104

我知道使用PHP和MYSQL已经在某种程度上回答了这个问题,但是我想知道是否有人可以教我在Oracle 10g(最好是11g)和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

我已经看到了一些围绕堆栈的潜在解决方案,但是它们仅占了一个列(即逗号分隔的字符串)。任何帮助将不胜感激。


2
对于使用的例子REGEXPXMLTABLEMODEL条款,请参阅 表中的拆分逗号分隔字符串使用Oracle SQL
拉利特·库马尔·乙

Answers:


121

这可能是一种改进的方式(也可以使用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

编辑:这是查询的简单说明(例如,“不深入”)。

  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)) 执行一些oracle类型的转换。
    • cast(multiset(.....)) as sys.OdciNumberList变换多个集合(一个收集在原始数据集中的每一行)转换成数字的单个集合,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_value用作的nth_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
485秒,“正常” CONNECT BY。此方式为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 2015年

缓慢的原因是Names的每个组合都已连接,如果将其删除,则可以看到它distinct。不幸的是,添加and Name = prior Nameconnect by子句中会导致ORA-01436: CONNECT BY loop in user data
mik

您可以ORA-01436通过添加AND name = PRIOR name(或任何主键) AND PRIOR SYS_GUID() IS NOT NULL
David Faber,

28

以下两个之间存在巨大差异:

  • 分割单个分隔字符串
  • 为表中的多行分割定界字符串。

如果不限制行,则CONNECT BY子句将产生多行,并且不会提供所需的输出。

正则表达式外,还有其他一些替代方法正在使用:

  • XML表格
  • 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

@Betlista,它是一个XQuery表达式。
Lalit Kumar B

由于某种原因,XMLTABLE解决方案始终无法输出混合长度行的最后一个条目。例如。第1行:3个字;第2行:2个字,第3行:1个字;第4行:2个单词,第5行:1个单词-将不输出最后一个单词。行的顺序无关紧要。
努努迪夫

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

直到Oracle 11i才添加REGEXP_COUNT。这是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;

我该如何添加过滤器,可以说我只想过滤name ='108'。我尝试在from子句后添加一个where,但最终出现重复。
DRTauli 2014年

4

从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 <> fiddle演示


1
我承认这是一个聪明的把戏,但是坦率地说,如果我在代码库中遇到它,这会让我感到困惑。
APC

@APC这只是显示SQL可能实现的功能。如果必须在我的代码库中使用这样的代码,我肯定会将其包装在一个函数中或留下扩展注释:)
Lukasz Szozda

当然。只是该线程是Oracle字符串标记化更受欢迎的命中之一,因此,我认为我们应该对更奇特的解决方案进行警告,以保护无辜者免受其害:)
APC

3

这是使用XMLTABLE的替代实现,该实现允许转换为不同的数据类型:

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

...或者如果定界的字符串存储在表的一个或多个行中:

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 byregexp

    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,从t,xmltable(('“'|| REPLACE(text,',','”,“')||'”'))修剪(COLUMN_VALUE)文本


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 | 测试 错误1
 109 | test2 | 错误1
 108 | 测试 错误2
 108 | 测试 错误3

db <> 在这里拨弄


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

我们可以使用临时表来实现吗?
Smart003

1
嗯,考虑到所有其他可行的解决方案,为什么我们要使用临时表,而这些表却需要大量的物化数据开销?
APC
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.