CSP-S 2021 遊記
概述:通常遞迴查詢是一個有難度的話題,儘管如此,它們仍使您能夠完成在 SQL 中無法實現的操作。本文通過示例進行了簡單介紹,並展示了與 PL/SQL的遞迴查詢實現的差異。
一、公用表表達式(WITH子句)
公用表表達式(CTE)可以被看作是一個檢視,只適用於一個單一的查詢:
1 2 3 4 5 |
WITH ctename AS (
SELECT ...
)
SELECT ...
FROM ctename ...
|
這也可以寫成 中的子查詢FROM,但使用 CTE 有一些優點:
- 查詢變得更具可讀性。
- 您可以在查詢中多次引用 CTE,並且只會計算一次。
- 您可以在 CTE 中使用資料修改語句(通常帶有RETURNING子句)。
請注意,在 V8R3 ,總是物化 CTE。這意味著,CTE 是獨立於包含查詢計算的。從 V8R6 開始,CTE 可以“內聯”到查詢中,這提供了進一步的優化潛力。
二、遞迴查詢的語法
遞迴查詢是使用遞迴 CTE編寫的,即包含RECURSIVE關鍵字的CTE :
1 2 3 4 5 6 7 |
WITH RECURSIVE ctename AS (
SELECT /* non-recursive branch, cannot reference "ctename" */
UNION [ALL]
SELECT /* recursive branch referencing "ctename" */ )
SELECT ...
FROM ctename ...
|
三、如何處理遞迴查詢
KingbaseES內部使用 WorkTable 來處理遞迴 CTE。這種處理並不是真正的遞迴,而是迭代:
首先,通過執行 CTE 的非遞迴分支來初始化WorkTable。CTE 的結果也用這個結果集初始化。如果遞迴 CTE 使用UNION而不是UNION ALL,則刪除重複的行。
然後,KingbaseES重複以下操作,直到WorkTable為空:
- 評估 CTE 的遞迴分支,用WorkTable替換對 CTE 的引用。
- 將所有結果行新增到 CTE 結果。如果UNION用於合併分支,則丟棄重複的行。
- 用上一步中的所有新行替換WorkTable(不包括任何已刪除的重複行)。
請注意,到目前為止,CTE的自引用分支並未使用完整的 CTE 結果執行,而是僅使用自上次迭代(WorkTable)以來的新行。
必須意識到這裡無限迴圈的危險:如果迭代永遠不會結束,查詢將一直執行直到結果表變得足夠大以導致錯誤。有兩種方法可以處理:
- 通常,您可以通過使用 UNION來避免無限遞迴,這會刪除重複的結果行(但當然需要額外的處理工作)。
- 另一種方法是LIMIT在使用 CTE 的查詢上放置一個子句,因為如果遞迴 CTE 計算的行數與父查詢獲取的行數一樣多,KingbaseES將停止處理。請注意,此技術不可移植到其他符合標準的資料庫。
請看實際執行計劃:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
test=# explain WITH RECURSIVE ctename AS (
test(# SELECT empno, ename
test(# FROM emp
test(# WHERE empno = 7566
test(# UNION ALL
test(# SELECT emp.empno, emp.ename
test(# FROM emp JOIN ctename ON emp.mgr = ctename.empno
test(# )
test-# SELECT * FROM ctename;
--------------------------------------------------------------------------------------------------
CTE Scan on ctename (cost= 417.62 .. 489.74 rows= 3606 width= 36 )
CTE ctename
-> Recursive Union (cost= 0.00 .. 417.62 rows= 3606 width= 36 )
-> Seq Scan on emp (cost= 0.00 .. 25.00 rows= 6 width= 36 )
Filter: (empno = 7566 )
-> Hash Join (cost= 1.95 .. 32.05 rows= 360 width= 36 )
Hash Cond: (emp_1.mgr = ctename_1.empno)
-> Seq Scan on emp emp_1 (cost= 0.00 .. 22.00 rows= 1200 width= 40 )
-> Hash (cost= 1.20 .. 1.20 rows= 60 width= 4 )
-> WorkTable Scan on ctename ctename_1 (cost= 0.00 .. 1.20 rows= 60 width= 4 )
|
四、一個簡單的例子
讓我們假設一個像這樣的自引用表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
TABLE emp;
empno | ename | job | mgr | hiredate | sal | comm | deptno
-------+--------+-----------+------+------------+---------+---------+--------
7839 | KING | PRESIDENT | | 1981 - 11 - 17 | 5000.00 | | 10
7698 | BLAKE | MANAGER | 7839 | 1981 - 05 - 01 | 2850.00 | | 30
7782 | CLARK | MANAGER | 7839 | 1981 - 06 - 09 | 2450.00 | | 10
7566 | JONES | MANAGER | 7839 | 1981 - 04 - 02 | 2975.00 | | 20
7902 | FORD | ANALYST | 7566 | 1981 - 12 - 03 | 3000.00 | | 20
7369 | SMITH | CLERK | 7902 | 1980 - 12 - 17 | 800.00 | | 20
7499 | ALLEN | SALESMAN | 7698 | 1981 - 02 - 20 | 1600.00 | 300.00 | 30
7521 | WARD | SALESMAN | 7698 | 1981 - 02 - 22 | 1250.00 | 500.00 | 30
7654 | MARTIN | SALESMAN | 7698 | 1981 - 09 - 28 | 1250.00 | 1400.00 | 30
7844 | TURNER | SALESMAN | 7698 | 1981 - 09 - 08 | 1500.00 | 0.00 | 30
7900 | JAMES | CLERK | 7698 | 1981 - 12 - 03 | 950.00 | | 30
7934 | MILLER | CLERK | 7782 | 1982 - 01 - 23 | 1300.00 | | 10
( 12 rows)
|
我們要查詢人員 7566 的所有下屬,包括人員本身。查詢的非遞迴分支將是:
1 2 3 |
SELECT empno, ename
FROM emp
WHERE empno = 7566 ;
|
遞迴分支會找到WorkTable中所有條目的所有下級:
1 2 |
SELECT emp.empno, emp.ename
FROM emp JOIN ctename ON emp.mgr = ctename.empno;
|
可以假設依賴項不包含迴圈(沒有人是他或她自己的經理,直接或間接)。所以可以將查詢與 UNION ALL 結合起來,因為不會發生重複。所以完整查詢將是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
WITH RECURSIVE ctename AS (
SELECT empno, ename
FROM emp
WHERE empno = 7566
UNION ALL
SELECT emp.empno, emp.ename
FROM emp JOIN ctename ON emp.mgr = ctename.empno
)
SELECT * FROM ctename;
empno | ename
-------+-------
7566 | JONES
7902 | FORD
7369 | SMITH
( 3 rows)
|
五、新增生成的列
有時您想新增更多資訊,例如層級。您可以通過將起始級別新增為非遞迴分支中的常量來實現。在遞迴分支中,您只需將 1 新增到級別:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
WITH RECURSIVE ctename AS (
SELECT empno, ename,
0 AS level
FROM emp
WHERE empno = 7566
UNION ALL
SELECT emp.empno, emp.ename,
ctename.level + 1
FROM emp
JOIN ctename ON emp.mgr = ctename.empno
)
SELECT * FROM ctename;
empno | ename | level
-------+-------+-------
7566 | JONES | 0
7902 | FORD | 1
7369 | SMITH | 2
( 3 rows)
|
如果UNION在迴圈引用的情況下使用避免重複行,則不能使用此技術。這是因為新增level會使之前相同的行不同。但在那種情況下,分層級別無論如何都沒有多大意義,因為一個條目可能出現在無限多個級別上。
另一個常見的要求是收集“路徑”中的所有祖先:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
WITH RECURSIVE ctename AS (
SELECT empno, ename,
ename AS path
FROM emp
WHERE empno = 7566
UNION ALL
SELECT emp.empno, emp.ename,
ctename.path || ' -> ' || emp.ename
FROM emp
JOIN ctename ON emp.mgr = ctename.empno
)
SELECT * FROM ctename;
empno | ename | path
-------+-------+------------------------
7566 | JONES | JONES
7902 | FORD | JONES -> FORD
7369 | SMITH | JONES -> FORD -> SMITH
|
六、與 PLSQL 的比較
PLSQL對於不符合 SQL 標準的遞迴查詢有不同的語法。原始示例如下所示:
1 2 3 4 5 6 7 8 9 10 |
SELECT empno, ename
FROM emp
START WITH empno = 7566
CONNECT BY PRIOR empno = mgr;
EMPNO ENAME
---------- ----------
7566 JONES
7902 FORD
7369 SMITH
|
這種語法更簡潔,但不如遞迴 CTE 強大。對於涉及連線的更復雜的查詢,它可能變得困難和混亂。將 PLSQL “分層查詢”轉換為遞迴 CTE 總是很容易的:
- 非遞迴分支是不帶CONNECT BY子句但包含START WITH子句的 Oracle 查詢。
- 遞迴分支是不帶START WITH子句但包含CONNECT BY子句的 Oracle 查詢。新增具有遞迴 CTE 名稱的PRIOR聯接,並用來自該聯接 CTE 的列替換所有列。
- 如果 Oracle 查詢使用CONNECT BY NOCYCLE,則使用UNION,否則使用UNION ALL。
一般把connect by語法稱為遞迴查詢,然而嚴格來說這是一個錯誤的叫法。因為它無法把當前層所計算得到的值傳遞到下一層,所以對它的稱呼都是Hierarchical Queries in Oracle (CONNECT BY) 。
七、遞迴查詢的真正實力
如果沒有遞迴 CTE,很多可以用過程語言編寫的東西就不能用 SQL 編寫。這通常影響資料庫的使用,因為 SQL 是用來查詢資料庫的。但是遞迴 CTE 使 SQL過程程式碼更完善,也就是說,它可以執行與任何其他程式語言相同的計算。前面的示例表明遞迴 CTE 可以完成您在 SQL 中無法執行的有用工作。
作為遞迴查詢功能的示例,這裡是一個遞迴 CTE,它計算斐波那契數列的第一個元素:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
WITH RECURSIVE t(n,last_n,cnt) AS (
SELECT 1 , 0 , 1 FROM DUAL
UNION ALL
SELECT t.n+t.last_n, t.n, t.cnt+ 1
FROM t
)
SELECT * FROM T limit 10
n | last_n | cnt
----+--------+-----
1 | 0 | 1
1 | 1 | 2
2 | 1 | 3
3 | 2 | 4
5 | 3 | 5
8 | 5 | 6
13 | 8 | 7
21 | 13 | 8
34 | 21 | 9
55 | 34 | 10
|