Kingbase-查询-WITH 查询(公共表表达式)

8 WITH 查询(公共表表达式)
WITH 提供了一种方式来书写在一个大型查询中使用的辅助语句。这些语句通常被称为公共表表达式或
CTE ,它们可以被看成是定义只在一个查询中存在的临时表。在 WITH 子句中的每一个辅助语句可以是一个
SELECT INSERT UPDATE DELETE ,并且 WITH 子句本身也可以被附加到一个主语句,主语句也可以
SELECT INSERT UPDATE DELETE
8.1 WITH 中的 SELECT
WITH SELECT 的基本价值是将复杂的查询分解称为简单的部分。一个例子:
WITH regional_sales AS (
SELECT region, SUM(amount) AS total_sales
FROM orders
GROUP BY region
), top_regions AS (
SELECT region
FROM regional_sales
WHERE total_sales > (SELECT SUM(total_sales) / 10 FROM regional_sales)
)
SELECT region,
product,
SUM(quantity) AS product_units,
SUM(amount) AS product_sales
FROM orders
WHERE region IN (SELECT region FROM top_regions)
GROUP BY region, product;
它只显示在高销售区域每种产品的销售总额。 WITH 子句定义了两个辅助语句 regional_sales
top_regions ,其中 regional_sales 的输出用在 top_regions 中而 top_regions 的输出用在
SELECT 查询。这个例子可以不用 WITH 来书写,但是我们必须要用两层嵌套的子 SELECT 。使用这种
方法要更简单些。
可选的 RECURSIVE 修饰符将 WITH 从单纯的句法便利变成了一种在标准 SQL 中不能完成的特性。通过使
RECURSIVE ,一个 WITH 查询可以引用它自己的输出。一个非常简单的例子是计算从 1 100 的整数合
的查询:
WITH RECURSIVE t(n) AS (
VALUES ( 1 )
UNION ALL
SELECT n + 1 FROM t WHERE n < 100
)
SELECT sum (n) FROM t;
一个递归 WITH 查询的通常形式总是一个非递归项,然后是 UNION (或者 UNION ALL ),再然后是一个递
归项,其中只有递归项能够包含对于查询自身输出的引用。这样一个查询可以被这样执行:
递归查询求值
1. 计算非递归项。对 UNION (但不对 UNION ALL ),抛弃重复行。把所有剩余的行包括在递归查询的
结果中,并且也把它们放在一个临时的 工作表 中。
2. 只要工作表不为空,重复下列步骤:
a. 计算递归项,用当前工作表的内容替换递归自引用。对 UNION (不是 UNION ALL ),抛弃重复
行以及那些与之前结果行重复的行。将剩下的所有行包括在递归查询的结果中,并且也把它们放
在一个临时的中间表中。
b. 用中间表的内容替换工作表的内容,然后清空中间表。
Note: 严格来说,这个处理是迭代而不是递归,但是 RECURSIVE SQL 标准委员会选择的术语。
在上面的例子中,工作表在每一步只有一个行,并且它在连续的步骤中取值从 1 100 。在第 100 步,由于
WHERE 子句导致没有输出,因此查询终止。
递归查询通常用于处理层次或者树状结构的数据。一个有用的例子是这个用于找到一个产品的直接或间接部
件的查询,只要给定一个显示了直接包含关系的表:
WITH RECURSIVE included_parts(sub_part, part, quantity) AS (
SELECT sub_part, part, quantity FROM parts WHERE part = 'our_product'
UNION ALL
SELECT p . sub_part, p . part, p . quantity
FROM included_parts pr, parts p
WHERE p . part = pr . sub_part
)
SELECT sub_part, SUM(quantity) as total_quantity
FROM included_parts
GROUP BY sub_part
在使用递归查询时,确保查询的递归部分最终将不返回元组非常重要,否则查询将会无限循环。在某些时候,
使用 UNION 替代 UNION ALL 可以通过抛弃与之前输出行重复的行来达到这个目的。不过,经常有循环不
涉及到完全重复的输出行:它可能只需要检查一个或几个域来看相同点之前是否达到过。处理这种情况的标
准方法是计算一个已经访问过值的数组。例如,考虑下面这个使用 link 域搜索表 graph 的查询:
WITH RECURSIVE search_graph( id , link, data, depth) AS (
SELECT g . id, g . link, g . data, 1
FROM graph g
UNION ALL
SELECT g . id, g . link, g . data, sg . depth + 1
FROM graph g, search_graph sg
WHERE g . id = sg . link
)
SELECT * FROM search_graph;
如果 link 关系包含环,这个查询将会循环。因为我们要求一个“ depth ”输出,仅仅将 UNION ALL 改为
UNION 不会消除循环。反过来在我们顺着一个特定链接路径搜索时,我们需要识别我们是否再次到达了一个
相同的行。我们可以项这个有循环倾向的查询增加两个列 path cycle
WITH RECURSIVE search_graph( id , link, data, depth, path, cycle) AS (
SELECT g . id, g . link, g . data, 1 ,
ARRAY[g . id],
false
FROM graph g
UNION ALL
SELECT g . id, g . link, g . data, sg . depth + 1 ,
path || g . id,
g . id = ANY(path)
FROM graph g, search_graph sg
WHERE g . id = sg . link AND NOT cycle
)
SELECT * FROM search_graph;
除了阻止环,数组值对于它们自己的工作显示到达任何特定行的“ path ”也有用。
在通常情况下如果需要检查多于一个域来识别一个环,请用行数组。例如,如果我们需要比较域 f1 f2
WITH RECURSIVE search_graph( id , link, data, depth, path, cycle) AS (
SELECT g . id, g . link, g . data, 1 ,
ARRAY[ROW(g . f1, g . f2)],
false
FROM graph g
UNION ALL
SELECT g . id, g . link, g . data, sg . depth + 1 ,
(continues on next page)
(continued from previous page)
path || ROW(g . f1, g . f2),
ROW(g . f1, g . f2) = ANY(path)
FROM graph g, search_graph sg
WHERE g . id = sg . link AND NOT cycle
)
SELECT * FROM search_graph;
Tip: 在通常情况下只有一个域需要被检查来识别一个环,可以省略 ROW() 语法。这允许使用一个简单的
数组而不是一个组合类型数组,可以获得效率。
递归查询计算算法使用宽度优先搜索顺序产生它的输出。你可以通过让外部查询 ORDER BY 一个以这种方
法构建的“ path ”,用来以深度优先搜索顺序显示结果。
当你不确定查询是否可能循环时,一个测试查询的有用技巧是在父查询中放一个 LIMIT 。例如,这个查询
没有 LIMIT 时会永远循环:
WITH RECURSIVE t(n) AS (
SELECT 1
UNION ALL
SELECT n + 1 FROM t
)
SELECT n FROM t LIMIT 100 ;
这会起作用,因为 KingbaseES 的实现只计算 WITH 查询中被父查询实际取到的行。不推荐在生产中使用这
个技巧,因为其他系统可能以不同方式工作。同样,如果你让外层查询排序递归查询的结果或者把它们连接
成某种其他表,这个技巧将不会起作用,因为在这些情况下外层查询通常将尝试取得 WITH 查询的所有输
出。
WITH 查询的一个有用的特性是在每一次父查询的执行中它们通常被计算一次,即使它们被父查询或兄弟
WITH 查询引用了超过一次。因此,在多个地方需要的昂贵计算可以被放在一个 WITH 查询中来避免冗余工
作。另一种可能的应用是阻止不希望的多个函数计算产生副作用。然而 , 硬币的另一面是 , 优化器不能将父查
询的限制推入一个多基准 WITH 查询 , 因为这可能会影响所有使用 WITH 查询的输出 , 它应该只影响一个。多
基准的 WITH 查询将会被按照所写的方式计算,而不抑制父查询以后可能会抛弃的行(但是,如上所述,如
果对查询的引用只请求有限数目的行,计算可能会提前停止)。
但是,如果一个 WITH 查询是非递归的,并且没有副作用 ( 也就是说,它是一个 SELECT 不包含 volatile 函数 )
那么它可以被折叠到父查询中,从而允许两个查询级别的联合优化。默认情况下,它发生于父查询引用 WITH
查询查询仅一次,如果它引用和查询不止一次,则不会发生这种情况。您可以通过指定 MATERIALIZED
强制使用 WITH 查询分离计算,或者通过指定 NOT MATERIALIZED 来强制合并到父查询中,从而推翻这
个决定。后一种选择有 WITH 查询重复计算的风险,但是如果每次使用 WITH 查询只需要使用 WITH 查询
的全部输出,它仍然可以提供净节省。
这些规则的一个简单例子是
WITH w AS (
SELECT * FROM big_table
)
SELECT * FROM w WHERE key = 123 ;
这个 WITH 查询将被折叠起来,生成与执行计划相同的执行计划
SELECT * FROM big_table WHERE key = 123 ;
特别是,如果 key 上有一个索引,那么它可能只用于获取具有 key = 123 的行。另一方面,在
WITH w AS (
SELECT * FROM big_table
)
SELECT * FROM w AS w1 JOIN w AS w2 ON w1 . key = w2 . ref
WHERE w2 . key = 123 ;
WITH 查询将被实例化 , 生成 big_table 的临时副本,然后与自身连接(没有任何索引的好处)。如果将此
查询编写为
WITH w AS NOT MATERIALIZED (
SELECT * FROM big_table
)
SELECT * FROM w AS w1 JOIN w AS w2 ON w1 . key = w2 . ref
WHERE w2 . key = 123 ;
因此,父查询的限制可以直接应用于 big_table 的扫描。
例如, NOT MATERIALIZED 可能不受欢迎
WITH w AS (
SELECT key, very_expensive_function(val) as f FROM some_table
)
SELECT * FROM w AS w1 JOIN w AS w2 ON w1 . f = w2 . f;
在这里,使用 WITH 查询实现 very_expensive_function 对于每个表行只计算一次,而不是两次。
以上的例子只展示了和 SELECT 一起使用的 WITH ,但是它可以被以相同的方式附加在 INSERT UPDATE
DELETE 上。在每一种情况中,它实际上提供了可在主命令中引用的临时表。
8.2 WITH 中的数据修改语句
可以在 WITH 中使用数据修改语句( INSERT UPDATE DELETE )。这允许你在同一个查询中执行多个
而不同操作。一个例子:
WITH moved_rows AS (
DELETE FROM products
WHERE
"date" >= '2010-10-01' AND
"date" < '2010-11-01'
RETURNING *
)
INSERT INTO products_log
SELECT * FROM moved_rows;
这个查询实际上从 products 把行移动到 products_log WITH 中的 DELETE 删除来自 products
的 指 定 行, 以 它 的 RETURNING 子 句 返 回 它 们 的 内 容, 并 且 接 着 主 查 询 读 该 输 出 并 将 它 插 入 到
products_log
上述例子中好的一点是 WITH 子句被附加给 INSERT ,而没有附加给 INSERT 的子 SELECT 。这是必需的,
因为数据修改语句只允许出现在附加给顶层语句的 WITH 子句中。不过,普通 WITH 可见性规则应用,这
样才可能从子 SELECT 中引用到 WITH 语句的输出。
正如上述例子所示, WITH 中的数据修改语句通常具有 RETURNING 子句(见 从修改的行中返回数据 )。它
RETURNING 子句的输出,不是数据修改语句的目标表,它形成了剩余查询可以引用的临时表。如果一个
WITH 中的数据修改语句缺少一个 RETURNING 子句,则它形不成临时表并且不能在剩余的查询中被引用。
但是这样一个语句将被执行。一个非特殊使用的例子:
WITH t AS (
DELETE FROM foo
)
DELETE FROM bar;
这个例子将从表 foo bar 中移除所有行。被报告给客户端的受影响行的数目可能只包括从 bar 中移除
的行。
数据修改语句中不允许递归自引用。在某些情况中可以采取引用一个递归 WITH 的输出来操作这个限制,例
如:
WITH RECURSIVE included_parts(sub_part, part) AS (
SELECT sub_part, part FROM parts WHERE part = 'our_product'
UNION ALL
SELECT p . sub_part, p . part
FROM included_parts pr, parts p
WHERE p . part = pr . sub_part
)
DELETE FROM parts
WHERE part IN (SELECT part FROM included_parts);
这个查询将会移除一个产品的所有直接或间接子部件。
WITH 中的数据修改语句只被执行一次,并且总是能结束,而不管主查询是否读取它们所有(或者任何)的
输出。注意这和 WITH SELECT 的规则不同:正如前一小节所述,直到主查询要求 SELECT 的输出时,
SELECT 才会被执行。
WITH 中的子语句彼此并发执行,并与主查询一起执行。因此在使用 WITH 中的数据修改语句时,指定更新
的顺序实际是以不可预测的方式发生的。所有的语句都使用同一个 snapshot 执行,因此它们不能“看见”在
目标表上另一个执行的效果。这减轻了行更新的实际顺序的不可预见性的影响,并且意味着 RETURNING
据是在不同 WITH 子语句和主查询之间传达改变的唯一方法。其例子
WITH t AS (
UPDATE products SET price = price * 1.05
RETURNING *
)
SELECT * FROM products;
外层 SELECT 可以返回在 UPDATE 动作之前的原始价格,而在
WITH t AS (
UPDATE products SET price = price * 1.05
RETURNING *
)
SELECT * FROM t;
外部 SELECT 将返回更新过的数据。
在一个语句中试图两次更新同一行是不被支持的。只会发生一次修改,但是该办法不能很容易地(有时是不
可能)可靠地预测哪一个会被执行。这也应用于删除一个已经在同一个语句中被更新过的行:只有更新被执
行。因此你通常应该避免尝试在一个语句中尝试两次修改同一个行。尤其是防止书写可能影响被主语句或兄
弟子语句修改的相同行。这样一个语句的效果将是不可预测的。
当前,在 WITH 中一个数据修改语句中被用作目标的任何表不能有条件规则、 ALSO 规则或 INSTEAD 规则,
这些规则会扩展成为多个语句。
  • 22
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值