Monday, December 24, 2012

Recursive WITH clause - to implement Connect By features

ORACLE 11g release 2 introduced the recursive WITH clause as an alternative to the well known CONNECT BY clause. This blog entry shows how to implement all CONNECT BY features in Recursive WITH clause.

CONNECT BY clause to show all employees starting with KING (who has no manager [mgr IS NULL]):

rajesh@ORA11G> select empno,ename,mgr
  2  from emp
  3  start with mgr is null
  4  connect by prior empno = mgr;

     EMPNO ENAME             MGR
---------- ---------- ----------
      7839 KING
      7566 JONES            7839
      7788 SCOTT            7566
      7876 ADAMS            7788
      7902 FORD             7566
      7369 SMITH            7902
      7698 BLAKE            7839
      7499 ALLEN            7698
      7521 WARD             7698
      7654 MARTIN           7698
      7844 TURNER           7698
      7900 JAMES            7698
      7782 CLARK            7839
      7934 MILLER           7782

14 rows selected.

Elapsed: 00:00:00.01
rajesh@ORA11G>

Same result using the recursive WITH clause

rajesh@ORA11G> with r(empno,ename,mgr) as
  2  ( select empno,ename,mgr
  3    from emp
  4    where mgr is null
  5    union all
  6    select e.empno,e.ename,r.empno
  7    from emp e, r
  8    where e.mgr = r.empno )
  9  select * from r;

     EMPNO ENAME             MGR
---------- ---------- ----------
      7839 KING
      7566 JONES            7839
      7698 BLAKE            7839
      7782 CLARK            7839
      7499 ALLEN            7698
      7521 WARD             7698
      7654 MARTIN           7698
      7788 SCOTT            7566
      7844 TURNER           7698
      7900 JAMES            7698
      7902 FORD             7566
      7934 MILLER           7782
      7369 SMITH            7902
      7876 ADAMS            7788

14 rows selected.

Elapsed: 00:00:00.01
rajesh@ORA11G>

The first query block (anchor member) of the recursive WITH clause defines the root(s) of the hierarchy, the second the recursion. In the second block, we can see a join between the anchor member and the emp table which is pretty much the same as the CONNECT BY clause in the traditional approach.

The CONNECT BY clause has a dedicated ORDER BY clause (ORDER SIBLINGS BY) to sort the elements of a hierarchy.

rajesh@ORA11G> select level lvl, rpad(' ',2*level)||ename enames,empno,mgr
  2  from emp
  3  start with mgr is null
  4  connect by prior empno = mgr
  5  order siblings by ename;

       LVL ENAMES                    EMPNO        MGR
---------- -------------------- ---------- ----------
         1   KING                     7839
         2     BLAKE                  7698       7839
         3       ALLEN                7499       7698
         3       JAMES                7900       7698
         3       MARTIN               7654       7698
         3       TURNER               7844       7698
         3       WARD                 7521       7698
         2     CLARK                  7782       7839
         3       MILLER               7934       7782
         2     JONES                  7566       7839
         3       FORD                 7902       7566
         4         SMITH              7369       7902
         3       SCOTT                7788       7566
         4         ADAMS              7876       7788

14 rows selected.

Elapsed: 00:00:00.01
rajesh@ORA11G>

Same result can be achieved using the recursive WITH clause with the SEARCH clause

rajesh@ORA11G>
rajesh@ORA11G> with r(empno,ename,mgr,lvl) as
  2  ( select empno,ename,mgr,1 as lvl
  3    from emp
  4    where mgr is null
  5    union all
  6    select e.empno, e.ename, e.mgr, lvl+1
  7    from emp e, r
  8    where e.mgr = r.empno
  9  )
 10  search depth first by ename set ord1
 11  select lvl, rpad(' ',2*lvl)||ename as enames, empno,mgr
 12  from r
 13  order by ord1;

       LVL ENAMES                    EMPNO        MGR
---------- -------------------- ---------- ----------
         1   KING                     7839
         2     BLAKE                  7698       7839
         3       ALLEN                7499       7698
         3       JAMES                7900       7698
         3       MARTIN               7654       7698
         3       TURNER               7844       7698
         3       WARD                 7521       7698
         2     CLARK                  7782       7839
         3       MILLER               7934       7782
         2     JONES                  7566       7839
         3       FORD                 7902       7566
         4         SMITH              7369       7902
         3       SCOTT                7788       7566
         4         ADAMS              7876       7788

14 rows selected.

Elapsed: 00:00:00.02
rajesh@ORA11G>

SEARCH DEPTH FIRST will show the children before the siblings whereas SEARCH BREADTH FIRST would show the siblings before the children.

CONNECT BY clause knows the CONNECT_BY_ROOT operator which returns the root(s) of a hierarchy. Furthermore the SYS_CONNECT_BY_PATH function may be used to get a path from the root to the current element within the hierarchy.

rajesh@ORA11G> select ename, empno, mgr,
  2     sys_connect_by_path(ename,'/') path,
  3     connect_by_root ename
  4  from emp
  5  start with mgr is null
  6  connect by prior empno = mgr ;

ENAME           EMPNO        MGR PATH                           CONNECT_BY
---------- ---------- ---------- ------------------------------ ----------
KING             7839            /KING                          KING
JONES            7566       7839 /KING/JONES                    KING
SCOTT            7788       7566 /KING/JONES/SCOTT              KING
ADAMS            7876       7788 /KING/JONES/SCOTT/ADAMS        KING
FORD             7902       7566 /KING/JONES/FORD               KING
SMITH            7369       7902 /KING/JONES/FORD/SMITH         KING
BLAKE            7698       7839 /KING/BLAKE                    KING
ALLEN            7499       7698 /KING/BLAKE/ALLEN              KING
WARD             7521       7698 /KING/BLAKE/WARD               KING
MARTIN           7654       7698 /KING/BLAKE/MARTIN             KING
TURNER           7844       7698 /KING/BLAKE/TURNER             KING
JAMES            7900       7698 /KING/BLAKE/JAMES              KING
CLARK            7782       7839 /KING/CLARK                    KING
MILLER           7934       7782 /KING/CLARK/MILLER             KING

14 rows selected.

Elapsed: 00:00:00.01
rajesh@ORA11G>

Same result using the recursive WITH clause

rajesh@ORA11G>
rajesh@ORA11G> with r(ename,empno,mgr,path,by_root) as
  2  ( select ename,empno,mgr,
  3     '/'||ename as path,
  4     ename as by_root
  5    from emp
  6    where mgr is null
  7    union all
  8    select e.ename, e.empno, e.mgr,
  9      r.path||'/'||e.ename as path,
 10      r.by_root
 11    from emp e, r
 12    where e.mgr = r.empno )
 13  select ename,empno,mgr,path,by_root from r ;

ENAME           EMPNO        MGR PATH                           BY_ROOT
---------- ---------- ---------- ------------------------------ ----------
KING             7839            /KING                          KING
JONES            7566       7839 /KING/JONES                    KING
BLAKE            7698       7839 /KING/BLAKE                    KING
CLARK            7782       7839 /KING/CLARK                    KING
ALLEN            7499       7698 /KING/BLAKE/ALLEN              KING
WARD             7521       7698 /KING/BLAKE/WARD               KING
MARTIN           7654       7698 /KING/BLAKE/MARTIN             KING
SCOTT            7788       7566 /KING/JONES/SCOTT              KING
TURNER           7844       7698 /KING/BLAKE/TURNER             KING
JAMES            7900       7698 /KING/BLAKE/JAMES              KING
FORD             7902       7566 /KING/JONES/FORD               KING
MILLER           7934       7782 /KING/CLARK/MILLER             KING
SMITH            7369       7902 /KING/JONES/FORD/SMITH         KING
ADAMS            7876       7788 /KING/JONES/SCOTT/ADAMS        KING

14 rows selected.

Elapsed: 00:00:00.03
rajesh@ORA11G>

If the data contains a cylce the query would run indefinitely. ORACLE detects these situations and lets the query fail.

rajesh@ORA11G>
rajesh@ORA11G> UPDATE emp
  2  SET mgr = 7499
  3  WHERE empno = 7839 ;

1 row updated.

Elapsed: 00:00:00.00
rajesh@ORA11G>
rajesh@ORA11G> select level, empno,ename
  2  from emp
  3  start with empno = 7839
  4  connect by prior empno = mgr;
ERROR:
ORA-01436: CONNECT BY loop in user data



no rows selected

Elapsed: 00:00:00.01
rajesh@ORA11G>

Recursive suquery factoring throws a different error code with a similar message

rajesh@ORA11G>
rajesh@ORA11G> with r(lvl,empno,ename) as
  2  ( select 1 as lvl, empno, ename
  3    from emp
  4    where empno = 7839
  5    union all
  6    select lvl+1, e.empno, e.ename
  7    from emp e, r
  8    where e.mgr = r.empno )
  9  select * from r ;
ERROR:
ORA-32044: cycle detected while executing recursive WITH query



no rows selected

Elapsed: 00:00:00.01
rajesh@ORA11G>


Since ORACLE 10g the CONNECT BY clause knows the NOCYLCE attribute as well as the CONNECT_BY_ISCYCLE pseudo column to ignore cycles 

rajesh@ORA11G> select level, empno,ename,mgr,connect_by_iscycle,
  2     sys_connect_by_path(ename,'/') path
  3  from emp
  4  start with empno = 7839
  5  connect by nocycle prior empno = mgr;

     LEVEL      EMPNO ENAME             MGR CONNECT_BY_ISCYCLE PATH
---------- ---------- ---------- ---------- ------------------ -----------------------------
         1       7839 KING             7499                  0 /KING
         2       7566 JONES            7839                  0 /KING/JONES
         3       7788 SCOTT            7566                  0 /KING/JONES/SCOTT
         4       7876 ADAMS            7788                  0 /KING/JONES/SCOTT/ADAMS
         3       7902 FORD             7566                  0 /KING/JONES/FORD
         4       7369 SMITH            7902                  0 /KING/JONES/FORD/SMITH
         2       7698 BLAKE            7839                  0 /KING/BLAKE
         3       7499 ALLEN            7698                  1 /KING/BLAKE/ALLEN
         3       7521 WARD             7698                  0 /KING/BLAKE/WARD
         3       7654 MARTIN           7698                  0 /KING/BLAKE/MARTIN
         3       7844 TURNER           7698                  0 /KING/BLAKE/TURNER
         3       7900 JAMES            7698                  0 /KING/BLAKE/JAMES
         2       7782 CLARK            7839                  0 /KING/CLARK
         3       7934 MILLER           7782                  0 /KING/CLARK/MILLER

14 rows selected.

Elapsed: 00:00:00.00
rajesh@ORA11G>
The Recursive WITH clause also offers clause to handle this way.

rajesh@ORA11G>
rajesh@ORA11G> with r(lvl,empno,ename,mgr,path) as
  2  ( select 1 as lvl, empno, ename,
  3     mgr,'/'||ename as path
  4    from emp
  5    where empno = 7839
  6    union all
  7    select lvl+1, e.empno, e.ename,
  8     e.mgr,r.path||'/'||e.ename
  9    from emp e, r
 10    where e.mgr = r.empno )
 11  search depth first by ename set ord1
 12  cycle empno set y_cycle to 1 default 0
 13  select lvl,empno,ename,mgr,path,y_cycle from r ;

       LVL      EMPNO ENAME             MGR PATH                           Y
---------- ---------- ---------- ---------- ------------------------------ -
         1       7839 KING             7499 /KING                          0
         2       7698 BLAKE            7839 /KING/BLAKE                    0
         3       7499 ALLEN            7698 /KING/BLAKE/ALLEN              0
         4       7839 KING             7499 /KING/BLAKE/ALLEN/KING         1
         3       7900 JAMES            7698 /KING/BLAKE/JAMES              0
         3       7654 MARTIN           7698 /KING/BLAKE/MARTIN             0
         3       7844 TURNER           7698 /KING/BLAKE/TURNER             0
         3       7521 WARD             7698 /KING/BLAKE/WARD               0
         2       7782 CLARK            7839 /KING/CLARK                    0
         3       7934 MILLER           7782 /KING/CLARK/MILLER             0
         2       7566 JONES            7839 /KING/JONES                    0
         3       7902 FORD             7566 /KING/JONES/FORD               0
         4       7369 SMITH            7902 /KING/JONES/FORD/SMITH         0
         3       7788 SCOTT            7566 /KING/JONES/SCOTT              0
         4       7876 ADAMS            7788 /KING/JONES/SCOTT/ADAMS        0

15 rows selected.

Elapsed: 00:00:00.03
rajesh@ORA11G>


The difference between CONNECT BY and RECURSIVE WITH cycle detection is that with RECURSIVE WITH the cycle is decteted after the next recursion level wass processed. The erroneous node is repeated and the cycle flag is set one level lower than the CONNECT_BY_ISCYCLE pseudo column.