如何将逗号分隔值转换为oracle中的行?

Posted

技术标签:

【中文标题】如何将逗号分隔值转换为oracle中的行?【英文标题】:How to convert comma separated values to rows in oracle? 【发布时间】:2016-11-17 05:45:52 【问题描述】:

这里是 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');

注意,这里的值是逗号分隔字符串。

但是,我们需要如下结果-

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

我们如何为此编写 SQL?

【问题讨论】:

设计本身在我眼里已经有缺陷了。在一列中存储多个值已经破坏了规范化,不应该这样做。我宁愿在插入之前拆分它们(或者不使用单个值开始)并使用循环插入单个值。 oracle -- Split multiple comma separated values in oracle table to multiple rows的可能重复 【参考方案1】:

我同意这是一个非常糟糕的设计。 如果您无法更改该设计,请尝试以下操作:

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;

输出

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

感谢this

以更优雅和有效的方式删除重复项(感谢@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;

如果您想要“ANSIer”方法,请使用 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;

输出

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

MT0 的另一种递归方法,但没有正则表达式:

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;

我尝试了 3 种方法,数据集为 30000 行,返回 118104 行,得到以下平均结果:

我的递归方法:5 秒 MT0 接近:4 秒 Mathguy 方法:16 秒 MT0 递归方法无正则表达式:3.45 秒

@Mathguy 还使用更大的数据集进行了测试:

在所有情况下,递归查询(我只测试了一个常规的 substr 和 instr) 效果更好,提高了 2 到 5 倍。这里是 # of strings/tokens per string 和 CTAS 执行的组合 分层与递归的时间,分层优先。一直在 秒

30,000 x 4: 5 / 1。 30,000 x 10: 15 / 3。 30,000 x 25: 56 / 37。 5,000 x 50: 33 / 14。 5,000 x 100: 160 / 81。 10,000 x 200:1,924 / 772

【讨论】:

查看这个,例如,如何避免重复(所以你不必select DISTINCT):community.oracle.com/thread/2526535 @vercelli 请注意,这是一个 hack。使用相关表集合表达式(根据my answer 或this answer)不会产生重复,也不需要这种技巧来防止数据中的循环连接。 @Vercelli - 性能差异将是更喜欢“集合”路线(MT0)和“递归”路线的更好理由。我要测试更多 - 我怀疑更长的字符串(每个输入字符串有更多的标记)性能可能会有所不同。如果有兴趣,我可能应该打开一个单独的问题/线程来发布测试结果等。现在,我刚刚测试了分层解决方案(“我的”解决方案),每个字符串有 30000 个输入字符串和 4 个标记,总输出 120000行;在我的笔记本电脑上使用免费版本的 Oracle 11.2,运行需要 5.1 秒。 好的,我刚刚测试了不同的场景。在所有情况下,递归查询(我只用常规的substrinstr 测试了一个)的效果更好,提高了 2 到 5 倍。以下是每个字符串的字符串/标记数和 CTAS 执行时间的组合分层与递归,分层优先。所有时间以秒为单位。 30,000 x 4: 5 / 1. 30,000 x 10: 15 / 3. 30,000 x 25: 56 / 37. 5,000 x 50: 33 / 14. 5,000 x 100: 160 / 81. 10,000 x 200: 1,924 / 772 @Raky 视情况而定。这里的一些解决方案是递归的,旧版本的 mysql 不支持 WITH。 Redshift 不支持递归。大多数正则表达式也必须更改。所以,我会说,如果您的 DBMS 支持递归,请使用“MT0 的另一种递归方法,但没有正则表达式:”解决方案【参考方案2】:

这将获得值,而无需您删除重复项或不必使用在CONNECT BY 中包含SYS_GUID()DBMS_RANDOM.VALUE() 的技巧:

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

更新

返回列表中元素的索引:

选项 1 - 返回 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;

选项 2 - 使用 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;

【讨论】:

我不同意; CONNECT BY 没有条件使用 PRIOR 运算符,就像你做的那样,是一种 hack(它违反了 Oracle 对分层查询的要求)。当使用PRIOR SYS_GUID() 打破循环时,我看不出有什么技巧;这是对分层查询的完全合法使用。 @mathguy Tom Kyte 表示这是一个bug with the documentation,并且“不,您在连接中从来不需要PRIOR。” (link)。 刚刚读到cast(multiset(....)) - 如果我没记错的话(几个月前读到这个),Oracle 中的嵌套表只能有一个列,对吧?或者是否可以不仅提取标记,还提取原始字符串中的“级别”?在某些情况下,可能令牌的顺序具有可能需要在进一步处理中使用的含义。 有多种方法可以解决这个问题: 您可以将 UDT 作为具有多个字段的对象类型的表返回;或者您可以使用ROW_NUMBER() 分析函数。只有 2 个立即浮现在脑海中。 @MT0 - 我会花一些时间在两者上。无论如何,与我被引导相信的相反,递归 CTE 方法(我总是比分层方法更喜欢它)似乎更快,而不仅仅是一小部分。我会带着一个类似的问题回到旧线程(几天前),那里的 OP 询问我们是否可以帮助提高性能,因为他的实际数据比他发布的样本“大”得多。如果我能弄明白的话,我会在那个练习中尝试递归查询,以及cast(multiset(...)) 方法(我相信你已经发布了)。谢谢!【参考方案3】:

Vercelli 发布了正确答案。但是,如果要拆分多个字符串,connect by 将生成呈指数增长的行数,其中包含许多重复项。 (只需尝试不带distinct 的查询。)这将破坏非平凡数据的性能。

解决此问题的一种常见方法是使用prior 条件和额外检查以避免层次结构中的循环。像这样:

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;

例如,请参阅 OTN 上的讨论:https://community.oracle.com/thread/2526535

【讨论】:

我知道我们之前已经讨论过这个问题,但是使用 SYS_GUID() 是一种黑客行为,我相信最好使用不会生成这些重复项的相关表集合表达式,因此您不必采取变通办法来处理它们。 我们之前确实讨论过。我什么都不知道(我今年 2 月才开始学习 SQL 和 Oracle),但我发现 OTN、Tom Kyte 等上的所有大师都使用sys_guid()dbms_random.value() 技巧。请参阅我提供的链接。请注意,没有使用 PRIOR 运算符的条件的 connect by 已经是 hack(它违反了 Oracle 对 CONNECT BY 的要求 - 请参阅文档:docs.oracle.com/cd/B28359_01/server.111/b28286/queries003.htm,请参阅语法图后的第二个要点)。跨度> @MT0 - 实际上,我改变了主意。 CONNECT BY 没有使用 PRIOR 的条件是一种 hack,将来可能不支持。我使用它的方式 不是 hack,因为我确实在至少一种情况下使用了 PRIOR 运算符。 SYS_GUID() 保证为每一行生成不同的值,从而导致层次结构中没有循环。我不同意这是一个黑客。为什么是黑客攻击? 这是一个 hack,因为 AND PRIOR SYS_GUID() IS NOT NULL 将始终为真,因此条件归结为 AND TRUE 并且应该是无关紧要的 - 但是,删除它你会得到 ORA-01436: CONNECT BY loop in user data 这是不正确的。 AND PRIOR SYS_GUID() IS NOT NULL 做了两件事,而不是一件事。它在所有情况下都评估为 TRUE,但它也会向每个新生成的行添加一个唯一的数据位。 AND TRUE 只做第一份工作,不做第二份。【参考方案4】:

另一种方法是定义一个简单的 PL/SQL 函数:

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

那么SQL就变得很简单了:

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

【讨论】:

【参考方案5】:
--converting row of data into comma sepaerated string
SELECT
    department_id,
    LISTAGG(first_name, ',') WITHIN GROUP(
        ORDER BY
            first_name
    ) comma_separted_data
FROM
    hr.employees
GROUP BY
    department_id;

--comma-separated string into row of data

CREATE TABLE t (
    deptno          NUMBER,
    employee_name   VARCHAR2(255)
);

INSERT INTO t VALUES (
    10,
    'mohan,sam,john'
);

INSERT INTO t VALUES (
    20,
    'manideeep,ashok,uma'
);

INSERT INTO t VALUES (
    30,
    'gopal,gopi,manoj'
);

SELECT
    deptno,
    employee_name,
    regexp_count(employee_name, ',') + 1,
    regexp_substr(employee_name, '\w+', 1, 1)
FROM
    t,
    LATERAL (
        SELECT
            level l
        FROM
            dual
        CONNECT BY
            level < regexp_count(employee_name, ',') + 1
    );

DROP TABLE t;

【讨论】:

您可能需要添加说明。

以上是关于如何将逗号分隔值转换为oracle中的行?的主要内容,如果未能解决你的问题,请参考以下文章

如何将数字转换为 Klaviyo 中的逗号分隔值

oracle中PLSQL存储过程中如何使用逗号分隔的集合(逗号分隔字符串转换为一个集合)

cfspreadsheet 在逗号分隔的行插入中转义逗号

将逗号分隔值拆分为 Oracle 中的列

将单个逗号分隔的行转换为多行

如何根据oracle plsql中列中的逗号分隔值拆分选择查询行