MySQL的SQL語句 - 數據操做語句(17)- WITH 語句(2)

遞歸公共表表達式html

遞歸公共表表達式是具備引用其自身名稱的子查詢的表達式。例如:mysql

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;

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

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

遞歸 CTE 具備如下結構:閉包

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

若是忘記給遞歸 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 語句的聯合。code

● CTE 結果列的類型僅從非遞歸 SELECT 部分的列類型中推斷出來,而且這些列均可覺得空。對於如何肯定類型,會忽略遞歸 SELECT 部分的語句。htm

● 若是非遞歸部分和遞歸部分由 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 可能須要一個足夠大的內部臨時表來從內存轉換爲磁盤格式,而且可能會致使性能損失。若是是這樣,增長容許的內存臨時表大小能夠提升性能。

官方網址:
https://dev.mysql.com/doc/refman/8.0/en/with.html

相關文章
相關標籤/搜索