pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。
WITH Queries (Common Table Expressions) #WITH提供了一种为更大的SELECT查询编写子查询的方法。这些子查询通常被称为公共表表达式或CTE,可以视为仅在此查询期间存在的临时表定义。该特性的一个用途是把复杂的查询分解成更简单的部分。例如:
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来编写,但那样就需要两层嵌套的子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),然后是一个递归项,其中只有递归项才能包含对查询自身输出的引用。这样的查询按以下方式执行:
递归查询求值
计算非递归项。对UNION(但不对UNION ALL),抛弃重复行。把所有剩余的行包括在递归查询的结果中,并且也把它们放在一个临时的工作表中。
只要工作表不为空,重复下列步骤:
计算递归项,用当前工作表的内容替换递归自引用。对UNION(不是UNION ALL),抛弃重复行以及那些与之前结果行重复的行。将剩下的所有行包括在递归查询的结果中,并且也把它们放在一个临时的中间表中。
用中间表的内容替换工作表的内容,然后清空中间表。
严格地说,这个过程是迭代而非递归,但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,通过丢弃与先前输出行重复的行来做到这一点。不过,循环往往并不涉及完全重复的输出行:可能需要只检查一个或几个字段,以判断是否曾到达同一点。处理这种情况的标准方法是计算一个包含已访问值的数组。例如,考虑以下查询,它搜索表graph,使用其中的link字段:
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;
除了防止循环,数组值本身也常常很有用,因为它表示到达某一行所经过的“路径”。
在需要检查多个字段以识别循环的一般情况下,使用行数组。例如,如果需要比较字段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,
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;
在通常只有一个字段需要检查即可识别环的情况下,可以省略ROW()语法。这样就能使用简单数组而不是复合类型数组,从而提高效率。
递归查询求值算法按广度优先搜索顺序产生输出。可以让外层查询按这样构造的“路径”列进行ORDER BY排序,以深度优先搜索顺序显示结果。
当不确定查询是否会循环时,一个有用的测试技巧是在父查询中加入LIMIT。例如,下面的查询将会无限循环,除非加上LIMIT:
WITH RECURSIVE t(n) AS (
SELECT 1
UNION ALL
SELECT n+1 FROM t
)
SELECT n FROM t LIMIT 100;
这一技巧之所以有效,是因为PostgreSQL的实现只会对WITH查询中父查询实际取出的那些行求值。不推荐在生产中使用这一技巧,因为其他系统可能有不同的行为。另外,如果让外层查询对递归查询结果排序或将其与其他表连接,这一技巧通常就不起作用。
WITH查询的一个有用特性是,在父查询每次执行时,它只求值一次,即使父查询或同级WITH查询对它有多次引用也是如此。因此,在多处需要的昂贵计算可以放在一个WITH查询中,以避免重复工作。另一个用途是防止带副作用的函数被意外求值多次。不过,另一方面,与普通子查询相比,优化器更难把父查询中的限制下推到WITH查询中。WITH查询通常会按书写的样子求值,不会抑制父查询稍后可能丢弃的行。(不过,如上所述,如果对该查询的引用只需要有限数量的行,求值可能会提前停止。)
译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。