选择 打开 改范围 完整检索页

pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。

受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
测试与开发版本: 19 / devel
不受支持的版本: 13 / 12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0
历史版本PostgreSQL 9.0 已于 2015 年 10 月结束社区维护,本页译文保留供仍在使用旧版本的读者参考。新系统请看当前版本

7.8. 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),然后是一个递归项,其中只有递归项才能包含对查询自身输出的引用。这样的查询按以下方式执行:

递归查询求值

  1. 计算非递归项。对UNION(但不对UNION ALL),抛弃重复行。把所有剩余的行包括在递归查询的结果中,并且也把它们放在一个临时的工作表中。

  2. 只要工作表不为空,重复下列步骤:

    1. 计算递归项,用当前工作表的内容替换递归自引用。对UNION(不是UNION ALL),抛弃重复行以及那些与之前结果行重复的行。将剩下的所有行包括在递归查询的结果中,并且也把它们放在一个临时的中间表中。

    2. 用中间表的内容替换工作表的内容,然后清空中间表。

注意

严格地说,这个过程是迭代而非递归,但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并不能消除循环。我们需要判断在沿特定链接路径前进时,是否再次到达了同一行。为这个容易循环的查询添加pathcycle两列:

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;

除了防止循环,数组值本身也常常很有用,因为它表示到达某一行所经过的路径

在需要检查多个字段以识别循环的一般情况下,使用行数组。例如,如果需要比较字段f1f2

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 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。