Documentation Home
MySQL 8.4 Reference Manual
Related Documentation Download this Manual
PDF (US Ltr) - 39.8Mb
PDF (A4) - 39.9Mb
Man Pages (TGZ) - 257.9Kb
Man Pages (Zip) - 364.9Kb
Info (Gzip) - 4.0Mb
Info (Zip) - 4.0Mb


MySQL 8.4 Reference Manual  /  ...  /  WITH (Common Table Expressions)

15.2.20 WITH (Common Table Expressions)

常见表达式(CTE)是一种临时结果集,它在单个语句的作用域内存在,并且可以在该语句中多次引用。以下讨论描述了使用 CTE 的语句编写方法。

关于 CTE 优化的信息,请见Section 10.2.2.4, “Optimizing Derived Tables, View References, and Common Table Expressions with Merging or Materialization”.

Common Table 表达式

要指定常见表达式,请使用WITH子句,该子句包含一个或多个逗号分隔的子句。每个子句都提供一个子查询,该子查询生成结果集,并将子查询与名称关联。以下示例定义了名为 cte1cte2 的 CTE,在WITH子句中,并在后续的SELECT语句中引用它们:

WITH
  cte1 AS (SELECT a, b FROM table1),
  cte2 AS (SELECT c, d FROM table2)
SELECT b, d FROM cte1 JOIN cte2
WHERE cte1.a = cte2.c;

在包含WITH子句的语句中,每个 CTE 名称都可以引用相应的 CTE 结果集。

CTE 名称可以在其他 CTE 中引用,以便定义基于其他 CTE 的 CTE。

CTE 可以引用自己以定义递归 CTE。常见的递归 CTE 应用场景包括系列生成和对层次结构或树形数据的遍历。

常见表达式是语句语法的可选部分。它们使用WITH子句定义:

with_clause:
    WITH [RECURSIVE]
        cte_name [(col_name [, col_name] ...)] AS (subquery)
        [, cte_name [(col_name [, col_name] ...)] AS (subquery)] ...

cte_name命名单个常见表达式,并且可以在包含WITH子句的语句中引用该表达式。

子查询》部分的AS (子查询)称为CTE的子查询”,它生产CTE结果集。代码AS后的括号是必需的。

常规表达式是递归的,如果其子查询引用自己的名称。代码RECURSIVE关键字必须包括,如果CTE中的任何CTE是递归的。更多信息,请参见递归常规表达式

确定CTE的列名如下:

  • 如果CTE名称后面跟随括号列表,那么这些名称是列名:

    WITH cte (col1, col2) AS
    (
      SELECT 1, 2
      UNION ALL
      SELECT 3, 4
    )
    SELECT col1, col2 FROM cte;

    列表中的名称数量必须与结果集中的列数量相同。

  • 否则,列名来自于select列表中的第一个SELECT部分:

    WITH cte AS
    (
      SELECT 1 AS col1, 2 AS col2
      UNION ALL
      SELECT 3, 4
    )
    SELECT col1, col2 FROM cte;

WITH子句在以下上下文中允许:

  • SELECTUPDATEDELETE语句的开始处。

    WITH ... SELECT ...
    WITH ... UPDATE ...
    WITH ... DELETE ...
  • 在子查询(包括派生表子查询)中的开始处:

    SELECT ... WHERE id IN (WITH ... SELECT ...) ...
    SELECT * FROM (WITH ... SELECT ...) AS dt ...
  • 紧接着SELECT语句的开始处:

    INSERT ... WITH ... SELECT ...
    REPLACE ... WITH ... SELECT ...
    CREATE TABLE ... WITH ... SELECT ...
    CREATE VIEW ... WITH ... SELECT ...
    DECLARE CURSOR ... WITH ... SELECT ...
    EXPLAIN ... WITH ... SELECT ...

只能在同一个级别上出现一个WITH子句。代码WITH后面跟随WITH不允许在同一个级别上出现,所以这不是合法的:

WITH cte1 AS (...) WITH cte2 AS (...) SELECT ...

要使语句合法,使用单个WITH子句,使用逗号分隔子句:

WITH cte1 AS (...), cte2 AS (...) SELECT ...

然而,语句可以包含多个WITH子句,如果它们出现在不同的级别上:

WITH cte1 AS (SELECT 1)
SELECT * FROM (WITH cte2 AS (SELECT 2) SELECT * FROM cte2 JOIN cte1) AS dt;

WITH子句可以定义一个或多个常规表达式,但是每个CTE名称都必须在该子句中唯一。这不是合法的:

WITH cte1 AS (...), cte1 AS (...) SELECT ...

要使语句合法,定义CTE的唯一名称:

WITH cte1 AS (...), cte2 AS (...) SELECT ...

CTE可以引用自己或其他CTE:

  • 递归CTE是自我引用CTE。

  • CTE 可以引用在同一个WITH子句中定义的CTE,但不能引用后面定义的CTE。

    这项约束规出互递归CTE,例如cte1引用cte2cte2引用cte1。其中一个引用必须是对后面定义的CTE,这是不允许的。

  • 在一个查询块中,CTE 可以引用在更高级别的查询块中定义的CTE,但不能引用在更低级别的查询块中定义的CTE。

在解决对象名称的引用时,派生表隐藏CTE;CTE隐藏基本表、TEMPORARY表和视图。名称解析是从同一个查询块中搜索对象,然后顺序地搜索外部块直到找到对象名称为止。

有关递归CTE的特定语法考虑,请见Recursive Common Table Expressions

Recursive Common Table 表达式

递归CTE 是指具有子查询引用自己的名称的CTE。例如:

WITH RECURSIVE cte (n) AS
(
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM cte WHERE n < 5
)
SELECT * FROM cte;

执行该语句时,语句将产生以下结果,一个包含简单线性序列的单列:

+------+
| n    |
+------+
|    1 |
|    2 |
|    3 |
|    4 |
|    5 |
+------+

递归CTE具有以下结构:

  • WITH 子句必须以WITH RECURSIVE开头,如果CTE中的任何CTE引用自己。(如果没有CTE引用自己,RECURSIVE是允许的,但不是必需的。)

    如果您忘记RECURSIVE,这可能会导致以下错误:

    ERROR 1146 (42S02): Table 'cte_name' doesn't exist
  • 递归CTE子查询有两个部分,使用UNION ALLUNION [DISTINCT]分隔:

    SELECT ...      -- return initial row set
    UNION ALL
    SELECT ...      -- return additional row sets

    第一个SELECT语句生产初始行或行,并且不引用CTE名称。第二个SELECT语句生产额外行,并递归地引用CTE名称在其FROM子句中。递归结束时,这部分不生产新的行。因此,递归CTE由非递归SELECT部分和递归SELECT部分组成。

    每个SELECT部分可以本身是一个联合多个SELECT语句的结果。

  • CTE结果列的类型是从非递归SELECT部分的列类型中推断的,并且所有列都是可空的。对于类型确定,递归SELECT部分被忽略。

  • 如果非递归和递归部分之间使用了UNION DISTINCT,则重复行将被删除。这对于执行传递闭包的查询非常有用,以避免无限循环。

  • 每个递归部分的迭代都只操作前一个迭代的结果。如果递归部分有多个查询块,迭代的顺序是未指定的,每个查询块都操作自上一个迭代或其他查询块自上一个迭代结束以来产生的行。

之前展示的递归CTE子查询具有以下非递归部分,该部分检索单个行以产生初始行集:

SELECT 1

CTE子查询还具有以下递归部分:

SELECT n + 1 FROM cte WHERE n < 5

在每个迭代中,SELECT 生成一行,其中的新值是前一个行集中的值加1。第一个迭代操作初始行集(1)并生成1+1=2;第二个迭代操作第一个迭代的行集(2)并生成2+1=3;以此类推。直到递归结束,这发生在n不再小于5时。

如果递归部分的CTE生成的列值比非递归部分更宽,可能需要在非递归部分中扩展该列以避免数据截断。考虑以下语句:

WITH RECURSIVE cte AS
(
  SELECT 1 AS n, 'abc' AS str
  UNION ALL
  SELECT n + 1, CONCAT(str, str) FROM cte WHERE n < 3
)
SELECT * FROM cte;

在非严格SQL模式下,该语句生成以下输出:

+------+------+
| n    | str  |
+------+------+
|    1 | abc  |
|    2 | abc  |
|    3 | abc  |
+------+------+

所有str列值都是'abc',因为非递归SELECT确定列宽。因此,递归SELECT生成的更宽的str值将被截断。

在严格SQL模式下,该语句生成错误:

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

为了解决这个问题,使语句不生成截断或错误,请在非递归SELECT中使用CAST()以使str列更宽:

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

现在语句生成以下结果,无截断:

+------+--------------+
| n    | str          |
+------+--------------+
|    1 | abc          |
|    2 | abcabc       |
|    3 | abcabcabcabc |
+------+--------------+

可以通过名称访问列,而不是位置,这意味着递归部分可以访问非递归部分的列,即使它们的位置不同,如CTE示例所示:

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

因为p在一行中是从前一个行中的q派生出来的,反之亦然,在每个连续行的输出中,正负值的顺序会交换:

+------+------+------+
| n    | p    | q    |
+------+------+------+
|    1 |    1 |   -1 |
|    2 |   -2 |    2 |
|    3 |    4 |   -4 |
|    4 |   -8 |    8 |
|    5 |   16 |  -16 |
+------+------+------+

在递归CTE子查询中有一些语法约束:

  • 递归SELECT部分不能包含以下构造:

    • 聚合函数,如SUM()

    • 窗口函数

    • GROUP BY

    • ORDER BY

    • DISTINCT

    递归SELECT部分也可以使用LIMIT子句,另外还可以使用可选的OFFSET子句。结果集的影响与在外层SELECT中使用LIMIT相同,但使用它与递归SELECT停止生成行的过程,直到请求的行数被生成。

    禁止UNION成员的DISTINCT子句只适用于UNION成员;UNION DISTINCT允许。

  • 递归SELECT部分必须引用CTE一次,并且只能在FROM子句中引用CTE,而不是在任何子查询中引用CTE。它可以引用其他表,并将其与CTE连接。如果在连接中使用CTE,它不能在LEFT JOIN中使用CTE。

这些约束来自SQL标准,除了MySQL中提到的特定排除。

对于递归CTE,EXPLAIN输出行显示SELECT部分的Extra列中的Recursive

EXPLAIN显示的成本估算表示每次迭代的成本,这可能与总成本不同。优化器无法预测迭代次数,因为它无法预测WHERE子句何时变为假。

CTE实际成本也可能受到结果集大小的影响。CTE生成许多行可能需要内部临时表转换为磁盘格式,并且可能会受到性能 penalty。如果是这样,增加允许的内存临时表大小可能会改善性能;请参见Section 10.4.4, “MySQL 中的内部临时表使用”

对于递归CTE,重要的是递归SELECT部分包括终止递归的条件。作为开发技术来防止递归CTE的运行,可以强制终止执行:

假设一个递归CTE错误地没有写入递归执行终止条件:

WITH RECURSIVE cte (n) AS
(
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM cte
)
SELECT * FROM cte;

默认情况下,cte_max_recursion_depth 的值为1000,导致CTE在递归超过1000级时终止。应用程序可以更改会话值以适应其需求:

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

您也可以将全局cte_max_recursion_depth 值设置,以影响所有随后开始的会话。

对于执行时间较长或在某些情况下需要设置cte_max_recursion_depth 值非常高的查询,可以使用会话超时来保护深度递归。要做到这一点,请在执行CTE语句之前执行以下语句:

SET max_execution_time = 1000; -- impose one second timeout

或者,您可以在CTE语句本身中包含优化提示:

WITH RECURSIVE cte (n) AS
(
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM cte
)
SELECT /*+ SET_VAR(cte_max_recursion_depth = 1M) */ * FROM cte;

WITH RECURSIVE cte (n) AS
(
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM cte
)
SELECT /*+ MAX_EXECUTION_TIME(1000) */ * FROM cte;

您也可以使用 LIMIT 在递归查询中来限制返回到最外层SELECT 的最大行数,例如:

WITH RECURSIVE cte (n) AS
(
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM cte LIMIT 10000
)
SELECT * FROM cte;

您可以在设置超时限制的同时或代替设置超时限制。因此,以下CTE在返回十万行或运行一秒(1000毫秒)后终止, whichever occurs first:

WITH RECURSIVE cte (n) AS
(
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM cte LIMIT 10000
)
SELECT /*+ MAX_EXECUTION_TIME(1000) */ * FROM cte;

如果一个递归查询没有执行时间限制进入无限循环,您可以在另一个会话中使用KILL QUERY 终止它。从会话本身,客户端程序可能提供了终止查询的方式。例如,在mysql 中,输入Control+C 可以中断当前语句。

如前所述,递归常见表达式(CTE)常用于生成系列和遍历树状或层次结构数据。这部分展示了一些简单的示例这些技术。

斐波那契数列从两个数字0和1开始(或1和1),每个数字都是前两个数字的和。使用递归通用表达式可以生成斐波那契数列,如果每个由递归SELECT产生的行都可以访问前两个数字。以下CTE使用0和1作为第一个两个数字生成一个10个数字的系列:

WITH RECURSIVE fibonacci (n, fib_n, next_fib_n) AS
(
  SELECT 1, 0, 1
  UNION ALL
  SELECT n + 1, next_fib_n, fib_n + next_fib_n
    FROM fibonacci WHERE n < 10
)
SELECT * FROM fibonacci;

CTE的结果如下:

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

CTE的工作原理:

  • n是一个显示列,表示该行包含第n-个斐波那契数字。例如,第8个斐波那契数字是13。

  • fib_n列显示斐波那契数字n

  • next_fib_n列显示数字n后的斐波那契数字。这列提供了下一个系列值,以便下一行可以在其fib_n列中生成前两个系列值的和。

  • 递归结束时n达到10。这是一个任意的选择,以限制输出到一个小的行集。

前面的输出显示了整个CTE结果。要选择其中的一部分,添加适当的WHERE子句到顶级SELECT。例如,要选择第8个斐波那契数字,做如下操作:

mysql> WITH RECURSIVE fibonacci ...
       ...
       SELECT fib_n FROM fibonacci WHERE n = 8;
+-------+
| fib_n |
+-------+
|    13 |
+-------+

通用表达式可以生成一系列连续日期,这对于生成包含所有日期的摘要非常有用,其中包括日期不在摘要数据中的日期。

假设一个销售数据表包含以下行:

mysql> SELECT * FROM sales ORDER BY date, price;
+------------+--------+
| date       | price  |
+------------+--------+
| 2017-01-03 | 100.00 |
| 2017-01-03 | 200.00 |
| 2017-01-06 |  50.00 |
| 2017-01-08 |  10.00 |
| 2017-01-08 |  20.00 |
| 2017-01-08 | 150.00 |
| 2017-01-10 |   5.00 |
+------------+--------+

这个查询总结了每天的销售额:

mysql> SELECT date, SUM(price) AS sum_price
       FROM sales
       GROUP BY date
       ORDER BY date;
+------------+-----------+
| date       | sum_price |
+------------+-----------+
| 2017-01-03 |    300.00 |
| 2017-01-06 |     50.00 |
| 2017-01-08 |    180.00 |
| 2017-01-10 |      5.00 |
+------------+-----------+

然而,这个结果包含了holes,即日期不在销售数据中的日期。使用递归CTE生成日期系列,可以与销售数据进行LEFT JOIN,生成包含所有日期的结果。

以下是生成日期系列CTE的代码:

WITH RECURSIVE dates (date) AS
(
  SELECT MIN(date) FROM sales
  UNION ALL
  SELECT date + INTERVAL 1 DAY FROM dates
  WHERE date + INTERVAL 1 DAY <= (SELECT MAX(date) FROM sales)
)
SELECT * FROM dates;

CTE的结果如下:

+------------+
| date       |
+------------+
| 2017-01-03 |
| 2017-01-04 |
| 2017-01-05 |
| 2017-01-06 |
| 2017-01-07 |
| 2017-01-08 |
| 2017-01-09 |
| 2017-01-10 |
+------------+

CTE的工作原理:

  • 非递归SELECT生产日期范围内的最低日期。

  • 每个由递归SELECT产生的行都将日期增加一天。

  • 递归结束时日期达到日期范围内的最高日期。

将CTE与LEFT JOIN对销售数据表进行连接,生成了每天的销售摘要:

WITH RECURSIVE dates (date) AS
(
  SELECT MIN(date) FROM sales
  UNION ALL
  SELECT date + INTERVAL 1 DAY FROM dates
  WHERE date + INTERVAL 1 DAY <= (SELECT MAX(date) FROM sales)
)
SELECT dates.date, COALESCE(SUM(price), 0) AS sum_price
FROM dates LEFT JOIN sales ON dates.date = sales.date
GROUP BY dates.date
ORDER BY dates.date;

输出结果如下:

+------------+-----------+
| date       | sum_price |
+------------+-----------+
| 2017-01-03 |    300.00 |
| 2017-01-04 |      0.00 |
| 2017-01-05 |      0.00 |
| 2017-01-06 |     50.00 |
| 2017-01-07 |      0.00 |
| 2017-01-08 |    180.00 |
| 2017-01-09 |      0.00 |
| 2017-01-10 |      5.00 |
+------------+-----------+

需要注意的一些点:

  • 这些查询是否效率低下,特别是包含MAX()的子查询是否每行都执行?EXPLAIN显示,包含MAX()的子查询只被评估一次,并且结果被缓存。

  • 使用COALESCE()避免在sum_price列中显示NULL值,在sales表中没有销售数据的日子。

递归常规表达式非常有用,可以遍历形成层次结构的数据。考虑这些语句,创建一个小数据集,显示每个员工在公司中的员工名称和ID号,以及员工的经理ID号。公司的CEO(总裁)没有经理ID(NULL)。

CREATE TABLE employees (
  id         INT PRIMARY KEY NOT NULL,
  name       VARCHAR(100) NOT NULL,
  manager_id INT NULL,
  INDEX (manager_id),
FOREIGN KEY (manager_id) REFERENCES employees (id)
);
INSERT INTO employees VALUES
(333, "Yasmina", NULL),  # Yasmina is the CEO (manager_id is NULL)
(198, "John", 333),      # John has ID 198 and reports to 333 (Yasmina)
(692, "Tarek", 333),
(29, "Pedro", 198),
(4610, "Sarah", 29),
(72, "Pierre", 29),
(123, "Adil", 692);

结果数据集如下:

mysql> SELECT * FROM employees ORDER BY id;
+------+---------+------------+
| id   | name    | manager_id |
+------+---------+------------+
|   29 | Pedro   |        198 |
|   72 | Pierre  |         29 |
|  123 | Adil    |        692 |
|  198 | John    |        333 |
|  333 | Yasmina |       NULL |
|  692 | Tarek   |        333 |
| 4610 | Sarah   |         29 |
+------+---------+------------+

为了生成员工的组织结构(即从CEO到员工),使用递归CTE:

WITH RECURSIVE employee_paths (id, name, path) AS
(
  SELECT id, name, CAST(id AS CHAR(200))
    FROM employees
    WHERE manager_id IS NULL
  UNION ALL
  SELECT e.id, e.name, CONCAT(ep.path, ',', e.id)
    FROM employee_paths AS ep JOIN employees AS e
      ON ep.id = e.manager_id
)
SELECT * FROM employee_paths ORDER BY path;

CTE产生的输出:

+------+---------+-----------------+
| id   | name    | path            |
+------+---------+-----------------+
|  333 | Yasmina | 333             |
|  198 | John    | 333,198         |
|   29 | Pedro   | 333,198,29      |
| 4610 | Sarah   | 333,198,29,4610 |
|   72 | Pierre  | 333,198,29,72   |
|  692 | Tarek   | 333,692         |
|  123 | Adil    | 333,692,123     |
+------+---------+-----------------+

CTE是如何工作的:

  • 非递归SELECT生产CEO(总裁)的行(该行的经理ID为NULL)。

    path列扩展到CHAR(200)以确保有足够的空间存储由递归SELECT生产的path值。

  • 每行由递归SELECT生产的员工找到所有直接报告给员工的员工。对于每个这样的员工,该行包括员工ID和名称,以及员工的管理链。链是经理的链,员工ID添加到链的末尾。

  • 递归结束时,员工没有其他员工报告给他们。

要找到特定员工或员工的路径,添加WHERE子句到顶级SELECT。例如,要显示Tarek和Sarah的结果,修改该SELECT如下:

mysql> WITH RECURSIVE ...
       ...
       SELECT * FROM employees_extended
       WHERE id IN (692, 4610)
       ORDER BY path;
+------+-------+-----------------+
| id   | name  | path            |
+------+-------+-----------------+
| 4610 | Sarah | 333,198,29,4610 |
|  692 | Tarek | 333,692         |
+------+-------+-----------------+

Common Table 表达式与相似构造的比较

Common table 表达式(CTE)与某些构造相似:

  • 两种构造都有名称。

  • 两种构造都存在于单个语句的范围内。

由于这些相似之处,CTE和派生表可以互换使用。例如,这些语句是等价的:

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

然而,CTE相比派生表有以下优势:

  • 派生表只能被引用一次。CTE可以被引用多次。要使用多个派生表结果,必须多次派生结果。

  • CTE 可以是自引用的(递归的)。

  • 一个 CTE 可以引用另一个。

  • CTE 在语句开始处定义可能更易读,而不是嵌入其中。

CTE 类似于使用 CREATE [TEMPORARY] TABLE 创建的表,但不需要定义或删除。为 CTE,您不需要创建表的权限。