1. 程式人生 > 其它 >CSP-S 2021 遊記

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.74rows=3606width=36) CTE ctename -> Recursive Union (cost=0.00..417.62rows=3606width=36) -> Seq Scan on emp (cost=0.00..25.00rows=6width=36) Filter: (empno =7566) -> Hash Join (cost=1.95..32.05rows=360width=36) Hash Cond: (emp_1.mgr = ctename_1.empno) -> Seq Scan on emp emp_1 (cost=0.00..22.00rows=1200width=40) -> Hash (cost=1.20..1.20rows=60width=4) -> WorkTable Scan on ctename ctename_1 (cost=0.00..1.20rows=60width=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 (12rows)

我們要查詢人員 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 (3rows)

五、新增生成的列

有時您想新增更多資訊,例如層級。您可以通過將起始級別新增為非遞迴分支中的常量來實現。在遞迴分支中,您只需將 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, 0AS 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 (3rows)

如果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 ---------- ---------- 7566JONES 7902FORD 7369SMITH

這種語法更簡潔,但不如遞迴 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 ( SELECT1,0,1FROM DUAL UNION ALL SELECT t.n+t.last_n, t.n, t.cnt+1 FROM t ) SELECT * FROM T limit10 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