目录
在介绍CTE之前先介绍一下子查询、派生表、临时表。
子查询是嵌套在另一个查询(如select、insert、update和delete)中的查询。子查询又称为内部查询,而包含子查询的查询称为外部查询。 子查询可以在使用表达式的任何地方使用,并且必须在括号中关闭。
派生表是从select语句返回的虚拟表。派生表类似于临时表,但是在SELECT语句中使用派生表比临时表简单得多,因为派生表没有创建临时表的步骤。派生表会在使用过后即时清除的,所以我们在简化复杂查询的时候可以考虑使用。
注意:派生表和子查询通常可以互换使用,但是与子查询不同的是,派生表必须具有别名。
SELECT column_list FROM
( SELECT column_list FROM table_1) derived_table_name --派生表
WHERE derived_table_name.c1 > 0;
注意:派生表之间不可以相互引用。例如:SELECT ... FROM (SELECT ... FROM ...) AS d1, (SELECT ... FROM d1 ...) AS d2,第一个查询标记为d1,在第二个查询语句中使用d1是不允许的。
#创建临时表
CREATE TEMPORARY TABLE table_name (
name VARCHAR(10) NOT NULL,
value INTEGER NOT NULL
);
#删除临时表
drop temporary table table_name;
MySQL 从 8.0 开始支持 WITH 语法,即:Common Table Expressions - CTE,公用表表达式。官网:公用表达式
CTE(Common Table Expression,公共表表达式)是一个命名的临时结果集,它存在于单个语句的作用域中,以后可以在该语句中多次被引用。如下查询语句的with cte as (select 1 as one, 2 as two)
即为公共表表达式,它是一个命名的(名字叫cte)结果集,而且在该条语句中它可以被当做临时表来引用(最后的select语句引用了它)。初次接触CTE是不是感觉跟派生表子查询很像?没错!他们确实在某些方面很像,如它们都有名字、它们都在单条语句中生效。
CTE语法属于DML
with cte_name (column_list) as (
query
)
select * from cte_name;
查询中的列数必须与 column_list 中的列数相同。 如果省略 column_list,CTE 将使用定义 CTE 的查询的列列表。
创建测试环境:
#创建department表
create table
department (
id bigint auto_increment comment '主键ID' primary key,
dept_name varchar(32) not null comment '部门名称',
parent_id bigint default 0 not null comment '父级id'
);
#插入数据
insert into
`department`
values
(null, '总部', 0),
(null, '研发部', 1),
(null, '测试部', 1),
(null, 'Java组', 2),
(null, 'Python组', 2),
(null, '前端组', 2),
(null, '供应链测试组', 3),
(null, '商城测试组', 3),
(null, '供应链产品组', 4),
(null, '商城产品组', 4),
(null, 'Java1组', 5),
(null, 'Java2组', 5);
with cte1 as (select * from `department` where id in (1, 2)),
cte2 as (select * from `department` where id in (2, 3))
select
*
from
cte1
join cte2
where
cte1.id = cte2.id;
with cte1 as (select * from `department` where id = 1),
cte2 as (select * from cte1)
select *
from cte2;
RCTE(Recursive Common Table Expressions,递归公共表表达式)是一种会引用自身的CTE,一般用于生成序列、遍历层次数据结构。下面看一个生成1至10的序列样例。RCTE有如下几个特点:
with recursive cte_name as (
initial_query -- anchor member
union all
recursive_query -- recursive member that references to the cte name
)
select * from cte_name;
上述约束不适用于锚点成员。 另外,只有在使用 union 运算符时,要禁止 distinct 才适用。 如果使用 union distinct 运算符,则允许使用 distinct。
with recursive cte_count (n)
as (
select 1
union all
select n + 1
from cte_count
where n < 3
)
select n from cte_count;
select 1是作为基本结果集返回1的锚成员;
select n + 1 from cte_count where n < 3是递归成员,因为它引用了 cte_count 的 CTE 名称。递归成员中的表达式 < 3 是终止条件。当 n 等于 3,递归成员将返回一个空集合,将停止递归。
下图显示了上述CTE的元素:
递归CTE返回以下输出:
+------+
| n |
+------+
| 1 |
| 2 |
| 3 |
+------+
#查询部门id=2的所有下级部门和本级
with recursive
cte_tab as (
select
id,
dept_name,
parent_id,
1 as level
from
department
where
id = 2
union all
select
d.id,
d.dept_name,
d.parent_id,
level + 1
from
cte_tab c
inner join department d on c.id = d.parent_id
)
select
*
from
cte_tab;
针对 from 子句里面的 subquery,MySQL 在不同版本中,是做过一系列的优化,接下来我们就来看看。
MySQL 会对每一个 Derived Table 进行物化,生成一个临时表保存 Derived Table 的结果,然后利用临时表来完成父查询的操作,具体如下:
explain
select
*
from
(
select
*
from
department
where
id <= 1000
) t1
join (
select
*
from
department
where
id >= 990
) t2 on t1.id = t2.id;
MySQL 引入了 Derived Merge 新特性,允许符合条件的 Derived Table 中的子表与父查询的表进行合并,具体如下:
explain
select
*
from
(
select
*
from
department
where
id <= 1000
) t1
join (
select
*
from
department
where
id >= 990
) t2 on t1.id = t2.id;
我们可以使用 CTE 实现,其执行计划也是和 Derived Table 一样
explain
with
t1 as (
select
*
from
department
where
id <= 1000
),
t2 as (
select
*
from
department
where
id >= 990
)
select
*
from
t1
join t2 on t1.id = t2.id;
从测试结果来看,CTE 似乎是 Derived Table 的一个替代品?其实不是的,虽然 CTE 内部优化流程与 Derived Table 类似,但是两者还是区别的,具体如下: