在Oracle中将字符串拆分为多行
Posted
技术标签:
【中文标题】在Oracle中将字符串拆分为多行【英文标题】:Splitting string into multiple rows in Oracle 【发布时间】:2013-01-14 23:20:58 【问题描述】:我知道 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
我已经看到了一些关于堆栈的潜在解决方案,但是它们只占一列(即逗号分隔的字符串)。任何帮助将不胜感激。
【问题讨论】:
有关使用REGEXP
、XMLTABLE
和MODEL
子句的示例,请参阅Split comma delimited strings in a table using Oracle SQL
【参考方案1】:
这可能是一种改进的方式(也可以使用正则表达式和连接方式):
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
编辑: 这是对查询的简单解释(如“不深入”)。
length (regexp_replace(t.error, '[^,]+')) + 1
使用 regexp_replace
删除任何不是分隔符(在本例中为逗号)和 length +1
以获取有多少元素(错误)。
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
table(cast(multiset(.....) as sys.OdciNumberList))
做了一些 oracle 类型的转换。
cast(multiset(.....)) as sys.OdciNumberList
将多个集合(原始数据集中的每一行对应一个集合)转换为一个数字集合 OdciNumberList。
table()
函数将集合转换为结果集。
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
trim(regexp_substr(t.error, '[^,]+', 1, levels.column_value))
使用column_value
作为regexp_substr
的nth_appearance/ocurrence 参数。
您可以从数据集中添加一些其他列(以t.name, t.project
为例),以便于可视化。
Oracle 文档的一些参考:
REGEXP_REPLACE REGEXP_SUBSTR Extensibility Constants, Types, and Mappings (OdciNumberList) CAST (multiset) Hierarchical Queries【讨论】:
当心!如果列表中有空元素,则用于解析字符串的'[^,]+'
格式的正则表达式不会返回正确的项目。请参阅此处了解更多信息:***.com/questions/31464275/…
从11g开始可以使用regexp_count(t.error, ',')
代替length (regexp_replace(t.error, '[^,]+'))
,这可能会带来另一个性能提升
485 秒,“正常”连接方式。这样0.296秒。你摇滚!现在我要做的就是了解它是如何工作的。 :-)
@BobJarvis 添加了一个编辑来解释它的作用。欢迎拼写/语法更正。
“接受的答案表现不佳” - 本主题中接受的答案是什么?请使用链接来引用其他帖子。【参考方案2】:
正则表达式是一件很棒的事情:)
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
【讨论】:
嗨,如果我没有在查询中使用 distinct 关键字,您能否解释一下为什么上述查询会给出重复的行 由于@JagadeeshG,该查询无法使用,尤其是在大表上。 极慢,下面有更好的答案 慢的原因是Name
s的每一个组合都是连通的,去掉distinct
就可以看到。不幸的是,将and Name = prior Name
添加到connect by
子句会导致ORA-01436: CONNECT BY loop in user data
。
您可以通过添加AND name = PRIOR name
(或任何可能的主键)和 AND PRIOR SYS_GUID() IS NOT NULL
来避免ORA-01436
错误【参考方案3】:
以下两者有很大区别:
拆分单个分隔字符串 为表中的多行拆分分隔字符串。如果您不限制行,则 CONNECT BY 子句将产生 多行,并且不会提供所需的输出。
对于单个分隔字符串,请查看Split single comma delimited string into rows 要拆分表格中的分隔字符串,请查看Split comma delimited strings in a table除了正则表达式,还有其他一些替代方法正在使用:
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>
【讨论】:
您能否详细说明,为什么必须有('"' || REPLACE(text, ',', '","') || '"')
并且不能删除括号? Oracle 文档 ([docs.oracle.com/database/121/SQLRF/functions268.htm) 我不清楚。是XQuery_string
吗?
@Betlista 这是一个 XQuery 表达式。
XMLTABLE 解决方案由于某种原因经常无法输出混合长度行的最后一个条目。例如。第1行:3个字;第2行:2个词,第3行:1个词; row4 : 2 words, row5: 1 word -- 不会输出最后一个词。行的顺序无关紧要。【参考方案4】:
几个相同的例子:
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'
这样的字符串上。【参考方案5】:
我想提出一种使用 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】:从 Oracle 12c 开始,您可以使用 JSON_TABLE
和 JSON_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 demo
【讨论】:
我承认这是一个聪明的把戏,但坦率地说,如果我在代码库中遇到它,我会感到困惑。 @APC 这只是展示了 SQL 的可能性。如果我必须在我的代码库中使用这样的代码,我肯定会将它包装在一个函数中或留下一个扩展评论:) 当然。只是这个线程是使用 Oracle 进行字符串标记化的更受欢迎的热门话题之一,所以我认为我们应该对更奇特的解决方案提出警告,以保护无辜者免受自身伤害:)【参考方案7】:直到 Oracle 11i 才添加 REGEXP_COUNT。这是一个 Oracle 10g 解决方案,采用了 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;
【讨论】:
如何为此添加过滤器假设我只想使用 name = '108' 进行过滤。我尝试在 from 子句后添加一个 where 但结果是重复的。【参考方案8】:这是一个使用 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 及更高版本。【参考方案9】:我也遇到了同样的问题,xmltable 帮了我:
选择 id,修剪(COLUMN_VALUE)文本 FROM t, xmltable(('"' || REPLACE(text, ',', '","') || '"'))
【讨论】:
select trim(column_value) from xmltable('"SVN","ITA"') select to_number(column_value) from xmltable('1,2,3')【参考方案10】:我想添加另一种方法。这个使用递归查询,这是我在其他答案中没有看到的。 Oracle 从 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
拆分字符非常灵活。只需在 INSTR
调用中更改它即可。
【讨论】:
【参考方案11】:不使用 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;
【讨论】:
【参考方案12】:在 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
输出:
姓名 |项目 |错误 ---: | :-------- | :---- 108 |测试 |错误1 109 |测试2 |错误1 108 |测试 |错误2 108 |测试 |错误3
db小提琴here
【讨论】:
【参考方案13】:如果您安装了 Oracle APEX 5.1 或更高版本,您可以使用方便的APEX_STRING.split
函数,例如:
select q.Name, q.Project, s.column_value as Error
from mytable q,
APEX_STRING.split(q.Error, ',') s
第二个参数是分隔符字符串。它还接受第三个参数来限制您希望它执行的拆分次数。
https://docs.oracle.com/en/database/oracle/application-express/20.1/aeapi/SPLIT-Function-Signature-1.html#GUID-3BE7FF37-E54F-4503-91B8-94F374E243E6
【讨论】:
【参考方案14】:我使用了 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;
我使用了自己的表名和列名
【讨论】:
请注意,comma_to_table()
仅适用于符合 Oracle 数据库对象命名约定的标记。例如,它将投掷到像'123,456,789'
这样的字符串上。
我们可以使用临时表来实现吗?
嗯,考虑到所有其他可行的解决方案,为什么我们要使用临时表,这会带来巨大的数据物化开销?以上是关于在Oracle中将字符串拆分为多行的主要内容,如果未能解决你的问题,请参考以下文章