公用表表达式(COMMON TABLE EXPRESSION)简称CTE,是一个临时命名的结果集,用于简化SQL。MaxCompute支持标准SQL的CTE功能,可以有效提高SQL语句的可读性与执行效率。本文为您介绍CTE的功能、命令格式及使用示例。
功能介绍
CTE可以被认为是在单个DML语句的执行范围内定义的临时结果集。CTE类似于派生表,它不作为对象存储,并且仅在查询期间持续。开发过程中结合CTE,可以提高SQL语句可读性,便于轻松维护复杂查询。
CTE是一个STATEMENT级别的子句表达式,以WITH开头,后跟表达式名称。包括以下两类:
NON RECURSIVE CTE:非递归CTE,即CTE不使用递归,不迭代访问自己。
RECURSIVE CTE:递归CTE,表示CTE可以迭代访问自身的场景,能够实现SQL的递归查询功能,通常用来遍历分层数据。
支持MATERIALIZE CTE(物化CTE)功能:定义CTE时,可在SELECT语句中使用MATERIALIZE HINT将CTE的计算结果缓存到一个临时表中,后续访问CTE时,可直接从缓存中读取结果,从而避免了多层CTE嵌套场景下出现内存超限问题,进而提升CTE语句性能。
NON RECURSIVE CTE(非递归CTE)
命令格式
WITH
<cte_name> [(col_name[,col_name]...)] AS
(
<cte_query>
)
[,<cte_name> [(col_name[,col_name]...)] AS
(
<cte_query2>
)
,……]
参数说明
参数 | 是否必填 | 说明 |
cte_name | 是 | CTE的名称,不能与当前 |
col_name | 否 | CTE输出列的列名称。 |
cte_query | 是 | 一个 |
使用示例
假设现有如下代码:
INSERT OVERWRITE TABLE srcp PARTITION (p='abc')
SELECT * FROM (
SELECT a.key, b.value
FROM (
SELECT * FROM src WHERE key IS NOT NULL ) a
JOIN (
SELECT * FROM src2 WHERE value > 0 ) b
ON a.key = b.key
) c
UNION ALL
SELECT * FROM (
SELECT a.key, b.value
FROM (
SELECT * FROM src WHERE key IS NOT NULL ) a
LEFT OUTER JOIN (
SELECT * FROM src3 WHERE value > 0 ) b
ON a.key = b.key AND b.key IS NOT NULL
) d;
顶层的UNION
两侧各为一个JOIN
,JOIN
的左表是相同的查询语句。通过写子查询的方式,只能重复这段代码。
使用CTE的方式重写以上语句,命令示例如下:
WITH
a AS (SELECT * FROM src WHERE key IS NOT NULL),
b AS (SELECT * FROM src2 WHERE value > 0),
c AS (SELECT * FROM src3 WHERE value > 0),
d AS (SELECT a.key, b.value FROM a JOIN b ON a.key=b.key),
e AS (SELECT a.key, c.value FROM a LEFT OUTER JOIN c ON a.key=c.key AND c.key IS NOT NULL)
INSERT OVERWRITE TABLE srcp PARTITION (p='abc')
SELECT * FROM d UNION ALL SELECT * FROM e;
重写后,a
对应的子查询只需写一次,便可在后面进行重用。您可以在CTE的WITH
子句中指定多个子查询,像使用变量一样在整个语句中反复重用。除重用外,不必反复嵌套。
RECURSIVE CTE(递归CTE)
命令格式
WITH RECURSIVE <cte_name>[(col_name[,col_name]...)] AS
(
<initial_part> UNION ALL <recursive_part>
)
SELECT ... FROM ...;
参数说明
参数 | 是否必填 | 说明 |
RECURSIVE | 是 | Recursive CTE子句必须以 |
cte_name | 是 | CTE的名称,不能与当前 |
col_name | 否 | CTE输出列的列名称。若不显式地指定输出列名,则系统也支持自动推断。 |
initial_part | 是 | 用于计算初始数据集,它不可以递归地引用cte_name指向CTE自身。 |
recursive_part | 是 | 用于计算后续迭代的结果,可以通过递归地引用cte_name,定义如何使用上一个迭代结果来递归地计算下一个迭代结果。 |
UNION ALL | 是 | initial_part和recursive_part之间必须通过UNION ALL来连接。 |
使用限制
RECURSIVE CTE不能用于IN/EXISTS/SCALAR的SUB-QUERY。
RECURSIVE CTE的默认递归操作次数限制为10次,支持设置
odps.sql.rcte.max.iterate.num
来修改这个默认值,最大上限为100(若设置值超过100,仍按照100来处理)。RECURSIVE CTE不支持在迭代计算过程中记录中间结果,即发生失败时则会重新从头开始计算。使用时建议控制递归次数,若整个计算过程耗时很长,建议通过创建临时表来把中间的计算结果落盘。
RECURSIVE CTE不支持在查询加速(MCQA)执行。若在MCQA模式中使用RECURSIVE CTE,当设置了
interactive_auto_rerun=true
,任务可以回退到普通模式执行,否则任务会失败。
使用示例
示例1:定义一个名称为cte_name的RECURSIVE CTE。
-- 方式1:定义一个名称为cte_name的递归CTE,包含名为a,b的两个输出列 WITH RECURSIVE cte_name(a, b) AS ( SELECT 1L, 1L -- intial_part:即迭代0数据集 UNION ALL -- 将cte的initial_part和recursive_part之间使用UNION ALL连接到一起 SELECT a+1, b+1 FROM cte_name WHERE a+1 <= 5) -- recursive_part:其中cte_name指向的是上一个迭代的计算结果 SELECT * FROM cte_name ORDER BY a LIMIT 100; -- 输出递归CTE结果,即将所有迭代的数据union all到一起 -- 方式2:定义一个名称为cte_name的递归CTE,不显示指定输出列名称 WITH RECURSIVE cte_name AS ( SELECT 1L AS a, 1L AS b UNION ALL SELECT a+1, b+1 FROM cte_name WHERE a+1 <= 5) SELECT * FROM cte_name ORDER BY a LIMIT 100;
说明在recursive_part中为了避免递归操作陷入无限循环,需要设置迭代终止的条件。示例中使用
WHERE a + 1 <= 5
来作为迭代是否应该结束的判据,如果WHERE条件不满足会导致本轮迭代生成的数据集为空,则迭代终止。当未显式地指定输出列名时,系统也支持进行自动推断。例如方式2中,使用initial_part的输出列名作为RECURSIVE CTE的输出列名。
返回结果如下:
+------------+------------+ | a | b | +------------+------------+ | 1 | 1 | | 2 | 2 | | 3 | 3 | | 4 | 4 | | 5 | 5 | +------------+------------+
示例2:RECURSIVE CTE不能被用于IN/EXISTS/SCALAR的SUB-QUERY中,如下query在编译的时候会报错。
WITH RECURSIVE cte_name(a, b) AS ( SELECT 1L, 1L UNION ALL SELECT a+1, b+1 FROM cte_name WHERE a+1 <= 5) SELECT x, x in (SELECT a FROM cte_name) FROM VALUES (1L), (2L) AS t(x);
返回结果如下:
-- 返回报错,在第5行存在一个in sub-query,且在sub-query中引用了RECURSIVE CTE cte_name FAILED: ODPS-0130071:[5,31] Semantic analysis exception - using Recursive-CTE cte_name in scalar/in/exists sub-query is not allowed, please check your query, the query text location is from [line 5, column 13] to [line 5, column 40]
示例3:创建employees表,描述某个公司的员工和他的管理者的对应关系,并插入数据。
CREATE TABLE employees(name STRING, boss_name STRING); INSERT INTO TABLE employees VALUES ('zhang_3', null), ('li_4', 'zhang_3'), ('wang_5', 'zhang_3'), ('zhao_6', 'li_4'), ('qian_7', 'wang_5');
定义一个名为
company_hierarchy
的RECURSIVE CTE,以获取此表所描述的组织架构。该RECURSIVE CTE包含3个输出字段,分别是员工的名字、他的管理者的名字以及他在公司组织架构中的level。WITH RECURSIVE company_hierarchy(name, boss_name, level) AS ( SELECT name, boss_name, 0L FROM employees WHERE boss_name IS NULL UNION ALL SELECT e.name, e.boss_name, h.level+1 FROM employees e JOIN company_hierarchy h WHERE e.boss_name = h.name ) SELECT * FROM company_hierarchy ORDER BY level, boss_name, name LIMIT 1000;
第二行initial_part,对应着employees中boss_name是null的记录,并且给这些记录的level字段赋值为0。
第四行recursive_part,将employees和company_hierarchy进行
JOIN
操作,JOIN的条件是e.boss_name = h.name
,即从employees表中获取这些记录,这些记录对应的员工的管理者是上一个迭代获取的员工记录。
返回结果如下:
+------+-----------+------------+ | name | boss_name | level | +------+-----------+---------------+ | zhang_3 | NULL | 0 | | li_4 | zhang_3 | 1 | | wang_5 | zhang_3 | 1 | | zhao_6 | li_4 | 2 | | qian_7 | wang_5 | 2 | +------+-----------+------------+
整体计算过程可分解如下:
迭代0:获取初始数据集,通过条件
boss_name IS NULL
对employees中的记录进行过滤,得到一条记录。'zhang_3', null, 0
迭代1:执行如下query,其中company_hierarchy对应于上面迭代0的计算结果。
SELECT e.name, e.boss_name, h.level+1 FROM employees e JOIN company_hierarchy h WHERE e.boss_name = h.name
执行后获取到了level = 1的两条记录,他们的管理者都是zhang_3。
'li_4', 'zhang_3', 1 'wang_5', 'zhang_3', 1
迭代2:执行的过程和迭代1类似,但是company_hierarchy对应于迭代1的结果数据集。执行后获取了
level = 2
的两条记录,它们的管理者是li_4或者wang_5。'zhao_6', 'li_4', 2 'qian_7', 'wang_5', 2
迭代3:因为employees表中没有员工的管理者是zhao_6或者qian_7,因此返回空集,导致Recursive-CTE的递归计算结束。
示例4:假设向示例2的employees表中多插入一条记录,则返回报错。
INSERT INTO TABLE employees VALUES('qian_7', 'qian_7');
这条记录声明qian_7的主管是他自己,这时再运行前面定义的RECURSIVE CTE,就会出现死循环。注意,系统对最大迭代次数有一定限制(请参见使用限制),这个query最终会失败并结束。
返回结果如下:
-- 返回报错 FAILED: ODPS-0010000:System internal error - recursive-cte: company_hierarchy exceed max iterate number 10
MATERIALIZE CTE
背景介绍
对于NON RECURSIVE CTE,MaxCompute在生成执行计划的时候,所有的CTE都会被展开。
示例如下:
WITH
v1 AS (SELECT SIN(1.0) AS a)
SELECT a FROM v1 UNION ALL SELECT a FROM v1;
返回结果如下:
+------------+
| a |
+------------+
| 0.8414709848078965 |
| 0.8414709848078965 |
+------------+
在执行层面相当于如下SQL,即函数sin(1.0)
会被执行两次。
SELECT a FROM (SELECT SIN(1.0) AS a)
UNION ALL
SELECT a FROM (SELECT SIN(1.0) AS a);
在有多层嵌套CTE的复杂场景下,若所有的CTE被展开成最基本的叶子节点,所生成的结果是一个巨大的语法树。在生成执行计划时可能因语法树节点过多导致失败,并可能引发内存超限。示例如下:
WITH
v1 AS (SELECT 1L AS a, 2L AS b, 3L AS c),
v2 AS (SELECT * FROM v1 UNION ALL SELECT * FROM v1 UNION ALL SELECT * FROM v1),
v3 AS (SELECT * FROM v2 UNION ALL SELECT * FROM v2 UNION ALL SELECT * FROM v2),
v4 AS (SELECT * FROM v3 UNION ALL SELECT * FROM v3 UNION ALL SELECT * FROM v3),
v5 AS (SELECT * FROM v4 UNION ALL SELECT * FROM v4 UNION ALL SELECT * FROM v4),
v6 AS (SELECT * FROM v5 UNION ALL SELECT * FROM v5 UNION ALL SELECT * FROM v5),
v7 AS (SELECT * FROM v6 UNION ALL SELECT * FROM v6 UNION ALL SELECT * FROM v6),
v8 AS (SELECT * FROM v7 UNION ALL SELECT * FROM v7 UNION ALL SELECT * FROM v7),
v9 AS (SELECT * FROM v8 UNION ALL SELECT * FROM v8 UNION ALL SELECT * FROM v8)
SELECT * FROM v9;
为了解决上述场景中的问题,MaxCompute提供了MATERIALIZE CTE能力,支持将CTE计算结果缓存,以供WITH语句外层的SQL引用,而无需全部展开。这一机制能够有效避免嵌套CTE展开而导致的内存超限问题,从而提升CTE的语句性能。
使用示例
定义CTE时,可在SELECT语句中使用MATERIALIZE HINT提示 /*+ MATERIALIZE */
将CTE的计算结果缓存到一张临时表中,后续引用时候,可以直接从缓存中读取结果,不需要重新计算。示例如下:
WITH
v1 AS (SELECT /*+ MATERIALIZE */ SIN(1.0) AS a)
SELECT a FROM v1 UNION ALL SELECT a FROM v1;
-- 返回结果如下。
+------------+
| a |
+------------+
| 0.8414709848078965 |
| 0.8414709848078965 |
+------------+
当MATERIALIZE HINT生效时,在LogView的Job Details页签中可以看到,中间的结果落盘,对应提交了多个Fuxi Job。
使用限制
MATERIALIZE HINT必须应用于NON RECURSIVE CTE顶级的SELECT语句中,递归CTE不需要使用MATERIALIZE HINT。
错误示例:如下CTE中的顶级语句是UNION而不是SELECT,所以MATERIALIZE HINT不会生效。
WITH v1 AS (SELECT /*+ MATERIALIZE */ SIN(1.0) AS a UNION ALL SELECT /*+ MATERIALIZE */ SIN(1.0) AS a) SELECT a FROM v1 UNION ALL SELECT a FROM v1;
在LogView中可以看到,对应仅提交了一个Fuxi Job,如下图所示。
正确示例:将上述错误示例改写为子查询来解决。
WITH v1 AS (SELECT /*+ MATERIALIZE */ * FROM (SELECT SIN(1.0) AS a UNION ALL SELECT SIN(1.0) AS a) ) SELECT a FROM v1 UNION ALL SELECT a FROM v1;
如果CTE中使用了非确定性的函数,例如RAND函数或者是用户自定义的非确定性的Java/Python UDF,将CTE修改为MATERIALIZE CTE之后,缓存机制可能会导致最终计算结果发生变化。
MATERIALIZE CTE不支持在开启查询加速(MCQA)模式中执行。若用户在MCQA模式中使用MATERIALIZE CTE,当设置了
interactive_auto_rerun=true
,任务可以回退到普通模式执行。否则任务会失败。