讨论了从单行字符串拆分到多行字符串拆分,最后的解决不算圆满。

从一个比较简单的问题出发的,需求如下:

将源表的name列的字符串拆分成一个个字符,不使用union all等语法,因为数据量大了之后扩展性不好。

另外尽量使用一条sql语句,尽量简洁、高效


sql server 列拆分为行 sql 分隔符拆分多行_字符串拆分

--小数据量+短字符串的情况
select * from t;

ID	NAME
1	a
2	bb
3	ccc

select id, split_chr
  from (select t.id,
               t.name,
               sys_connect_by_path(id, ',') path,
               regexp_substr(t.name, '.', 1, level) split_chr
          from t
         where regexp_substr(t.name, '.', 1, level) is not null
        connect by level <= 3)
 where regexp_like(path, '^(,.)\1{0,2}$');

ID	SPLIT_CHR
1	a
2	b
2	b
3	c
3	c
3	c

以上sql解释请参考上篇文章,
虽然解决了上面的简化问题,但是还是存在一个最大的问题,这种写法不能扩展,当记录数稍微增多,字符串长度稍微增长,那就是无尽的等待。

上面写法的致命缺陷是没有限制条件(prior xx=yy)的connect by,会导致巨大的连接。

忽然间想到,用connect by的用意其实就是复制记录,把一个字符串长度为n的行复制出n行出来,然后结合level来截取即可。

一直盯着层次查询、正则表达式,反而忽视了这个最重要的一点。

基于以上分析,使用以下写法,扩展性也不错,我使用了100万条记录测试,每条字符串长度为26。基本上无需等待。

具体尝试如下:

--字符串拆分为字符
drop table x purge;

create table x as 
select level id, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' name
  from dual
connect by level <= 1000000;

select id,regexp_substr(x.name, '.', 1, y.lv) split_chr
  from x,
       (select level lv
          from dual
        connect by level <=
                   (select max(length(name)) level_depth_max from x)) y
 where length(x.name) >= y.lv;

--符号分隔的字符串拆分(以逗号分隔为例) 
drop table x purge;

create table x as 
select level id, 'A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z' name
  from dual
connect by level <= 1000000;
 
select id,regexp_substr(x.name, '[^,]+', 1, y.lv) split_chr
  from x,
       (select level lv
          from dual
        connect by level <=
                   (select max(regexp_count(name, '[^,]+')) level_depth_max from x)) y --此行下面有说明
 where regexp_count(name, '[^,]+') >= y.lv;

说明:
这一行的查询做了一次全表扫描,目的是获取需要拆分列的实际最大长度,如果全表扫描代价太大,可以尝试直接赋值为本列的宽度。
--在另外一个地方看到这么一个写法,很有趣,使用(prior dbms_random.value is not null)消除了循环,也就是把我们上面讨论的(小数据量+短字符串的情况)之所以不能扩展的巨大的连接给消除了,所以也能支持大数据量和长字符串的拆分了
--网址:http://www.itpub.net/thread-1803498-1-1.html 见网友hh7yx的回复
--有兴趣可以比较下两种写法的执行计划和效率

drop table x purge;

create table x as 
select level id, 'A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z' name
  from dual
connect by level <= 1000000;

select id, regexp_substr(x.name, '[^,]+', 1, level) split_chr
  from x
connect by id = prior id
       and prior dbms_random.value is not null
       and level <= regexp_count(name, '[^,]+');

总结,虽然可以通过pl/sql大而化小,或者通过编写函数处理,但是通过系统自带的函数和语法解决问题看起来还是非常简洁的。