【问题标题】:How to best split csv strings in oracle 9i如何在 oracle 9i 中最好地拆分 csv 字符串
【发布时间】:2009-07-06 22:31:57
【问题描述】:

我希望能够在 Oracle 9i 中拆分 csv 字符串

我已阅读以下文章 http://www.oappssurd.com/2009/03/string-split-in-oracle.html

但我不明白如何进行这项工作。 以下是我的一些相关问题

  1. 这在 Oracle 9i 中是否有效,如果不能,为什么不呢?
  2. 有没有比上述解决方案更好的分割 csv 字符串的方法?
  3. 我需要创建一个新类型吗?如果是这样,我是否需要特定的特权?
  4. 我可以在函数中声明类型吗?

【问题讨论】:

    标签: oracle csv tokenize


    【解决方案1】:

    乔伊斯,

    这里是三个例子:

    1) 使用 dbms_utility.comma_to_table。这不是通用例程,因为元素应该是有效的标识符。通过一些肮脏的技巧,我们可以使其更通用:

    SQL> declare
      2    cn_non_occuring_prefix constant varchar2(4) := 'zzzz';
      3    mystring varchar2(2000):='a:sd:dfg:31456:dasd: :sdfsdf'; -- just an example
      4    l_tablen binary_integer;
      5    l_tab    dbms_utility.uncl_array;
      6  begin
      7    dbms_utility.comma_to_table
      8    ( list   => cn_non_occuring_prefix || replace(mystring,':',','||cn_non_occuring_prefix)
      9    , tablen => l_tablen
     10    , tab    => l_tab
     11    );
     12    for i in 1..l_tablen
     13    loop
     14      dbms_output.put_line(substr(l_tab(i),1+length(cn_non_occuring_prefix)));
     15    end loop;
     16  end;
     17  /
    a
    sd
    dfg
    31456
    dasd
    
    sdfsdf
    
    PL/SQL-procedure is geslaagd.
    

    2) 使用 SQL 的按级别连接。如果您使用的是 10g 或更高版本,则可以将逐级连接方法与正则表达式结合使用,如下所示:

    SQL> declare
      2    mystring varchar2(2000):='a:sd:dfg:31456:dasd: :sdfsdf'; -- just an example
      3  begin
      4    for r in
      5    ( select regexp_substr(mystring,'[^:]+',1,level) element
      6        from dual
      7     connect by level <= length(regexp_replace(mystring,'[^:]+')) + 1
      8    )
      9    loop
     10      dbms_output.put_line(r.element);
     11    end loop;
     12  end;
     13  /
    a
    sd
    dfg
    31456
    dasd
    
    sdfsdf
    
    PL/SQL-procedure is geslaagd.
    

    3) 再次使用 SQL 的按级别连接,但现在与旧的 SUBSTR/INSTR 结合使用,以防您使用的是版本 9,就像您一样:

        SQL> declare
          2    mystring varchar2(2000):='a:sd:dfg:31456:dasd: :sdfsdf'; -- just an example
          3  begin
          4    for r in
          5    ( select substr
          6             ( str
          7             , instr(str,':',1,level) + 1
          8             , instr(str,':',1,level+1) - instr(str,':',1,level) - 1
          9             ) element
         10        from (select ':' || mystring || ':' str from dual)
         11     connect by level <= length(str) - length(replace(str,':')) - 1
         12    )
         13    loop
         14      dbms_output.put_line(r.element);
         15    end loop;
         16  end;
         17  /
        a
        sd
        dfg
        31456
        dasd
    
        sdfsdf
    
    PL/SQL-procedure is geslaagd.
    

    您可以在这篇博文中看到更多类似的技术:http://rwijk.blogspot.com/2007/11/interval-based-row-generation.html

    希望这会有所帮助。

    问候, 抢。


    解决您的评论:

    将分隔值插入规范化表的示例。

    首先创建表:

    SQL> create table csv_table (col)
      2  as
      3  select 'a,sd,dfg,31456,dasd,,sdfsdf' from dual union all
      4  select 'a,bb,ccc,dddd' from dual union all
      5  select 'zz,yy,' from dual
      6  /
    
    Table created.
    
    SQL> create table normalized_table (value varchar2(10))
      2  /
    
    Table created.
    

    因为您似乎对 dbms_utility.comma_to_table 方法感兴趣,所以我在这里提到它。但是,我当然不推荐这种变体,因为标识符的怪癖和逐行处理的速度很慢。

    SQL> declare
      2    cn_non_occuring_prefix constant varchar2(4) := 'zzzz';
      3    l_tablen binary_integer;
      4    l_tab    dbms_utility.uncl_array;
      5  begin
      6    for r in (select col from csv_table)
      7    loop
      8      dbms_utility.comma_to_table
      9      ( list   => cn_non_occuring_prefix || replace(r.col,',',','||cn_non_occuring_prefix)
     10      , tablen => l_tablen
     11      , tab    => l_tab
     12      );
     13      forall i in 1..l_tablen
     14        insert into normalized_table (value)
     15        values (substr(l_tab(i),length(cn_non_occuring_prefix)+1))
     16      ;
     17    end loop;
     18  end;
     19  /
    
    PL/SQL procedure successfully completed.
    
    SQL> select * from normalized_table
      2  /
    
    VALUE
    ----------
    a
    sd
    dfg
    31456
    dasd
    
    sdfsdf
    a
    bb
    ccc
    dddd
    zz
    yy
    
    
    14 rows selected.
    

    我推荐这个单一的 SQL 变体:

    SQL> truncate table normalized_table
      2  /
    
    Table truncated.
    
    SQL> insert into normalized_table (value)
      2   select substr
      3          ( col
      4          , instr(col,',',1,l) + 1
      5          , instr(col,',',1,l+1) - instr(col,',',1,l) - 1
      6          )
      7     from ( select ',' || col || ',' col from csv_table )
      8        , ( select level l from dual connect by level <= 100 )
      9    where l <= length(col) - length(replace(col,',')) - 1
     10  /
    
    14 rows created.
    
    SQL> select * from normalized_table
      2  /
    
    VALUE
    ----------
    a
    a
    zz
    sd
    bb
    yy
    dfg
    ccc
    
    31456
    dddd
    dasd
    
    sdfsdf
    
    14 rows selected.
    

    问候, 抢。

    【讨论】:

    • 我认为使用 dbms_utility.comma_to_table 很好。这有点离题了。我将如何在充满这些 csv 值的列上运行并将它们全部插入到新表中?抱歉,我对 Oracle 非常陌生。欣赏它!乔伊斯
    • 我在答案中添加了一个部分来解决您的评论。
    • 嗨 Rob,解决方案 #2 [使用 SQL 的按级别连接] 对我来说效果很好。但是现在,我需要将 2 个 CSV 字符串[长度相等] 作为输入传递给 PL/ SQL 存储过程。而且,我需要将这两个 CSV 字符串中的值插入到表中的两个不同列中。您能告诉我该怎么做吗?
    • 2 号很棒
    【解决方案2】:

    这是一个用于 Oracle 的字符串标记器,它比那个页面更简单一些,但不知道它是否一样快:

    create or replace function splitter_count(str in varchar2, delim in char) return int as
    val int;
    begin
      val := length(replace(str, delim, delim || ' '));
      return val - length(str); 
    end;
    
    create type token_list is varray(100) of varchar2(200);
    
    CREATE or replace function tokenize (str varchar2, delim char) return token_list as
    ret token_list;
    target int;
    i int;
    this_delim int;
    last_delim int;
    BEGIN
      ret := token_list();
      i := 1;
      last_delim := 0;
      target := splitter_count(str, delim);
      while i <= target
      loop
        ret.extend();
        this_delim := instr(str, delim, 1, i);
        ret(i):= substr(str, last_delim + 1, this_delim - last_delim -1);
        i := i + 1;
        last_delim := this_delim;
      end loop;
      ret.extend();
      ret(i):= substr(str, last_delim + 1);
      return ret;
    end;
    

    你可以这样使用它:

    select tokenize('hi you person', ' ') from dual;
    VARCHAR(hi,you,person)
    

    【讨论】:

    • 这是一个非常好的方法,至少在11g以下仍然适用。这种方法很快,因为您不会每次从零位置重新开始解析,而是从中断的地方继续解析。我已经开发了一些代码来处理它有点不同,发布在my blog on Parsing a string with a CSV into multiple columns。它有点相似,但我使用流水线函数和使用笛卡尔连接的奇怪方式走这条路。我确实在各个列中有结果。
    【解决方案3】:

    您可能想更清楚地了解您想要做什么,然后我们可以给您一个具体的答案。显示你的一些代码总是有帮助的:)

    如果您使用参数来拆分一串 csv 数字(例如:1、2、3、4),然后在 IN 语句中使用它,请查看 Question 670922 中的函数 str2tbl()。通过一些更改,您可以将其更改为 VARCHAR2 或您需要的任何内容。

    在下面你可以设置:sMyCatagories等于'1,2,3,4'

    create or replace type myTableType as table of number;
    
    create or replace function str2tbl( p_str in varchar2 ) return myTableType
      as
         l_str   long default p_str || ',';
         l_n        number;
         l_data    myTableType := myTabletype();
      begin
          loop
              l_n := instr( l_str, ',' );
              exit when (nvl(l_n,0) = 0);
              l_data.extend;
              l_data( l_data.count ) := ltrim(rtrim(substr(l_str,1,l_n-1)));
              l_str := substr( l_str, l_n+1 );
          end loop;
          return l_data;
      end;
    

    并在 select 语句中使用它......

    SELECT 
      *
    FROM
      atable a 
    WHERE 
      a.category in (
            select * from INLIST (
               select cast(str2tbl(:sMyCatagories) as mytableType) from dual
            ) 
      );
    

    这仅在您使用参数时才有用。如果您在应用程序中混合 SQL,则只需使用普通的 IN 语句。

    SELECT 
      *
    FROM
      atable a 
    WHERE 
      a.category in (1,2,3,4);
    

    【讨论】:

    • 基本上,我只是想反透视 csv 数据,并想看看我是否可以在不需要创建新类型的情况下做到这一点,因为我没有权限。
    【解决方案4】:

    我最后用了这个

    create or replace function split
    (
       p_list varchar2
    
    ) return sys.dbms_debug_vc2coll pipelined
    is
       l_idx    pls_integer;
       l_list    varchar2(32767) := p_list;
       l_value    varchar2(32767);
    begin
       loop
           l_idx := instr(l_list,',');
           if l_idx > 0 then
               pipe row(substr(l_list,1,l_idx-1));
               l_list := substr(l_list,l_idx+length(','));
    
           else
               pipe row(l_list);
               exit;
           end if;
       end loop;
       return;
    end split;
    

    declare
    CURSOR c IS  select occurrence_num, graphics from supp where graphics is not null and graphics not like ' %';
    begin
      FOR r IN c LOOP   
          insert into image (photo_id,report_id, filename) 
          select image_key_seq.nextval   photo_id, r.occurrence_num report_id, 
          t.column_value  filename from table(split(cast(r.graphics as varchar2(1000)))) t where t.column_value is not null;
       END LOOP;  
    end ;
    

    【讨论】:

      【解决方案5】:

      听起来您不想添加架构(类型、功能)。解析分隔文本的一种 SQL 唯一方法是使用 instr 和 substr 调用“发疯”。

          DECLARE
            V_CSV_STRING VARCHAR2(100);
          BEGIN
            --Create a test delimited list of first_name, last_name, middle_init
            V_CSV_STRING := 'Brian,Hart,M';
      
          select substr( V_CSV_STRING||',', 1, instr(V_CSV_STRING,',')-1 ) FIRST_NAME,
                 substr( V_CSV_STRING||',,', instr( V_CSV_STRING||',,', ',') +1, 
                                   instr( V_CSV_STRING||',,', ',', 1, 2 )-instr(V_CSV_STRING||',,',',')-1 ) LAST_NAME,
                 rtrim(substr( V_CSV_STRING||',,', instr( V_CSV_STRING||',,',',',1,2)+1),',') MIDDLE_INIT
           from dual;
           END;
      

      如果您希望将结构形式化并添加适当的应用程序代码(函数、视图、类型等...),我会在 subject 上查看 Tom Kyte 的 writing

      【讨论】:

      猜你喜欢
      • 2011-04-25
      • 2018-02-09
      • 1970-01-01
      • 1970-01-01
      • 2010-10-31
      • 1970-01-01
      • 2013-10-26
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多