關鍵字:SQL,CTE,遞歸查詢
概述:通常遞歸查詢是一個有難度的話題,盡管如此,它們仍使您能夠完成在 SQL 中無法實現的操作。本文通過示例進行了簡單介紹,並展示了與 PL/SQL的遞歸查詢實現的差異。
一、公用表表達式(WITH子句)
公用表表達式(CTE)可以被看作是一個視圖,只適用於一個單一的查詢:
WITH ctename AS ( SELECT ... ) SELECT ... FROM ctename ...
這也可以寫成 中的子查詢FROM,但使用 CTE 有一些優點:
- 查詢變得更具可讀性。
- 您可以在查詢中多次引用 CTE,並且只會計算一次。
- 您可以在 CTE 中使用數據修改語句(通常帶有RETURNING子句)。
請注意,在 V8R3 ,總是物化 CTE。這意味着,CTE 是獨立於包含查詢計算的。從 V8R6 開始,CTE 可以“內聯”到查詢中,這提供了進一步的優化潛力。
二、遞歸查詢的語法
遞歸查詢是使用遞歸 CTE編寫的,即包含RECURSIVE關鍵字的CTE :
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將停止處理。請注意,此技術不可移植到其他符合標准的數據庫。
請看實際執行計划:
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)
四、一個簡單的例子
讓我們假設一個像這樣的自引用表
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 的所有下屬,包括人員本身。查詢的非遞歸分支將是:
SELECT empno, ename FROM emp WHERE empno = 7566;
遞歸分支會找到WorkTable中所有條目的所有下級:
SELECT emp.empno, emp.ename FROM emp JOIN ctename ON emp.mgr = ctename.empno;
可以假設依賴項不包含循環(沒有人是他或她自己的經理,直接或間接)。所以可以將查詢與 UNION ALL 結合起來,因為不會發生重復。所以完整查詢將是:
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 添加到級別:
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會使之前相同的行不同。但在那種情況下,分層級別無論如何都沒有多大意義,因為一個條目可能出現在無限多個級別上。
另一個常見的要求是收集“路徑”中的所有祖先:
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 標准的遞歸查詢有不同的語法。原始示例如下所示:
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,它計算斐波那契數列的第一個元素:
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