MySQL公共表表达式(Common Table Expressions, CTE)

公共表表达式(Common Table Expressions, CTE)是MySQL在单一语句中执行过程中,预先定义的临时结果集。

有时我们需要在一个SQL中重复执行同一个子查询,而每次子查询都会重新计算结果,带来性能的浪费。而采用CTE可以在查询的一开始就定义好子查询的结果集,MySQL只会计算一次结果,然后在查询中使用CTE的名称可以反复引用。

目录

一、CTE定义及分类

二、普通CTE

2.1 普通CTE示例

2.2 CTE的使用场景

三、递归CTE

3.1 递归CTE示例

3.2 限制无限递归

四、一个递归CTE应用示例


一、CTE定义及分类

CTE的定义方式是在with子句后跟一个子查询,如果一个SQL中需要定义多个CTE,则用逗号分隔即可。

定义语法:

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

CTE分为两种:

  • 普通CTE:定义一个简单子查询
  • 递归CTE:定义时可以引用自己,产生一个递归的结果集

普通CTE和递归CTE的区别在于,递归CTE多了一个recursive关键字,且需要引用自己。

二、普通CTE

2.1 普通CTE示例

以下的演示SQL可以在在MySQL的官方示例数据库中执行:

with
cte1 as (select emp_no,first_name,last_name from employees where emp_no=10012), -- 定义cte1
cte2 as (select emp_no,dept_no from dept_emp)    -- 定义cte2
select cte1.emp_no,cte2.dept_no,cte1.first_name
from cte1
join cte2 on cte1.emp_no=cte2.emp_no;

示例中在select子句前定义了cte1和cte2(以逗号分隔),随后在select子句中可以直接引用cte1和cte2的名称进行查询。

cte定义时也可以引用其他cte,例如在上面的定义中,cte2的定义可以引用cte1:

with
cte1 as (select emp_no,first_name,last_name from employees where emp_no=10012),
cte2 as (select emp_no,last_name from cte1)    -- cte2的定义引用了cte1
select cte1.emp_no,cte2.last_name
from cte1
join cte2 on cte1.emp_no=cte2.emp_no;

注意之只有后定义的cte可以引用前面的定义的cte,如果把cte2定义位置调到前面,则会报错:cte1不存在.

cte定义的名称后面可以添加括号,显式定义cte的列名,但要和后面子查询返还的列数量相同:

with
cte1(col1, col2, col3) as (select emp_no,first_name,last_name from employees where emp_no=10012)
select col1, col2, col3    -- 引用定义的列名
from cte1;

此时后续cte则必须通过显示定义的列名来引用(col1, col2, col3),定义中子查询的列名不能再引用了。

2.2 CTE的使用场景

cte的定义不仅仅用在select中,也可以用在update/delete语句前,子查询中,以及其他可以嵌套select语句的地方(例如 insert …select):

  • WITH ... SELECT ...
  • WITH ... UPDATE ...
  • WITH ... DELETE …
  • SELECT ... WHERE id IN (WITH ... SELECT ...) ...
  • SELECT * FROM (WITH ... SELECT ...) AS dt ...
  • INSERT ... WITH ... SELECT ...
  • REPLACE ... WITH ... SELECT ...
  • CREATE TABLE ... WITH ... SELECT ...
  • CREATE VIEW ... WITH ... SELECT ...
  • DECLARE CURSOR ... WITH ... SELECT ...
  • EXPLAIN ... WITH ... SELECT ...

三、递归CTE

3.1 递归CTE示例

如果一个cte定义过程中引用了自己,则是递归cte,此时需要with recursive子句定义,其中recursive关键字是必须的。

递归cte包含2个部分,使用union all 或 union [distinct]连接:

with recursive
cte(n) as (
select 1
union all
select n+1 from cte where n<5)
select * from cte;

上述cte定义中第1部分生成了一条初始数据,union all后面的第二部分引用了cte自己,且递归执行,直到不再满足条件(n<5)。

1个递归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;

如上图所示,在strict SQL模式下,因为第二列以非递归部分的长度为准,递归后长度列的长度变长导致SQL直接报错。

而在非strict SQL模式下,以上SQL可以执行成功,但是第二列都被按非递归部分截断了,如下所示:

在遇到此类cte定义时,将非递归部分的列定义大一些,例如下面将'abc'的非递归部分加长,即可显示正确的递归结果:

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;

另外,对于递归cte的递归部分(即union后的SQL)还有部分使用限制:

  • 递归部分不能包含聚合函数、窗口函数、group by、order by、distinct
  • 递归部分引用自身只能引用一次且必须在from子句中,不能在子查询中。

3.2 限制无限递归

对于递归cte,如果没有加限制递归的条件,在逻辑上是可以无限递归的(死循环)。为了限制这种情况,MySQL有4种解决方式:

  • 使用参数cte_max_recursion_depth来限制最大递归的次数,超过递归深度强制终止。
  • 使用参数max_execution_time来限制最大的执行时间。
  • 使用优化器提示 MAX_EXECUTION_TIME来限制最大执行时间。
  • MySQL 8.0.19后,可以用limit子句限制最大返还行数。

示例:通过cte_max_recursion_depth限制递归次数,超过10次递归终止

set session cte_max_recursion_depth=10;  -- 全局默认值是1000,我们这里修改会话级为10次
with recursive
cte(n) as (
select 1
union all
select n+1 from cte)
select * from cte;

示例:超过10毫秒终止递归

set session cte_max_recursion_depth=100000;   -- 将递归次数增大,防止先触发
set session max_execution_time=10;    -- 将最大递归执行时长修改为10毫秒
with recursive
cte(n) as (
select 1
union all
select n+1 from cte)
select * from cte;

示例:使用优化器提示限制递归执行时间

with recursive
cte(n) as (
select 1
union all
select n+1 from cte)
select /*+ MAX_EXECUTION_TIME(10) */ * from cte;    -- 使用提示语法限制执行时间

四、一个递归CTE应用示例

假设我们有一张订单表,

create table orders (dt date,price decimal(10,2));
insert into orders values
('2022-01-01',100),
('2022-01-01',200),
('2022-01-03',200),
('2022-01-03',200),
('2022-01-05',300),
('2022-01-07',200);

现在要统计截止'2022-01-07'日的营业额,正常我们使用group by按日期汇集订单金额即可:

select dt, sum(price) sales from orders group by dt;

但是注意到由于2号/4号/6号没有订单,所以查询出来的结果中不包含这些日期,而通过递归cte我们可以先按日期递归,将这些日期列出来然后与orders连接:

with recursive cte(dt) as (
select min(dt) from orders
union all
select dt + interval 1 day from cte where dt <(select max(dt) from orders))
select e.dt,ifnull(sum(o.price),0) turnover
from cte e
left join orders o on o.dt=e.dt
group by e.dt
order by e.dt;

可以看到没有订单的日期也显示出来了,营业额显示为0,这个技巧在做报表类数据时很有用。

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
限时福利1:购课进答疑群专享柳峰(刘运强)老师答疑服务 为什么需要掌握高性能的MySQL实战? 由于互联网产品用户量大、高并发请求场景多,因此对MySQL的性能、可用性、扩展性都提出了很高的要求。使用MySQL解决大量数据以及高并发请求已经是程序员的必备技能,也是衡量一个程序员能力和薪资的标准之一。 为了让大家快速系统了解高性能MySQL核心知识全貌,我为你总结了「高性能 MySQL 知识框架图」,帮你梳理学习重点,建议收藏! 【课程设计】 课程分为四大篇章,将为你建立完整的 MySQL 知识体系,同时将重点讲解 MySQL 底层运行原理、数据库的性能调优、高并发、海量业务处理、面试解析等。 一、性能优化篇: 主要包括经典 MySQL 问题剖析、索引底层原理和事务与锁机制。通过深入理解 MySQL 的索引结构 B+Tree ,学员能够从根本上弄懂为什么有些 SQL 走索引、有些不走索引,从而彻底掌握索引的使用和优化技巧,能够避开很多实战中遇到的“坑”。 二、MySQL 8.0新特性篇: 主要包括窗口函数和通用表达式。企业中的许多报统计需求,如果不采用窗口函数,用普通的 SQL 语句是很难实现的。 三、高性能架构篇: 主要包括主从复制和读写分离。在企业的生产环境中,很少采用单台MySQL节点的情况,因为一旦单个节点发生故障,整个系统都不可用,后果往往不堪设想,因此掌握高可用架构的实现是非常有必要的。 四、面试篇: 程序员获得工作的第一步,就是高效的准备面试,面试篇主要从知识点回顾总结的角度出发,结合程序员面试高频MySQL问题精讲精练,帮助程序员吊打面试官,获得心仪的工作机会。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值