限制公共表表達式遞歸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,不須要建立表的權限。