1. 程式人生 > >oracle遞迴with

oracle遞迴with

簡介

遞迴with(Recursive WITH Clauses)是一個主要用於層次查詢(Hierarchical Queries)的語法。要使用它,需要oracle的版本為Oracle 11g Release 2及以上。這個語法可以看成是對connect語法的補充。

基本語法

建立測試資料

DROP TABLE tab1 PURGE;

CREATE TABLE tab1 (
  id        NUMBER,
  parent_id NUMBER,
  CONSTRAINT tab1_pk PRIMARY KEY (id),
  CONSTRAINT
tab1_tab1_fk FOREIGN KEY (parent_id) REFERENCES tab1(id) );
CREATE INDEX tab1_parent_id_idx ON tab1(parent_id); INSERT INTO tab1 VALUES (1, NULL); INSERT INTO tab1 VALUES (2, 1); INSERT INTO tab1 VALUES (3, 2); INSERT INTO tab1 VALUES (4, 2); INSERT INTO tab1 VALUES (5, 4); INSERT INTO tab1 VALUES
(6, 4);
INSERT INTO tab1 VALUES (7, 1); INSERT INTO tab1 VALUES (8, 7); INSERT INTO tab1 VALUES (9, 1); INSERT INTO tab1 VALUES (10, 9); INSERT INTO tab1 VALUES (11, 10); INSERT INTO tab1 VALUES (12, 9); COMMIT;
ID PARENT_ID
1
2 1
3 2
4 2
5 4
6 4
7 1
8 7
9 1
10 9
11 10
12 9

這是一個有父子關係的表。
傳統的寫法是這樣的

select *
  from tab1 t1
 start with t1.parent_id is null
connect by prior t1.id = t1.parent_id;
ID PARENT_ID
1
2 1
3 2
4 2
5 4
6 4
7 1
8 7
9 1
10 9
11 10
12 9

現在,我們有了一個新的寫法。

with t1(id, parent_id) as (

select*from tab1 t0 where t0.parent_id is null  -- Anchor member.

union all

select t2.id, t2.parent_id from tab1 t2, t1  -- Recursive member.
where t2.parent_id = t1.id
)

select*from t1;
ID PARENT_ID
1
2 1
7 1
9 1
3 2
4 2
8 7
10 9
12 9
5 4
6 4
11 10

來自官方的解釋

Basic Hierarchical Query

A recursive subquery factoring clause must contain two query blocks combined by a UNION ALL set operator. The first block is known as the anchor member, which can not reference the query name. It can be made up of one or more query blocks combined by the UNION ALL, UNION, INTERSECT or MINUS set operators. The second query block is known as the recursive member, which must reference the query name once.

The following query uses a recursive WITH clause to perform a tree walk. The anchor member queries the root nodes by testing for records with no parents. The recursive member successively adds the children to the root nodes.

這真是個很奇葩的語法。union all不是一個合併結果集的語法,而是成為了一個構成語法的關鍵詞。

with中的語句必須是由union all分開的兩部分。第一部分的作用是確定根節點,這一部分不會參與到遞迴當中。它相當於start with。第二部分可以接收來自上一層級的資料。相當於connect之後的語句。

結果集順序

The ordering of the rows is specified using the SEARCH clause, which can use two methods.

BREADTH FIRST BY : Sibling rows are returned before child rows are processed.
DEPTH FIRST BY : Child rows are returned before siblings are processed.

根據剛才的例子可以看出,預設的排序是廣度優先的。如果要改成深度優先,需要這麼寫

WITH t1(id, parent_id) AS (
  -- Anchor member.
  SELECT id,
         parent_id
  FROM   tab1
  WHERE  parent_id IS NULL
  UNION ALL
  -- Recursive member.
  SELECT t2.id,
         t2.parent_id
  FROM   tab1 t2, t1
  WHERE  t2.parent_id = t1.id
)
SEARCH DEPTH FIRST BY id SET order1
SELECT id,
       parent_id
FROM   t1
ORDER BY order1;

與connect相關語法的等效替換

LEVEL

WITH t1(id, parent_id, lvl) AS (
  -- Anchor member.
  SELECT id,
         parent_id,
         1 AS lvl
  FROM   tab1
  WHERE  parent_id IS NULL
  UNION ALL
  -- Recursive member.
  SELECT t2.id,
         t2.parent_id,
         lvl+1
  FROM   tab1 t2, t1
  WHERE  t2.parent_id = t1.id
)
SEARCH DEPTH FIRST BY id SET order1
SELECT id,
       parent_id,
       RPAD('.', (lvl-1)*2, '.') || id AS tree,
       lvl
FROM t1
ORDER BY order1;
ID PARENT_ID TREE LVL
1 1 1
2 1 ..2 2
3 2 ….3 3
4 2 ….4 3
5 4 ……5 4
6 4 ……6 4
7 1 ..7 2
8 7 ….8 3
9 1 ..9 2
10 9 ….10 3
11 10 ……11 4
12 9 ….12 3

CONNECT_BY_ROOT

WITH t1(id, parent_id, lvl, root_id) AS (
  -- Anchor member.
  SELECT id,
         parent_id,
         1 AS lvl,
         id AS root_id
  FROM   tab1
  WHERE  parent_id IS NULL
  UNION ALL
  -- Recursive member.
  SELECT t2.id,
         t2.parent_id,
         lvl+1,
         t1.root_id
  FROM   tab1 t2, t1
  WHERE  t2.parent_id = t1.id
)
SEARCH DEPTH FIRST BY id SET order1
SELECT id,
       parent_id,
       RPAD('.', (lvl-1)*2, '.') || id AS tree,
       lvl,
       root_id
FROM t1
ORDER BY order1;
ID PARENT_ID TREE LVL ROOT_ID
1 1 1 1
2 1 ..2 2 1
3 2 ….3 3 1
4 2 ….4 3 1
5 4 ……5 4 1
6 4 ……6 4 1
7 1 ..7 2 1
8 7 ….8 3 1
9 1 ..9 2 1
10 9 ….10 3 1
11 10 ……11 4 1
12 9 ….12 3 1

SYS_CONNECT_BY_PATH

WITH t1(id, parent_id, lvl, root_id, path) AS (
  -- Anchor member.
  SELECT id,
         parent_id,
         1 AS lvl,
         id AS root_id,
         TO_CHAR(id) AS path
  FROM   tab1
  WHERE  parent_id IS NULL
  UNION ALL
  -- Recursive member.
  SELECT t2.id,
         t2.parent_id,
         lvl+1,
         t1.root_id,
         t1.path || '-' || t2.id AS path
  FROM   tab1 t2, t1
  WHERE  t2.parent_id = t1.id
)
SEARCH DEPTH FIRST BY id SET order1
SELECT id,
       parent_id,
       RPAD('.', (lvl-1)*2, '.') || id AS tree,
       lvl,
       root_id,
       path
FROM t1
ORDER BY order1;
ID PARENT_ID TREE LVL ROOT_ID PATH
1 1 1 1 1
2 1 ..2 2 1 1-2
3 2 ….3 3 1 1-2-3
4 2 ….4 3 1 1-2-4
5 4 ……5 4 1 1-2-4-5
6 4 ……6 4 1 1-2-4-6
7 1 ..7 2 1 1-7
8 7 ….8 3 1 1-7-8
9 1 ..9 2 1 1-9
10 9 ….10 3 1 1-9-10
11 10 ……11 4 1 1-9-10-11
12 9 ….12 3 1 1-9-12

NOCYCLE and CONNECT_BY_ISCYCLE

UPDATE tab1 SET parent_id = 9 WHERE id = 1;
COMMIT;


WITH t1(id, parent_id, lvl, root_id, path) AS (
  -- Anchor member.
  SELECT id,
         parent_id,
         1 AS lvl,
         id AS root_id,
         TO_CHAR(id) AS path
  FROM   tab1
  WHERE  id = 1
  UNION ALL
  -- Recursive member.
  SELECT t2.id,
         t2.parent_id,
         lvl+1,
         t1.root_id,
         t1.path || '-' || t2.id AS path
  FROM   tab1 t2, t1
  WHERE  t2.parent_id = t1.id
)
SEARCH DEPTH FIRST BY id SET order1
SELECT id,
       parent_id,
       RPAD('.', (lvl-1)*2, '.') || id AS tree,
       lvl,
       root_id,
       path
FROM t1
ORDER BY order1;
     *
ERROR at line 27:
ORA-32044: cycle detected while executing recursive WITH query

如果不作處理的話,毫無疑問會報錯。

WITH t1(id, parent_id, lvl, root_id, path) AS (
  -- Anchor member.
  SELECT id,
         parent_id,
         1 AS lvl,
         id AS root_id,
         TO_CHAR(id) AS path
  FROM   tab1
  WHERE  id = 1
  UNION ALL
  -- Recursive member.
  SELECT t2.id,
         t2.parent_id,
         lvl+1,
         t1.root_id,
         t1.path || '-' || t2.id AS path
  FROM   tab1 t2, t1
  WHERE  t2.parent_id = t1.id
)
SEARCH DEPTH FIRST BY id SET order1
CYCLE id SET cycle TO 1 DEFAULT 0
SELECT id,
       parent_id,
       RPAD('.', (lvl-1)*2, '.') || id AS tree,
       lvl,
       root_id,
       path,
       cycle
FROM t1
ORDER BY order1;
ID PARENT_ID TREE LVL ROOT_ID PATH CYCLE
1 9 1 1 1 1 0
2 1 ..2 2 1 1-2 0
3 2 ….3 3 1 1-2-3 0
4 2 ….4 3 1 1-2-4 0
5 4 ……5 4 1 1-2-4-5 0
6 4 ……6 4 1 1-2-4-6 0
7 1 ..7 2 1 1-7 0
8 7 ….8 3 1 1-7-8 0
9 1 ..9 2 1 1-9 0
1 9 ….1 3 1 1-9-1 1
10 9 ….10 3 1 1-9-10 0
11 10 ……11 4 1 1-9-10-11 0
12 9 ….12 3 1 1-9-12 0

The NOCYCLE and CONNECT_BY_ISCYCLE functionality is replicated using the CYCLE clause. By specifying this clause, the cycle is detected and the recursion stops, with the cycle column set to the specified value, to indicate the row where the cycle is detected. Unlike the CONNECT BY NOCYCLE method, which stops at the row before the cycle, this method stops at the row after the cycle.

需要注意的是,遞迴with語法在檢測到迴圈後,依然會再向下遞迴一級,而connect語句則不會。

例子

select t1.*, level
  from tab1 t1
 start with t1.id = 1 
connect by nocycle prior t1.id = t1.parent_id;
ID PARENT_ID LEVEL
1 9 1
2 1 2
3 2 3
4 2 3
5 4 4
6 4 4
7 1 2
8 7 3
9 1 2
10 9 3
11 10 4
12 9 3

遞迴with語法對connect語法的改進

很多人都喜歡把connect語法稱為遞迴查詢,然而嚴格來說這是一個錯誤的叫法。因為它無法把當前層所計算得到的值傳遞到下一層。就連官方對它的稱呼都是Hierarchical Queries in Oracle (CONNECT BY)
而遞迴with則徹底改變了這個情況。它的名字都帶著遞迴 Recursive WITH Clauses

輾轉相除法求最大公約數,我覺得這是最能驗證遞迴能力的方法。它需要將兩個值交換並做一個取餘數的操作,再把這個結果傳遞到下一層中。
先用java簡單複習一下

    public static int gcd(int a, int b){
        return a % b == 0 ? b : gcd(b, a % b);
    }

用sql則需要這麼寫

with t1(id, a1, a2) as (
select 1, 176, 34
  from dual
union all
select id + 1, a2, mod(a1, a2)
  from t1
 where mod(a1, a2) > 0
)
select distinct first_value(t1.a2) over(order by t1.id desc) res from t1;

--2

為了方便理解,我把每一步的結果輸出一下

with t1(id, a1, a2) as (
select 1, 176, 34
  from dual
union all
select id + 1, a2, mod(a1, a2)
  from t1
 where mod(a1, a2) > 0
)
select t1.* from t1;
ID A1 A2
1 176 34
2 34 6
3 6 4
4 4 2

這是一個大幅增加sql適用範圍的語法,大牛們可以用這個來搞出不少騷操作。

參考資料

https://oracle-base.com/articles/11g/recursive-subquery-factoring-11gr2#setup