【问题标题】:swapping comma separated values in oracle在 oracle 中交换逗号分隔值
【发布时间】:2014-11-04 07:09:28
【问题描述】:

我有一个表格,其中一列有逗号分隔值, (例如:经度、纬度、经度1、纬度1等)..

现在我需要交换(纬度、经度、纬度1、经度1等)等值。

作为试用目的: 我创建了一个如下表:

CREATE TABLE string_table
     (
          slno       NUMBER,
          old_string VARCHAR2(50),
          new_string VARCHAR2(50)
     );
/
INSERT INTO STRING_TABLE (SLNO, OLD_STRING)
       VALUES (1, '1,2,3,4,5,6');
INSERT INTO STRING_TABLE (SLNO, OLD_STRING)
       VALUES (2, '1,2,3,4,5');
INSERT INTO STRING_TABLE (SLNO, OLD_STRING)
       VALUES (3, 'a,b,c,d,e,f');
INSERT INTO STRING_TABLE (SLNO, OLD_STRING)
       VALUES (4, 'a,b,c,d,e');
COMMIT;
/

现在表格看起来像:

slno  old_string  new_string                                         
----- ----------------------
1    1,2,3,4,5,6                                                                                           
2    1,2,3,4,5                                                                                             
3    a,b,c,d,e,f                                                                                           
4    a,b,c,d,e    

我需要将交换后的值更新到 new_string 列中,结果应该如下所示:

slno  old_string  new_string                                         
----- ----------------------
1    1,2,3,4,5,6    2,1,4,3,6,5
2    1,2,3,4,5      2,1,4,3,5
3    a,b,c,d,e,f    b,a,d,c,f,e
4    a,b,c,d,e      b,a,d,c,e

到目前为止,我所做的是使用如下所示的 COLLECTION 使用 PL/SQL 代码,并且工作正常:

SET serveroutput ON
DECLARE
TYPE my_type IS TABLE OF VARCHAR2(50);
     my_obj my_type := my_type();
     l_temp_var VARCHAR2(50);
     l_string   VARCHAR2(200);
BEGIN
     FOR i IN
     ( SELECT slno, old_string FROM string_table
     )
     loop
          FOR j IN
          (SELECT regexp_substr(i.old_string,'[^,]+',1, LEVEL) val
          FROM dual
               CONNECT BY regexp_substr(i.old_string, '[^,]+', 1, LEVEL) IS NOT NULL
          )
          loop
               my_obj.EXTEND;
               my_obj(my_obj.LAST) := j.val;
               IF mod(my_obj.count,2)= 0 THEN
                    l_temp_var := my_obj(my_obj.LAST -1);
                    my_obj(my_obj.LAST-1) := my_obj(my_obj.LAST) ;
                    my_obj(my_obj.LAST):= l_temp_var;
               END IF;
          END LOOP;
          FOR i IN my_obj.FIRST..my_obj.LAST
          loop
               l_string := l_string||my_obj(i)||',';
          END loop;
          l_string := substr(l_string , 1, length(l_string)-1);

          update string_table 
          SET new_string = l_string 
          WHERE slno = i.slno;
          l_string := NULL;
          my_obj   := my_type();
     END loop;
COMMIT;
END;
/

我认为这个解决方案很长,有没有其他好的/短/简单的方法来交换预期结果的值?

提前致谢;)

【问题讨论】:

标签: sql plsql oracle11g


【解决方案1】:

您可以使用connect by 语法将逗号分隔的列表拆分为单独的元素,然后以不同的顺序将它们重新组合在一起,所有这些都在纯 SQL 中。两个稍微棘手的位正在交换对,这可以通过将每个位置向上或向下调整一个来完成,具体取决于它是奇数还是偶数;并将此语法一次应用于多行数据,这可以通过使用确定性函数的技巧来完成:

select slno, old_string,
  listagg(item, ',') within group (order by new_pos) as new_string
from (
  select slno, old_string, regexp_substr(old_string, '[^,]+', 1, level) as item,
    case when mod(level, 2) = 1 then level + 1
      else level - 1 end as new_pos
  from string_table
  connect by level <= regexp_count(old_string, '[^,]+')
  and prior slno = slno
  and prior sys_guid() is not null
)
group by slno, old_string;

      SLNO OLD_STRING           NEW_STRING         
---------- -------------------- --------------------
         1 1,2,3,4,5,6          2,1,4,3,6,5          
         2 1,2,3,4,5            2,1,4,3,5            
         3 a,b,c,d,e,f          b,a,d,c,f,e          
         4 a,b,c,d,e            b,a,d,c,e            

然后您可以将其用作mergeusing 子句来更新原始表:

merge into string_table st
using (
  select slno, old_string,
    listagg(item, ',') within group (order by new_pos) as new_string
  from (
   select slno, old_string,
     regexp_substr(old_string, '[^,]+', 1, level) as item,
     case when mod(level, 2) = 1 then level + 1
       else level - 1 end as new_pos
   from string_table
   connect by level <= regexp_count(old_string, '[^,]+')
   and prior slno = slno
   and prior sys_guid() is not null
  )
  group by slno, old_string
) tmp
on (tmp.slno = st.slno)
when matched then
update set st.new_string = tmp.new_string;

select * from string_table order by slno;

      SLNO OLD_STRING           NEW_STRING         
---------- -------------------- --------------------
         1 1,2,3,4,5,6          2,1,4,3,6,5          
         2 1,2,3,4,5            2,1,4,3,5            
         3 a,b,c,d,e,f          b,a,d,c,f,e          
         4 a,b,c,d,e            b,a,d,c,e            

SQL Fiddle 包括内部查询产生的内容。

如果您需要更通用地使用它,您可以创建一个函数:

create or replace function swap_pairs (p_old_string varchar2)
return varchar2 as
  l_new_string string_table.new_string%type;
begin
  select listagg(item, ',') within group (order by new_pos)
  into l_new_string
  from (
   select regexp_substr(p_old_string, '[^,]+', 1, level) as item,
     case when mod(level, 2) = 1 then level + 1
       else level - 1 end as new_pos
   from dual
   connect by level <= regexp_count(p_old_string, '[^,]+')
  );

  return l_new_string;
end;
/

update string_table set new_string = swap_pairs(old_string);

SQL Fiddle.

当然,首先将逗号分隔的值存储在列中并不是一个好主意;如果您有多个对,则每个值都应该是其自己的列,在子表中。如果您要添加一个新列,我真的会认真考虑修改数据模型。有时您会卡在现有的东西上,即使您可以拆分数据,这种技术也可以用于一次性练习。

【讨论】:

  • 啊,啪。我一定是在你发帖之前开始写我的:-(。
  • @Ben - 你的奇数/偶数方法略有不同,而且解释得更好......你在我投票之前删除了 *8-)
  • 我认为差别不大,但谢谢。我将取消删除,以便如果解释上有足够的差异,它可能会对某人有所帮助。
  • @AlexPoole,感谢您的努力,我从您的回答中学到了很多东西。关于表结构,它不是我正在处理的直接表,这只是一个中介处理表。我知道将逗号分隔值存储在单个列中时的问题:).. 再次感谢您在 oracle 中教我很多东西,我希望 Ben 的 reverse_string 函数更好。
  • @ajmalmhd04 - 我们的两个函数只是在不同的地方应用mod,它们几乎是相同的方法(除了+!)。如果 Ben's 更清楚,那没关系 - 这可以算得更好。
【解决方案2】:

不,没有。这就是规范化如此重要的原因,如果您有一个看起来像这样的表格,那么您可以以任何您想要的方式聚合字符串以进行输出:

create table string_table (
     slno number
   , position number
   , string varchar2(50)
     );

但是,虽然没有一种简短或简单的方法可以做到这一点,但有一种更易于理解的方法。您想要输出的数据的顺序由以下语句描述。这里的重点(以及与您的主要区别)是 ORDER BY。 MOD(LEVEL, 2) 为偶数级别返回 0,为奇数级别返回 1。通过将 LEVEL 添加到此,您最终会得到由偶数描述的每个连续对,即 1 和 2(或 a 和 b)将是 2,3 和 4 将是 4。然后,再次按 LEVEL 排序给我们最高的优先,交换每一对。为方便起见,我将这些列中的每一列都包括在内。

SQL>  select regexp_substr('1,2,3,4,5,6', '[^,]+', 1, level) as str
  2        , mod(level, 2) as ml
  3        , level + mod(level, 2) as lml
  4        , level as l
  5     from dual
  6  connect by regexp_substr('1,2,3,4,5,6', '[^,]+', 1, level) is not null
  7    order by level + mod(level, 2), level desc;

STR   ML  LML    L
--- ---- ---- ----
2      0    2    2
1      1    2    1
4      0    4    4
3      1    4    3
6      0    6    6
5      1    6    5

6 rows selected.

然后您可以使用LISTAGG() 重新聚合。这包括一个 ORDER BY 子句,因此您不必在子选择中具有显式 ORDER BY。

SQL>  select listagg(str, ',') within group (order by lvl + mod(lvl, 2), lvl desc)
  2     from ( select regexp_substr('1,2,3,4,5,6', '[^,]+', 1, level) as str, level as lvl
  3              from dual
  4           connect by regexp_substr('1,2,3,4,5,6', '[^,]+', 1, level) is not null
  5                   );

LISTAGG(STR,',')WITHINGROUP(ORDERBYLVL+MOD(LVL,2),LVLDESC)
--------------------------------------------------------------------------------
2,1,4,3,6,5

SQL>

如果我们将所有这些都放在一个函数中(这样您就不会对每个列表中的最大项目数执行分层查询,而是对每个列表进行正确的数量),那么每个输入字符串都会得到一个返回值:

create or replace function reverse_string (PInput in varchar2) return varchar2 is

   /* Reverse each pair of items in a comma delimited list
      */
   l_output string_table.old_string%type;
begin

   select listagg(str, ',') within group (order by lvl + mod(lvl, 2), lvl desc)
     into l_output
     from ( select regexp_substr(PInput, '[^,]+', 1, level) as str, level as lvl
              from dual
           connect by regexp_substr(PInput, '[^,]+', 1, level) is not null
                   );

   return l_output; 

end reverse_string;

这可以通过一个简单的 SELECT 语句来演示:

SQL> select slno, old_string, reverse_string(old_string) as new_string
  2    from string_table;

      SLNO OLD_STRING      NEW_STRING
---------- --------------- ---------------
         1 1,2,3,4,5,6     2,1,4,3,6,5
         2 1,2,3,4,5       2,1,4,3,5
         3 a,b,c,d,e,f     b,a,d,c,f,e
         4 a,b,c,d,e       b,a,d,c,e

最后,这意味着 UPDATE 足以更新您的表。这意味着您可以在单个事务中执行它而无需循环等。

SQL> update string_table
  2     set new_string = reverse_string(old_string);

4 rows updated.

SQL>
SQL> select *
  2    from string_table;

      SLNO OLD_STRING      NEW_STRING
---------- --------------- ---------------
         1 1,2,3,4,5,6     2,1,4,3,6,5
         2 1,2,3,4,5       2,1,4,3,5
         3 a,b,c,d,e,f     b,a,d,c,f,e
         4 a,b,c,d,e       b,a,d,c,e

【讨论】:

  • 感谢@Ben,模式为 '[^,]+' 的 reverse_string 函数会为 'ab,cd' 等字符串提供更好的结果。
  • 是的,你是对的......我通常会这样做,所以不知道我遇到了什么 - 谢谢。
  • @Eat Å Peach 发表了一个很好的答案,我想我应该接受他的答案。
【解决方案3】:

仅使用 regexp_replace,

with string_table(slno, old_string)
as (
        select 1, '1,2,3,4,5,6' from dual union all
        select 2, '1,2,3,4,5' from dual union all
        select 3, 'a,b,c,d,e,f' from dual union all
        select 4, 'a,b,c,d,e' from dual
)
select
        slno,
        old_string,
        regexp_replace(old_string,'([^,]+),([^,]+)','\2,\1')    new_string
from 
        string_table;

      SLNO  OLD_STRING   NEW_STRING
----------  -----------  ------------------------------------------------------------
         1  1,2,3,4,5,6  2,1,4,3,6,5
         2  1,2,3,4,5    2,1,4,3,5
         3  a,b,c,d,e,f  b,a,d,c,f,e
         4  a,b,c,d,e    b,a,d,c,e

图案:

([^,]+) -- any string without a comma. Enclosed in brackets to form first capture group.
,       -- a comma
([^,]+) -- any string without a comma. Enclosed in brackets to form second capture group.

所以,这个模式匹配两个用逗号分隔的字符串。

替换字符串:

\2  -- the second capture group from the Pattern
,   -- a comma
\1  -- the first capture group from the Pattern

所以,这会用相同的字符串替换匹配的模式,但交换位置。

【讨论】:

  • 您的回答似乎很简单!!,我是正则表达式的初学者,如果您能稍微解释一下替换模式的工作原理,我将不胜感激。
  • 糟糕,忘记了组替换;这是一个更好的答案。
猜你喜欢
  • 2015-04-01
  • 2017-07-04
  • 1970-01-01
  • 1970-01-01
  • 2021-11-10
  • 1970-01-01
  • 2023-03-07
  • 1970-01-01
相关资源
最近更新 更多