限制公共表表达式递归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,不须要建立表的权限。