likes
comments
collection
share

MySQL公共表表达式CTE及实战

作者站长头像
站长
· 阅读数 3

什么是CTE

CTE(Common Table Expression公共表表达式)是一个临时的结果集,它在SQL语句执行期间存在,并且可以在包含它的SQL查询中被引用。它提供了一种更加清晰和灵活的方式来编写复杂的SQL查询,允许你将查询分解成更小的、可管理的部分,并且可以在同一查询中多次引用。它是以WITH语句开始的,后跟CTE的名称、可选的列名列表,以及一个AS关键字后跟一个括号,括号中包含CTE的查询。

MySQL从版本8.0开始支持CTE

以下是一个使用CTE的简单SQL查询示例:

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

cte_name: CTE名称,后续使用

col_name: 列名,未指定时从subquery中推断

subquery: subquery部分产生结果集,括号为必须

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

什么是RECURSIVE CTE

RECURSIVE 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 |
 +------+

WITH后必须包含RECURISIVE

subquery中,SELECT 1为初始查询(递归入口),SELECT n + 1 FROM cte WHERE n < 5为递归部分,查询终止条件(递归出口)为递归部分结果为空; 每次迭代只会处理上一次迭代产生的结果

以上两个例子看完还是不懂?没关系,下面我们看看其在OneTerm的实战。

github.com/veops/onete…

场景1: 查询资产的目录结构

在OneTerm中,每个资产节点都包含一个节点名称属性,即为资产所属节点目录的路径,如图中test/测试组1,很自然的想到这种查询需要自顶向下的递归过程。

MySQL公共表表达式CTE及实战

简单起见,示例表中仅包含需要用到的字段

CREATE  DATABASE IF NOT EXISTS test;
 
 CREATE TABLE IF NOT EXISTS test.node (
     `id` INT NOT NULL AUTO_INCREMENT,
     `name` VARCHAR(64) NOT NULL DEFAULT  '',
     `parent_id` INT NOT NULL DEFAULT 0,
     PRIMARY KEY (`id`)
 ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
 
 INSERT INTO test.node (id, name, parent_id)
 VALUES(1, "node1", 0);
 INSERT INTO test.node (id, name, parent_id)
 VALUES(2, "node2", 1);
 INSERT INTO test.node (id, name, parent_id)
 VALUES(3, "node3", 1);
 INSERT INTO test.node (id, name, parent_id)
 VALUES(4, "node4", 2);
 INSERT INTO test.node (id, name, parent_id)
 VALUES(5, "node5", 2);
 INSERT INTO test.node (id, name, parent_id)
 VALUES(6, "node6", 5);
 
 select * from test.node;
 
 +----+-------+-----------+
 | id | name  | parent_id |
 +----+-------+-----------+
 |  1 | node1 |         0 |
 |  2 | node2 |         1 |
 |  3 | node3 |         1 |
 |  4 | node4 |         2 |
 |  5 | node5 |         2 |
 |  6 | node6 |         5 |
 +----+-------+-----------+

递归CTE查询:

WITH  RECURSIVE cte AS (
     SELECT id,
         name,
         0 AS depth
     FROM test.node
     WHERE id = 1
     UNION ALL
     SELECT t.id,
         CONCAT(cte.name, '->',  t.name),
         cte.depth + 1
     FROM cte
         INNER JOIN test.node AS t ON  cte.id = t.parent_id
 )
 SELECT *
 FROM cte;
 
 +------+----------------------------+-------+
 | id   | name                       | depth |
 +------+----------------------------+-------+
 |    1 | node1                      |     0 |
 |    2 | node1->node2               |     1 |
 |    3 | node1->node3               |     1 |
 |    4 | node1->node2->node4        |     2 |
 |    5 | node1->node2->node5        |     2 |
 |    6 | node1->node2->node5->node6 |     3 |
 +------+----------------------------+-------+

场景2: 查询每个节点目录下包含的资产个数

与场景1相对,资产树每个节点需要展示他所包含的资产个数,资产个数不仅要包含当前节点直接所属的资产个数,同时要包含所有子节点的资产个数。此时就不能简单使用group by来统计。同样的还是得使用递归查询来统计。与场景1不同的是,我们需要自底向上的递归。

这个过程其实和算法中对树的统计是一样的,场景1需要求出根节点到每个节点的路径,场景2需要求出每个节点的子节点的权值和+该节点本身权值

MySQL公共表表达式CTE及实战

WITH  RECURSIVE cte AS (
     SELECT parent_id
     FROM test.node
     UNION ALL
     SELECT t.parent_id
     FROM cte
         INNER JOIN test.node AS t ON  cte.parent_id = t.id
 )
 SELECT parent_id,
     count(*) AS cnt
 FROM cte
 GROUP BY parent_id;
 
 +-----------+-----+
 | parent_id | cnt |
 +-----------+-----+
 |         0 |   6 |
 |         1 |   5 |
 |         2 |   3 |
 |         5 |   1 |
 +-----------+-----+

参考

dev.mysql.com/doc/refma