最近处理了一个connect by的case,分享给大家一下。
 
 首先介绍一下connect by作用:对于数据有着严密的层级关系的表,我们有时候希望能够把有着
 
 父子关系或者叫上下级关系的数据一次性展现出来,这个时候传统的sql 语法并不能就解决问题,
 
 例如一个部门有一个总经理,多个副经理,每个下面又有多个总监,总监下面是员工,
 
 我们设计表的时候,肯定只有一个字段来记录员工的上级,并不会记录他的上上级,那么我们
 
 想把某个副经理的下面的所有员工都列出来的时候,就存在递归查找底层员工的情况,这种
 
 就需要用到递归遍历,不同的DB给出了不同的解决办法,如DB2可以使用with+内嵌递归逻辑的
 
 sql实现,oracle 提供了connect by的语法来实现。下面简单的看个例子:
 
 drop table emp;
 
 create table emp( emp_no number , manager_no number, name varchar2(100));
 
 insert into emp values(1,,’总经理’);
 
 insert into emp values(2,1,’副经理1′);
 
 insert into emp values(3,1,’副经理2′);
 
 insert into emp values(4,2,’总监1′);
 
 insert into emp values(5,3,’总监2′);
 
 insert into emp values(6,4,’员工6′);
 
 insert into emp values(7,4,’员工7′);
 
 insert into emp values(8,5,’员工8′);
 
 insert into emp values(9,5,’员工9′); 
 
 commit;
 
 1个总经理两个副经理,每个副经理都有一个总监,每个总监都有两个员工。
 
 下面列出副总1的所有员工情况:
 
 select * from emp   start with emp_no=2
 
 connect by prior emp_no=manager_no
 
 2 1 副经理1  <<起始行
 
 4 2 总监1
 
 6 4 员工6
 
 7 4 员工7
 
 这就sql的原理就是,start with标识递归遍历的起点,从emp_no=2这条记录开始遍历,prior 标识子节点和父节点关系,
 
 可以理解成这个是一个函数,传入参数字段求值就可以得到他的父节点,根据emp_no父亲是在manager_no里面记录这种父子关系
 
 向下遍历,那么就找第二条总监1,进而递归遍历找到员工6,7两条记录。
 
 我们都知掉,任何编程语言递归遍历可能就存在死循环的情况,类似C语言中的goto滥用就会出现死循环一样,oracle在解决
 
 这种递归的时候一旦发现死循环就会报错,例如下面的例子:
 
 insert into emp values(2,7,’副经理1′);  <<我们在插入一条让其副经理1是下面总监1的员工7的下属(这显然是矛盾的)
 
 SQL>insert into emp values(2,7,’副经理1′);
 
 select * from emp   start with emp_no=2
 
 connect by prior emp_no=manager_no
 
 1 row created.
 
 SQL> SQL>   2  ;
 
 ERROR:
 
 ORA-01436: CONNECT BY loop in user data 
 
 no rows selected
 
 从上面看,的确发现了死循环,因为按照上面的逻辑,先列出前4条记录
 
 2 1 副经理1  <<起始行
 
 4 2 总监1
 
 6 4 员工6
 
 7 4 员工7   <<前4行记录
 
 然后记录遍历:发现7的下级记录
 
 2 7 副经理1′
 
 然后在找2的下级记录,又发现
 
 4 2 总监1 
 
 进而找到6,7,这样就无限的循环了。
 
 那么若是因为突然插入这一条违背逻辑的数据就造成SQL报错,程序异常就没有办法补救了么,例如很显然,
 
 我们就在死循环前停止,前面正常的数据正常输出即可,其实oracle的确已经帮我们想到了这个问题,我们可以
 
 通过nocycle关键字来规避,这个参数的意思就是不要进入死循环,进入前停止。下面看看添加后的效果
 
 select * from emp   start with emp_no=2
 
 connect by nocycle prior emp_no=manager_no
 
 2 1 副经理1  
 
 4 2 总监1
 
 6 4 员工6
 
 7 4 员工7  结果和我们期望的一致,显示正常的结果。
 
 有了上述基础知识之后,我们再来看一下另外一个真实的case
 
 drop table t1;
 
 create table t1 (id int, fst_child int, snd_child int);
 
 –base data
 
 insert into t1 values (1, 2, NULL);
 
 insert into t1 values (2, 3, 4);
 
 insert into t1 values (3, 5, NULL);
 
 insert into t1 values (4, 5, NULL);
 
 insert into t1 values (5, 6, NULL);
 
 insert into t1 values (6, 2, NULL);
 
 SELECT id, fst_child, nvl(snd_child,99999) snd_child, connect_by_iscycle, SYS_CONNECT_BY_PATH(id, ‘/’) “Path”
 
 from t1
 
 start with id = 1
 
 connect by nocycle ((id = prior fst_child) or (id = prior snd_child));
 
 这个结果返回如下:
 
 id  fst_child   snd_child connect_by_iscycle   Path
 
 1     2          99999      0         /1  
 
 2     3          4              0         /1/2  
 
 3     5          99999      0         /1/2/3  
 
 5     6          99999      1         /1/2/3/5 
 
 4     5          99999      0         /1/2/4  
 
 5     6          99999      1         /1/2/4/5  
 
 客户发现当然把最后一条记录 由2变成4 之后
 
 insert into t1 values (6, 4, NULL);
 
 数据变成如下:
 
 id  fst_child   snd_child connect_by_iscycle   Path
 
 1     2    99999      0          /1 
 
 2     3        4      0              /1/2 
 
 3     5    99999      0          /1/2/3  
 
 5     6    99999      0          /1/2/3/5 
 
 6     4    99999      1          /1/2/3/5/6  <<新多出记录
 
 4     5    99999      0          /1/2/4 
 
 5     6    99999      0          /1/2/4/5 
 
 6     4    99999      1          /1/2/4/5/6 <<新多出记录
 
 结果多出两条记录出来,从上述知识来看最后一条无论是2还是4都是出现了死循环所以,造成了connect_by_iscycle返回1,
 
 遍历停止,那么为啥后者能遍历出6,4这条记录,而前者不能遍历出6,2呢?因为2是6的祖先,4也是6的祖先,为啥
 
 停止位置不同呢?难道是遇到了bug?经过在11204和12.2两个最稳定版本上测试,效果都是一样,
 
 客户版本是11201,所以初步排除了bug的可能性,因为wrong result这种高优先级bug是不太可能跨越这两个版本仍然没有fix的。
 
 那原因只能是一个,也就是这个结果是合理的。
 
 再来重温一下connect_by_iscycle的解释:
 
 The CONNECT_BY_ISCYCLE pseudocolumn returns 1 if the current row has a child which is also its ancestor. Otherwise it returns 0.
 
 如果当前行含有一个child,而且这个child又是他的祖先,那么就返回1,否则返回0.
 
 我们来看一下第一个场景,
 
 insert into t1 values (1, 2, NULL);
 
 insert into t1 values (2, 3, 4);
 
 insert into t1 values (3, 5, NULL);
 
 insert into t1 values (4, 5, NULL);
 
 insert into t1 values (5, 6, NULL);
 
 insert into t1 values (6, 2, NULL);
 
 当走到(5,6) 的时候 也就是child节点6的时候,发现有child 行(6,2),而这行表名2是6的子节点,但是根据
 
 之前的遍历,2是3的祖先,3是5的祖先,5是6的祖先,自然2是6的祖先,所以connect_by_iscycle返回1,这个是合情合理。
 
 再来看一下第二个场景
 
 insert into t1 values (1, 2, NULL);
 
 insert into t1 values (2, 3, 4);
 
 insert into t1 values (3, 5, NULL);
 
 insert into t1 values (4, 5, NULL);
 
 insert into t1 values (5, 6, NULL);
 
 insert into t1 values (6, 4, NULL);
 
 当走到(5,6) 的时候 也就是child节点6的时候,发现有child 行(6,4),而这行表名4是6的子节点,但是根据第四行(4,5)
 
 5是4的子节点,6是5的子节点,显然6也是4的子节点,也违反逻辑,也应该connect_by_iscycle返回1才对,这个是为什么?
 
 其实这里面存在一个误导,我们在考虑这个递归逻辑的时候是看到了所有数据,但是计算机是严格的一步一步递归的,也就是
 
 当走到(5,6)的时候会,的确会进一步check下一行(6,4)情况,发现4是6的子节点,但是这个时候他是看不到(4,5)这一行的
 
 所以他不知道5是4的子节点,所以他认为(6,4)是有效的,所以进行了输出,然后check他的子节点(4,5)发现5是4的子节点,
 
 这个就有问题了,因为之前遍历的时候知道,5是6的祖先,6是4的祖先,所以5一定是4的祖先,所以进入时候死循环,那么本记录
 
 标记成connect_by_iscycle=1,遍历停止,所以本场景会多遍历一层,这也就是为啥和场景1不同的原因。
