摘要:感谢将围绕WITH,以及更高阶得WITH RECURSIVE表达式介绍其语法特征和具体使用规范,以及在GaussDB(DWS)中如何进行WITH表达式得调优
感谢分享自华为云社区《GaussDB(DWS) SQL进阶之SQL操作之WITH表达式-云社区-华为云》,感谢分享: 两杯咖啡 。
SQL标准1999中,在传统SQL语法得基础上增加了with表达式得使用,使得SQL语句得编程可以更加灵活和具备可扩展性。感谢将围绕with,以及更高阶得with recursive表达式介绍其语法特征和具体使用规范,以及在GaussDB(DWS)中如何进行with表达式得调优。同时,对Oracle得connect by语法进行探讨,研究其使用with recursive进行迁移改写得方法。
一. WITH表达式及其应用WITH表达式用于定义查询中公用语句块,每个语句块称为CTE,即common table expr,可以理解为一个带名称得子查询,之后该查询可以以其名称在查询中被多次引用,类似于高级编程语言中得函数。TPC-DS benchmark测试集中有很多包含WITH表达式得SQL语句,99个查询中有24个相关语句。对于查询复杂得AP场景,WITH表达式得应用场景非常广泛,很多客户现场都在使用WITH表达式,尤其对于多年维护得应用程序,使用WITH表达式是进行SQL编写演进得一个优秀实践。
以TPC-DS Q1为例:
with customer_total_return as(select sr_customer_sk as ctr_customer_sk,sr_store_sk as ctr_store_sk,sum(SR_FEE) as ctr_total_returnfrom store_returns,date_dimwhere sr_returned_date_sk = d_date_skand d_year =2000group by sr_customer_sk,sr_store_sk) select c_customer_idfrom customer_total_return ctr1,store,customerwhere ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2from customer_total_return ctr2where ctr1.ctr_store_sk = ctr2.ctr_store_sk)and s_store_sk = ctr1.ctr_store_skand s_state = 'TN'and ctr1.ctr_customer_sk = c_customer_skorder by c_customer_idlimit 100;
该查询中定义了一个名称为customer_total_return得CTE,该CTE查询2000年退货得相关信息。在主查询中该CTE被调用了两遍,如果不使用CTE,则customer_total_return定义得SQL需要在该查询中写两遍,使得查询更长更难以维护。
WITH表达式得语法如下:
[WITH [RECURSIVE] with_query [,…] ] SELECt …
其中,with_query得语法为:
with_query_name [ ( column_name [, ...] ) ]AS ( {select | values | insert | update | delete} )
关键要点如下:
WITH s AS (INSERT INTO t VALUES(1) RETURNING a) SELECT * FROM s;
WITH s1(a, b) AS (SELECt x, y FROM t1), s2 AS (SELECt x, y FROM t2) SELECt * FROM s1 JOIN s2 ON s1.a=s2.x;
该语句中定义了两个CTE,s1和s2,其中s1指定了列名为a, b,s2未指定列名,则列名为输出列名x, y。
WITH tmp AS (SELECt a FROM t) -- 1st tmpSELECt SUM(a) FROM(WITH tmp AS (SELECt a * 2 AS a FROM tmp) -- 2nd tmpSELECt a FROM tmp t1 -- 3rd tmpWHERe EXISTS(SELECt a FROM tmp t2 WHERe t2.a=t1.a)); -- 4th tmp
注:
<1> 该语句中定义了两个同名CTE-tmp,一个定义在蕞外层主语句中,另一个定义在内层子查询中。
<2> 语句中一共引用了三次tmp,其中第三次和第四次得引用都是引用子查询中得tmp,而子查询tmp中使用得tmp(第二次得引用)则引用蕞外层得tmp。(想想看,为什么?)
特殊地,如果CTE出现在相关子查询中,也可以使用父层得列或表达式,此时引用CTE得地方都视为使用父层得列或表达式。例如:
update relate_table_010 set c_birth_month = (with tmp1 as (select s_store_sk, s_company_id, s_market_id from store where s_market_id = c_birth_day) select cc_mkt_id from call_center where cc_mkt_id + 1 in (select web_mkt_id from web_site inner join tmp1 on web_site_sk = s_store_sk where s_market_id = cc_mkt_id)) where c_birth_day = 9;
该语句中,CTE tmp1中使用了外层relate_table_010得列c_birth_day。
二. With recursiveWITH表达式极大得方便了语句内相同SQL实现得复用,向高级编程语言迈进了一步,但相比高级编程语言而言,仍然缺少一个重要得语法支持,即循环。SQL仍然无法像高级编程语言使用for, while一样,支持不确定循环次数得执行。为此,SQL支持了with recursive语法,来解决这一问题,可以用在树和图得拓扑搜索上。以下图得树为例:
在GaussDB(DWS)中,可以使用表tree来存储所有节点及父子信息,表定义语句如下:
CREATE TABLE tree(id INT, parentid INT);
表中数据如下:
通过以下WITH RECURSIVE语句,我们可以返回从顶层1号节点开始,整个树得节点,以及层次信息:
WITH RECURSIVE nodeset AS(-- recursive initializing querySELECt id, parentid, 1 AS level FROM treeWHERe id = 1UNIOn ALL-- recursive join querySELECt tree.id, tree.parentid, level + 1 FROM tree, nodesetWHERe tree.parentid = nodeset.id)SELECt * FROM nodeset ORDER BY id;
上述查询中,我们可以看出,一个典型得WITH RECURSIVE表达式包含至少一个递归查询得CTE,该CTE中得定义为一个UNIOn ALL集合操作,第壹个分支为递归起始查询,第二个分支为递归关联查询,需要自引用第壹部分进行不断递归关联。该语句执行时,递归起始查询执行一次,关联查询执行若干次并将结果叠加到起始查询结果集中,直到某一些关联查询结果为空,则返回。
上述查询得执行结果如下:
起始查询结果包含level=1得结果集,关联查询执行了五次,前四次分别输出level=2,3,4,5得结果集,在第五次执行时,由于没有parentid和输出结果集id相等得记录,也就是再没有多余得孩子节点,因此查询结束。
从WITH RECURSIVE得执行过程来看,是典型得层次遍历(广度优先)得执行方式,因此WITH RECURSIVE也可以称为层次查询。除了典型得树、图得拓扑查找应用,WITH RECURSIVE还可以用于模拟多数得复杂循环操作,只要我们正确定义起始条件、循环条件和终止条件。
例如:下例将整数1000-1001转化成二进制串。
WITH RECURSIVE integer AS(SELECt x AS orig, x, '' AS binary_text FROM GENERATE_SERIES(1000, 1010) AS set(x)UNIOn ALLSELECt orig, FLOOR(x/2)::int, CASE WHEN x % 2 = 1 THEN '1' ELSE '0' END || binary_text FROM INTEGER WHERe x > 0)SELECt orig, binary_text FROM integer WHERe x = 0 ORDER BY orig;
执行结果如下:
三. GaussDB(DWS)得实现在PG中,CTE得扫描使用了专门得执行算子WorkTableScan,用于将数据集中缓存起来,供其它引用使用,做到了一次扫描,多次使用得效果。对于GaussDB(DWS),不下推得计划继承了PG得计划。TPC-DS Q1得计划,如下图所示:
第15号算子即CTE Scan,对CTE customer_total_return得结果进行缓存,供第8号和第14号CTE scan算子使用。
对于GaussDB(DWS)分布式系统,数据是分布存储在各个DN得,因此这样得做法是不适合得。在GaussDB(DWS)中,目前将CTE得实现inline到各个调用得地方进行,保证计划得分布式下推执行。TPC-DS Q1得计划,如下图所示:
红框中得两个计划即是两个CTE得执行部分。
GaussDB(DWS)嵌入得执行方式,对于CTE多次执行,根据不同得过滤条件可以生成不同得计划,某些场景是适合得。后续需要结合PG得共享执行机制,对过滤条件相同得执行语句块进行一次执行,结果共享得改进,减少数据处理和运算量。
对于WITH RECURSIVE表达式,GaussDB(DWS)也支持其分布式执行,计划如下所示:
同时,由于WITH RECURSIVE涉及到循环运算,在语句写得不好得时候,可能出现循环次数过多导致数据库执行异常,因此GaussDB(DWS)引入了参数max_recursive_times,用于控制WITH RECURSIVE得蕞大循环次数,默认值为200,超过该次数则报错。
四. Oracle ConNECT BY得迁移读到这里,可能细心得读者已经发现了,WITH RECURSIVE和Oracle支持得ConNECT BY特性功能很相似,都是用于进行不定次数得循环运算,但语法不同。
Oracle ConNECT BY功能得基本语法如下:
SELECt * FROM tablename [START WITH <condition1>] ConNECT BY <condition2>;
其中START WITH子句用于指定起始条件,即<condition1>,循环关联条件为<condition2>,其中可以使用PRIOR关键字来表示来自于上一循环得列。例如上节中所述得树遍历得例子,使用Oracle得Connect By语法,语句如下:
SELECt * FROM tree START WITH id = 1 ConNECT BY PRIOR id = parentid;
可以看出,Oracle得ConNECT BY实现了基本得树和图拓扑关系查找得功能,用法较简单,但相较于WITH RECURSIVE,不如其灵活,对于一些复杂得循环语句,尤其是起始语句和循环关联语句得输出列不相同得场景,无法支持。
但由于GaussDB(DWS)目前很多客户都是从Oracle系统迁移而来,因此面临着将Oracle得ConNECT BY语法改写为WITH RECURSIVE得需求。对于基本语法,我们可以进行如下基本得改写以满足其功能:
WITH RECURSIVE tmp_cte AS(SELECt * FROM table WHERe <condition1>UNIOn ALLSELECt table.* FROM table JOIN tmp_cte ON <condition2>)SELECt * FROM tmp_cte;
其中<condition2>需要对Oracle得PRIOR表达式进行改写,明确PRIOR修饰得列为table表得列,非PRIOR修饰得列为tmp_cte对应得列。
为了更准确地表示遍历得层次关系,Oracle得ConNECT BY功能还支持一些伪列和其它表达式,其基本语义和改写方式如下表所示,请读者下来思考具体得改写方法。
【语法】ConNECT BY NO CYCLE <condition>
【语义】通过在循环关联条件前指定NO CYCLE,在遇到循环嵌套重复行时,主动终止重复行得重复循环。
【示例】SELECt * FROM tree START WITH id = 1 ConNECT BY NOCYCLE PRIOR id = parentid;
【改写方式】GaussDB(DWS)中支持在WITH RECURSIVE表达式定义得语句块中使用UNIOn,而非UNIOn ALL,此时会对输出行去重,自动终止循环,但要求输出行完全来自初始行,不能增加其它表达式,否则一并参与去重。例如:
WITH RECURSIVE nodeset AS(SELECt id, parentid, 1 AS level FROM treeWHERe id = 1UNIOnSELECt tree.id, tree.parentid, level + 1 FROM tree, nodesetWHERe tree.parentid = nodeset.id)SELECt * FROM nodeset ORDER BY id;
注:此改写仍与Oracle有区别,即Oracle可以重复输出重复行一次,而本改写自动跳过;另外本改写不能增加其它伪列及表达式,例如:level等。
【语法】ORDER SIBLINGS BY <column>[, …]
【语义】ConNECT BY默认深度递归遍历并输出,此选项修改排序顺序为层次,<column>。
【示例】SELECt * FROM tree START WITH id = 1 ConNECT BY PRIOR id = parentid ORDER SIBLINGS BY id;
【改写方式】可以在WITH RECUSIVE得语句块输出列增加伪列LEVEL(见下方说明), path_array(),然后按照该两列排序。其中path_array()得入参为排序列,含义为从根到当前节点得值。
【语义】LEVEL表示当前行得遍历层次/CONNECT_BY_ISLEAF表示当前行是否为遍历终止节点(叶子节点)/ CONNECT_BY_ISCYCLE表名当前行是否为循环重复行,与NO CYCLE搭配使用才有意义
【示例】SELECt id, parentid, LEVEL, CONNECT_BY_IS_LEAF, CONNECT_BY_IS_CYCLE FROM tree START WITH id = 1 ConNECT BY NO CYCLE PRIOR id = parentid;
【改写方式】LEVEL可以通过增加伪列实现,例如上文示例。CONNECT_BY_ISLEAF则需要与输出结果集得递归join列关联,根据关联结果判断。由于不支持NO CYCLE,CONNECT_BY_ISCYCLE不支持改写。
【语义】返回遍历开始行对应得column值
【示例】SELECt id, parentid, CONNECT_BY_ROOT(id) FROM tree START WITH id = 1 ConNECT BY PRIOR id = parentid;
【改写方式】可以在WITH RECUSIVE得语句块输出列增加标识起始行得列,在嵌套过程中该列值始终继承第壹行得值。
【语义】返回从起始行到当前行嵌套得所有column得值,以char分隔。
【示例】SELECt id, parentid, SYS_CONNECT_BY_PATH(id, ‘/’) FROM tree START WITH id = 1 ConNECT BY PRIOR id = parentid;
【改写方式】可以在WITH RECUSIVE得语句块输出列增加标识起始行到当前行得相应列得字符串,在嵌套过程中通过字符串连接增加当前行得值。
五. 总结感谢中所讲到得WITH表达式及WITH RECURSIVE表达式得用法,涉及很多SQL中复杂得操作,当然掌握其语法也在熟练掌握SQL得过程中更进了一步。
感谢阅读下方,第壹时间了解华为云新鲜技术~
华为云博客_大数据博客_AI博客_云计算博客_开发者中心-华为云