1. 程式人生 > 資料庫 >MySQL的SQL語句 - 資料操作語句(17)- WITH 語句(2)

MySQL的SQL語句 - 資料操作語句(17)- WITH 語句(2)

遞迴公共表表達式

遞迴公共表表達式是具有引用其自身名稱的子查詢的表示式。例如:


1. WITH RECURSIVE cte (n) AS
2. (
3.   SELECT 1
4.   UNION ALL
5.   SELECT n + 1 FROM cte WHERE n < 5
6. )
7. SELECT * FROM cte;

執行時,語句將生成以下結果,即一個包含簡單線性序列的列:


1. +------+
2. | n    |
3. +------+
4. |    1 |
5. |    2 |
6. |    3 |
7. |    4 |
8. |    5 |
9. +------+

遞迴 CTE 具有以下結構:

● 如果 WITH 子句中的任何 CTE 引用自身,則 WITH 子句必須以 WITH RECURSIVE 開頭。(如果沒有 CTE 引用自身,也允許使用 RECURSIVE, 但不是必須的)

如果忘記給遞迴 CTE 使用 RECURSIVE 關鍵字,則可能會出現以下錯誤:

1. ERROR 1146 (42S02): Table 'cte_name' doesn't exist

● 遞迴 CTE 子查詢有兩部分,由 UNION [ALL] 或 UNION DISTINCT 分隔:

1. SELECT ...      -- return initial row set
2. UNION ALL
3. SELECT ...      -- return additional row sets

第一個 SELECT 生成 CTE 的初始行,但不引用 CTE 名稱。第二個 SELECT 生成額外的行,並通過引用 FROM 子句中的 CTE 名稱來遞迴呼叫。當此部分不生成新的行時遞迴結束。因此,遞迴 CTE 由一個非遞迴的 SELECT 部分和一個遞迴的 SELECT 部分組成。

每個 SELECT 部分本身可以是多個 SELECT 語句的聯合。

● CTE 結果列的型別僅從非遞迴 SELECT 部分的列型別中推斷出來,並且這些列都可以為空。對於如何確定型別,會忽略遞迴 SELECT 部分的語句。

● 如果非遞迴部分和遞迴部分由 UNION DISTINCT 分隔,則消除重複行。這對於執行傳遞閉包的查詢非常有用,可以避免無限迴圈。

● 遞迴部分的每次迭代只對上一次迭代產生的行進行操作。如果遞迴部分有多個查詢塊,則每個查詢塊的迭代將按未指定的順序進行排程,並且每個查詢塊將對其上一次迭代或自上次迭代結束後由其他查詢塊生成的行進行操作。

前面顯示的遞迴 CTE 子查詢具有以下非遞迴部分,它檢索一行以生成初始行集:

1.SELECT 1

CTE 子查詢還有以下遞迴部分:


1.SELECT n + 1 FROM cte WHERE n < 5

每次迭代時,該 SELECT 將生成一行,它的新值大於之前行集中 n 的值。第一次迭代對初始行集 (1) 進行操作,生成 1+1=2;第二次迭代對第一次迭代的行集 (2) 進行操作,生成 2+1=3;依此類推。以上過程將持續進行,直到 n 不再小於5時,遞迴結束。

如果 CTE 的遞迴部分生成的列值比非遞迴部分的值更寬,則可能需要加寬非遞迴部分中的列以避免資料截斷。考慮以下語句:

1. WITH RECURSIVE cte AS
2. (
3.   SELECT 1 AS n, 'abc' AS str
4.   UNION ALL
5.   SELECT n + 1, CONCAT(str, str) FROM cte WHERE n < 3
6. )
7. SELECT * FROM cte;

在非嚴格 SQL 模式下,語句生成以下輸出:

1. +------+------+
2. | n    | str  |
3. +------+------+
4. |    1 | abc  |
5. |    2 | abc  |
6. |    3 | abc  |
7. +------+------+

str 列值都是 'abc',因為非遞迴 SELECT 決定了列寬。因此,遞迴 SELECT 產生的更寬的 str 值被截斷。

在嚴格 SQL 模式下,該語句將引發錯誤:


1.ERROR 1406 (22001): Data too long for column 'str' at row 1

要解決此問題,以便語句不產生截斷或錯誤,請在非遞迴 SELECT 中使用 CAST() 使 str 列變寬:

1. WITH RECURSIVE cte AS
2. (
3.   SELECT 1 AS n, CAST('abc' AS CHAR(20)) AS str
4.   UNION ALL
5.   SELECT n + 1, CONCAT(str, str) FROM cte WHERE n < 3
6. )
7. SELECT * FROM cte;

現在,語句將生成以下結果,而不進行截斷:

1. +------+--------------+
2. | n    | str          |
3. +------+--------------+
4. |    1 | abc          |
5. |    2 | abcabc       |
6. |    3 | abcabcabcabc |
7. +------+--------------+

通過名稱而不是位置訪問列,這意味著遞迴部分中的列可以訪問非遞迴部分中不同位置的列,如本 CTE 所示:

1. WITH RECURSIVE cte AS
2. (
3.   SELECT 1 AS n, 1 AS p, -1 AS q
4.   UNION ALL
5.   SELECT n + 1, q * 2, p * 2 FROM cte WHERE n < 5
6. )
7. SELECT * FROM cte;

因為一行中的 p 是從前一行的 q 派生出來的,反之亦然,在連續輸出的每一行中正負值交換位置:

1. +------+------+------+
2. | n    | p    | q    |
3. +------+------+------+
4. |    1 |    1 |   -1 |
5. |    2 |   -2 |    2 |
6. |    3 |    4 |   -4 |
7. |    4 |   -8 |    8 |
8. |    5 |   16 |  -16 |
9. +------+------+------+

某些語法限制在遞迴 CTE 子查詢中使用:

● 遞迴 SELECT 部分不能包含以下結構:

■ 聚合函式,如 SUM()

■ 視窗函式

■ GROUP BY

■ ORDER BY

■ DISTINCT

在 MySQL 8.0.19 之前,遞迴 CTE 的遞迴 SELECT 部分也不能使用 LIMIT 子句。這個限制在 MySQL 8.0.19 中被取消了,現在在這種情況下支援 LIMIT,同時還支援可選的 OFFSET 子句。對結果集的影響與在最外層的 SELECT 中使用 LIMIT 時的效果相同,但效率也更高,因為在遞迴 SELECT 中使用它時,一旦生成所請求的行數,就會停止繼續生成這些行。

這些約束不適用於遞迴 CTE 的非遞迴 SELECT 部分。對 DISTINCT 的禁止只適用於 UNION 成員;但是允許 UNION DISTINCT。

● 遞迴 SELECT 部分只能引用 CTE 一次,並且只能在其 FROM 子句中引用,而不能在任何子查詢中引用。它可以引用 CTE 以外的表,並將它們與 CTE 連線起來。在這種情況下,CTE 一定不能在 LEFT JOIN 的右邊。

這些約束來自 SQL 標準,而不是特定於 MySQL 的對 ORDER BY、LIMIT(MySQL 8.0.18及更早版本)和 DISTINCT 的排除。

對於遞迴 CTE,EXPLAIN 輸出遞迴 SELECT 部分的行,在 Extra 列中顯示 Recursive。

EXPLAIN 顯示的成本估算代表每次迭代的成本,這可能與總成本相差很大。優化器無法預測迭代次數,因為它無法預測 WHERE 子句條件何時不滿足。

CTE 的實際成本也可能受到結果集大小的影響。產生許多行的 CTE 可能需要一個足夠大的內部臨時表來從記憶體轉換為磁碟格式,並且可能會導致效能損失。如果是這樣,增加允許的記憶體臨時表大小可以提高效能。

官方網址: