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

限制公共表表達式遞歸html

對於遞歸 CTE 來講,遞歸 SELECT 部分包含終止遞歸的條件是很重要的。做爲一種防止遞歸 CTE 失控的開發技術,能夠經過限制執行時間來強制終止遞歸:mysql

● cte_max_recursion_depth 系統變量對 CTE 的遞歸層次數強制限制。服務器終止任何遞歸層次超過此變量值的 CTE 的執行。sql

● max_execution_time 系統變量爲當前會話中執行的 SELECT 語句設置強制執行超時時間。緩存

● MAX_EXECUTION_TIME 優化器提示對出如今 SELECT 語句中的每一個查詢設置強制執行超時時間。服務器

假設在沒有遞歸執行終止條件的狀況下,編寫了遞歸 CTE:ide

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

默認狀況下,cte_max_recursion_depth 的值爲 1000,當它遞歸超過1000次時,會致使 cte 終止。應用程序能夠更改會話值以適應其要求:優化

1. SET SESSION cte_max_recursion_depth = 10;      -- permit only shallow recursion
2. SET SESSION cte_max_recursion_depth = 1000000; -- permit deeper recursion

也能夠設置全局 cte_max_recursion_depth 值來影響隨後開始的全部會話。code

對於執行並所以遞歸緩慢的查詢,或者在有理由將 cte_max_recursion_depth 值設置得很是高的上下文中,防止深度遞歸的另外一種方法是設置基於會話的超時。要實現此效果,請在執行 CTE 語句以前執行以下語句:htm

1. SET max_execution_time = 1000; -- impose one second timeout遞歸

或者,在 CTE 語句自己中包含一個優化器提示:

1. WITH RECURSIVE cte (n) AS
2. (
3.   SELECT 1
4.   UNION ALL
5.   SELECT n + 1 FROM cte
6. )
7. SELECT /*+ SET_VAR(cte_max_recursion_depth = 1M) */ * FROM cte;
8. 
9. WITH RECURSIVE cte (n) AS
10. (
11.   SELECT 1
12.   UNION ALL
13.   SELECT n + 1 FROM cte
14. )
15. SELECT /*+ MAX_EXECUTION_TIME(1000) */ * FROM cte;

從 MySQL 8.0.19 開始,還能夠在遞歸查詢中使用 LIMIT 來限制返回給最外層 SELECT 的最大行數,例如:

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

能夠在設置時間限制以外執行此操做,也能夠不設置時間限制。如下 CTE 在返回一萬行或運行一千秒後終止,以先發生的爲準:

1. WITH RECURSIVE cte (n) AS
2. (
3.   SELECT 1
4.   UNION ALL
5.   SELECT n + 1 FROM cte LIMIT 10000
6. )
7. SELECT /*+ MAX_EXECUTION_TIME(1000) */ * FROM cte;

若是沒有執行時間限制的遞歸查詢進入無限循環,則可使用 KILL QUERY 命令從另外一個會話終止它。在會話自己內,用於運行查詢的客戶端程序也可能提供終止查詢的方法。例如,在 mysql 中,鍵入 Control+C 會中斷當前語句。

遞歸公共表表達式示例

如前所述,遞歸公共表表達式(CTE)常常用於序列生成和遍歷層次或樹結構數據。本節將展現一些簡單示例。

斐波納契數列生成

斐波納契數列從兩個數字0和1(或1和1)開始,以後的每一個數字都是前兩個數字的和。若是遞歸 SELECT 生成的每一行均可以訪問序列中的前兩個數字,則遞歸公共表表達式能夠生成一個 Fibonacci 數列。如下 CTE 使用0和1做爲前兩個數字生成10個數字系列:

1. WITH RECURSIVE fibonacci (n, fib_n, next_fib_n) AS
2. (
3.   SELECT 1, 0, 1
4.   UNION ALL
5.   SELECT n + 1, next_fib_n, fib_n + next_fib_n
6.     FROM fibonacci WHERE n < 10
7. )
8. SELECT * FROM fibonacci;

CTE 產生如下結果:

1. +------+-------+------------+
2. | n    | fib_n | next_fib_n |
3. +------+-------+------------+
4. |    1 |     0 |          1 |
5. |    2 |     1 |          1 |
6. |    3 |     1 |          2 |
7. |    4 |     2 |          3 |
8. |    5 |     3 |          5 |
9. |    6 |     5 |          8 |
10. |    7 |     8 |         13 |
11. |    8 |    13 |         21 |
12. |    9 |    21 |         34 |
13. |   10 |    34 |         55 |
14. +------+-------+------------+

CTE 的工做原理:

● n 是一個顯示列,指示該行包含第 n 個 Fibonacci 數。例如,第8個斐波納契數是13。

● fib_n 列顯示 Fibonacci 數 n。

● next_fib_n 列顯示數字 n 後面的下一個 Fibonacci 數。此列給下一行提供下一個數列值,這樣該行就能夠在 fib_n 列中生成前兩個數列值的和。

● 當 n 達到10時遞歸結束。這是一個任意的選擇,將輸出限制爲一個小的行集合。

前面的輸出顯示了整個 CTE 結果。要只選擇其中的一部分,請在頂層 SELECT 中添加適當的 WHERE 子句。例如,要選擇第8個斐波納契數,請執行如下操做:

1. mysql> WITH RECURSIVE fibonacci ...
2.        ...
3.        SELECT fib_n FROM fibonacci WHERE n = 8;
4. +-------+
5. | fib_n |
6. +-------+
7. |    13 |
8. +-------+

日期序列生成

公共表表達式能夠生成一系列連續的日期,這對於生成摘要很是有用,這些摘要包含一列表示數列中的全部日期,包括摘要數據中未展現的日期。

假設銷售數據表包含如下行:

1. mysql> SELECT * FROM sales ORDER BY date, price;
2. +------------+--------+
3. | date       | price  |
4. +------------+--------+
5. | 2017-01-03 | 100.00 |
6. | 2017-01-03 | 200.00 |
7. | 2017-01-06 |  50.00 |
8. | 2017-01-08 |  10.00 |
9. | 2017-01-08 |  20.00 |
10. | 2017-01-08 | 150.00 |
11.| 2017-01-10 |   5.00 |
12. +------------+--------+

此查詢彙總天天的銷售額:

1. mysql> SELECT date, SUM(price) AS sum_price
2.        FROM sales
3.        GROUP BY date
4.        ORDER BY date;
5. +------------+-----------+
6. | date       | sum_price |
7. +------------+-----------+
8. | 2017-01-03 |    300.00 |
9. | 2017-01-06 |     50.00 |
10. | 2017-01-08 |    180.00 |
11. | 2017-01-10 |      5.00 |
12. +------------+-----------+

可是,該結果缺失表日期範圍中未表示的日期。可使用遞歸 CTE 生成表示範圍內全部日期的結果,生成的日期集與銷售數據用 LEFT JOIN 鏈接。

下面是生成日期範圍系列的 CTE:

1. WITH RECURSIVE dates (date) AS
2. (
3.   SELECT MIN(date) FROM sales
4.   UNION ALL
5.   SELECT date + INTERVAL 1 DAY FROM dates
6.   WHERE date + INTERVAL 1 DAY <= (SELECT MAX(date) FROM sales)
7. )
8. SELECT * FROM dates;

CTE 產生如下結果:

1. +------------+
2. | date       |
3. +------------+
4. | 2017-01-03 |
5. | 2017-01-04 |
6. | 2017-01-05 |
7. | 2017-01-06 |
8. | 2017-01-07 |
9. | 2017-01-08 |
10. | 2017-01-09 |
11. | 2017-01-10 |
12. +------------+

CTE的工做原理:

● 非遞歸 SELECT 生成銷售表日期範圍中的最小日期。

● 遞歸 SELECT 生成的每一行將在前一行生成的日期上增長一天。

● 遞歸在日期到達銷售表日期範圍中的最大日期以後結束。

用 LEFT JOIN 將 CTE 和銷售錶鏈接,生成銷售摘要,其中包含範圍內每一個日期行:

1. WITH RECURSIVE dates (date) AS
2. (
3.   SELECT MIN(date) FROM sales
4.   UNION ALL
5.   SELECT date + INTERVAL 1 DAY FROM dates
6.   WHERE date + INTERVAL 1 DAY <= (SELECT MAX(date) FROM sales)
7. )
8. SELECT dates.date, COALESCE(SUM(price), 0) AS sum_price
9. FROM dates LEFT JOIN sales ON dates.date = sales.date
10. GROUP BY dates.date
11. ORDER BY dates.date;

輸出以下:

1. +------------+-----------+
2. | date       | sum_price |
3. +------------+-----------+
4. | 2017-01-03 |    300.00 |
5. | 2017-01-04 |      0.00 |
6. | 2017-01-05 |      0.00 |
7. | 2017-01-06 |     50.00 |
8. | 2017-01-07 |      0.00 |
9. | 2017-01-08 |    180.00 |
10. | 2017-01-09 |      0.00 |
11. | 2017-01-10 |      5.00 |
12. +------------+-----------+

注意事項:

● 查詢是否效率低下,尤爲是遞歸 SELECT 中每行都執行 MAX() 子查詢的查詢?EXPLAIN 顯示包含 MAX() 的子查詢只計算一次,結果被緩存。

● 使用 COALESCE() 能夠避免在銷售表中沒有銷售數據的日子在 sum_price 列中顯示 NULL。

分層數據遍歷

遞歸公共表表達式對於遍歷造成層次結構的數據很是有用。考慮這些語句,建立一個小數據集,該數據集顯示公司中每一個員工的員工姓名和 ID 號,以及員工經理的 ID。頂級員工(CEO)的經理 ID 爲 NULL(無經理)。

1. CREATE TABLE employees (
2.   id         INT PRIMARY KEY NOT NULL,
3.   name       VARCHAR(100) NOT NULL,
4.   manager_id INT NULL,
5.   INDEX (manager_id),
6. FOREIGN KEY (manager_id) REFERENCES employees (id)
7. );
8. INSERT INTO employees VALUES
9. (333, "Yasmina", NULL),  # Yasmina is the CEO (manager_id is NULL)
10. (198, "John", 333),      # John has ID 198 and reports to 333 (Yasmina)
11. (692, "Tarek", 333),
12. (29, "Pedro", 198),
13. (4610, "Sarah", 29),
14. (72, "Pierre", 29),
15. (123, "Adil", 692);

結果數據集以下所示:

1. mysql> SELECT * FROM employees ORDER BY id;
2. +------+---------+------------+
3. | id   | name    | manager_id |
4. +------+---------+------------+
5. |   29 | Pedro   |        198 |
6. |   72 | Pierre  |         29 |
7. |  123 | Adil    |        692 |
8. |  198 | John    |        333 |
9. |  333 | Yasmina |       NULL |
10. |  692 | Tarek   |        333 |
11. | 4610 | Sarah   |         29 |
12. +------+---------+------------+

要生成包含每一個員工管理鏈的組織結構圖(即從CEO到員工的路徑),請使用遞歸 CTE:

1. WITH RECURSIVE employee_paths (id, name, path) AS
2. (
3.   SELECT id, name, CAST(id AS CHAR(200))
4.     FROM employees
5.     WHERE manager_id IS NULL
6.   UNION ALL
7.   SELECT e.id, e.name, CONCAT(ep.path, ',', e.id)
8.     FROM employee_paths AS ep JOIN employees AS e
9.       ON ep.id = e.manager_id
10. )
11. SELECT * FROM employee_paths ORDER BY path;

CTE 產生如下輸出:

1. +------+---------+-----------------+
2. | id   | name    | path            |
3. +------+---------+-----------------+
4. |  333 | Yasmina | 333             |
5. |  198 | John    | 333,198         |
6. |   29 | Pedro   | 333,198,29      |
7. | 4610 | Sarah   | 333,198,29,4610 |
8. |   72 | Pierre  | 333,198,29,72   |
9. |  692 | Tarek   | 333,692         |
10. |  123 | Adil    | 333,692,123     |
11. +------+---------+-----------------+

CTE 的工做原理:

● 非遞歸 SELECT 爲 CEO 生成行(經理 ID 爲 NULL 的行)。

path 列被加寬爲 CHAR(200),以確保有空間容納遞歸 SELECT 生成的較長路徑值。

● 遞歸 SELECT 生成的每一行都會查找直接向前一行生成的員工報告的全部員工。對於每一個這樣的員工,該行包括員工 ID 和姓名,以及員工管理鏈。管理鏈是經理的管理鏈,末尾添加了員工 ID。

● 當員工沒有其餘人向他們報告時,遞歸結束。

若要查找特定員工的路徑,請在頂層 SELECT 中添加 WHERE 子句。例如,要顯示 Tarek 和 Sarah 的結果,請按以下方式修改 SELECT 語句:

1. mysql> WITH RECURSIVE ...
2.        ...
3.        SELECT * FROM employees_extended
4.        WHERE id IN (692, 4610)
5.        ORDER BY path;
6. +------+-------+-----------------+
7. | id   | name  | path            |
8. +------+-------+-----------------+
9. | 4610 | Sarah | 333,198,29,4610 |
10. |  692 | Tarek | 333,692         |
11. +------+-------+-----------------+

公共表表達式與相似結構的比較

公共表表達式(CTE)在某些方面與派生表類似:

● 兩個結構都須要命名。

● 這兩個結構都存在於單個語句的範圍內。

因爲這些類似性,CTE 和派生表一般能夠互換使用。下面的一個簡單例子中,這些語句是等價的:

1.  WITH cte AS (SELECT 1) SELECT * FROM cte;
2.  SELECT * FROM (SELECT 1) AS dt;

可是,與派生表相比,CTE 有一些優點:

● 派生表只能在查詢中被引用一次。一個 CTE 能夠被屢次引用。要使用派生表結果的多個實例,必須屢次派生結果。

● CTE 能夠是自引用(遞歸的)。

● 一個 CTE 能夠引用另外一個 CTE。

● 當 CTE 的定義出如今語句的開頭而不是嵌入其中時,它可能更易於閱讀。

CTE 相似於用 CREATE [TEMPORARY] TABLE 建立的表,但不須要顯式定義或刪除。對於 CTE,不須要建立表的權限。

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

相關文章
相關標籤/搜索