WITH 提供了一种为更复杂的查询编写辅助语句的方法。这些语句通常被称为公用表表达式或CTE,可以看作是定义仅用于单个查询的临时表。WITH 子句中的每个辅助语句都可以是 SELECT、INSERT、UPDATE、DELETE 或 MERGE;而 WITH 子句本身则附属于一个主要语句,该主要语句也可以是 SELECT、INSERT、UPDATE、DELETE 或 MERGE。
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),然后是递归项,其中只有递归项可以包含对查询自身输出的引用。此类查询的执行方式如下:
递归查询的求值
求值非递归项。对于 UNION(但不适用于 UNION ALL),丢弃重复的行。将所有剩余的行包含在递归查询的结果中,并将它们放入一个临时的工作表。
只要工作表不为空,就重复这些步骤:
求值递归项,将工作表的当前内容替换为递归自引用的内容。对于 UNION(但不适用于 UNION ALL),丢弃重复的行以及与任何先前结果行重复的行。将所有剩余的行包含在递归查询的结果中,并将它们放入一个临时的中间表。
用中间表的内容替换工作表的内容,然后清空中间表。
虽然 RECURSIVE 允许以递归方式指定查询,但内部以迭代方式求值这些查询。
在上面的例子中,工作表在每一步只有一个行,并且在连续的步骤中取值从 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 * pr.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
在计算树遍历时使用递归查询,您可能希望按深度优先或广度优先的顺序对结果进行排序。这可以通过与数据的其他列一起计算一个排序列,并在最后使用该列对结果进行排序来完成。请注意,这实际上并不控制查询求值访问行的顺序;这始终取决于 SQL 实现。这种方法只是提供了一种方便的方式来事后对结果进行排序。
为了创建深度优先顺序,我们为每行结果计算一个迄今为止已访问过的行数组。例如,考虑以下使用 link
字段搜索 tree
表的查询:
WITH RECURSIVE search_tree(id, link, data) AS ( SELECT t.id, t.link, t.data FROM tree t UNION ALL SELECT t.id, t.link, t.data FROM tree t, search_tree st WHERE t.id = st.link ) SELECT * FROM search_tree;
要添加深度优先排序信息,您可以这样写:
WITH RECURSIVE search_tree(id, link, data, path) AS ( SELECT t.id, t.link, t.data, ARRAY[t.id] FROM tree t UNION ALL SELECT t.id, t.link, t.data, path || t.id FROM tree t, search_tree st WHERE t.id = st.link ) SELECT * FROM search_tree ORDER BY path;
在需要使用多个字段来标识行的通用情况下,请使用行数组。例如,如果我们需要跟踪字段 f1
和 f2
:
WITH RECURSIVE search_tree(id, link, data, path) AS ( SELECT t.id, t.link, t.data, ARRAY[ROW(t.f1, t.f2)] FROM tree t UNION ALL SELECT t.id, t.link, t.data, path || ROW(t.f1, t.f2) FROM tree t, search_tree st WHERE t.id = st.link ) SELECT * FROM search_tree ORDER BY path;
在只需要跟踪一个字段的常见情况下,请省略 ROW() 语法。这允许使用简单的数组而不是复合类型数组,从而提高效率。
为了创建广度优先顺序,您可以添加一个跟踪搜索深度的列,例如:
WITH RECURSIVE search_tree(id, link, data, depth) AS ( SELECT t.id, t.link, t.data, 0 FROM tree t UNION ALL SELECT t.id, t.link, t.data, depth + 1 FROM tree t, search_tree st WHERE t.id = st.link ) SELECT * FROM search_tree ORDER BY depth;
为了获得稳定的排序,请将数据列添加为次要排序列。
递归查询求值算法以广度优先搜索顺序产生其输出。然而,这是一个实现细节,依赖它可能是不稳妥的。每层内的行顺序肯定也是未定义的,所以在任何情况下都可能需要显式排序。
内置语法可用于计算深度或广度优先排序列。例如:
WITH RECURSIVE search_tree(id, link, data) AS ( SELECT t.id, t.link, t.data FROM tree t UNION ALL SELECT t.id, t.link, t.data FROM tree t, search_tree st WHERE t.id = st.link ) SEARCH DEPTH FIRST BY id SET ordercol SELECT * FROM search_tree ORDER BY ordercol; WITH RECURSIVE search_tree(id, link, data) AS ( SELECT t.id, t.link, t.data FROM tree t UNION ALL SELECT t.id, t.link, t.data FROM tree t, search_tree st WHERE t.id = st.link ) SEARCH BREADTH FIRST BY id SET ordercol SELECT * FROM search_tree ORDER BY ordercol;
此语法在内部展开为类似于上述手动编写的形式。SEARCH 子句指定了想要的深度优先或广度优先搜索,用于排序的列列表,以及一个将包含可用于排序的结果数据列的名称。该列将隐式添加到 CTE 的输出行中。
在使用递归查询时,必须确保查询的递归部分最终不会返回任何元组,否则查询将无限循环。有时,使用 UNION 而不是 UNION ALL 可以通过丢弃重复先前输出行的行来完成此操作。然而,通常循环不涉及完全重复的输出行:可能需要仅检查一个或几个字段以查看是否已再次到达同一点。处理这种情况的标准方法是计算已访问值的数组。例如,再次考虑使用 link
字段搜索 graph
表的以下查询:
WITH RECURSIVE search_graph(id, link, data, depth) AS ( SELECT g.id, g.link, g.data, 0 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 关系包含循环,此查询将循环。由于我们需要一个“深度”输出,仅将 UNION ALL 更改为 UNION 就无法消除循环。相反,我们需要识别在跟踪特定链接路径时是否再次到达同一行。我们在易循环的查询中添加了 is_cycle
和 path
两列:
WITH RECURSIVE search_graph(id, link, data, depth, is_cycle, path) AS ( SELECT g.id, g.link, g.data, 0, false, ARRAY[g.id] FROM graph g UNION ALL SELECT g.id, g.link, g.data, sg.depth + 1, g.id = ANY(path), path || g.id FROM graph g, search_graph sg WHERE g.id = sg.link AND NOT is_cycle ) SELECT * FROM search_graph;
除了防止循环之外,数组值本身通常也很有用,因为它代表了到达任何特定行的“路径”。
在需要检查多个字段以识别循环的通用情况下,请使用行数组。例如,如果我们只需要比较字段 f1
和 f2
:
WITH RECURSIVE search_graph(id, link, data, depth, is_cycle, path) AS ( SELECT g.id, g.link, g.data, 0, false, ARRAY[ROW(g.f1, g.f2)] FROM graph g UNION ALL SELECT g.id, g.link, g.data, sg.depth + 1, ROW(g.f1, g.f2) = ANY(path), path || ROW(g.f1, g.f2) FROM graph g, search_graph sg WHERE g.id = sg.link AND NOT is_cycle ) SELECT * FROM search_graph;
在只需要检查一个字段以识别循环的常见情况下,请省略 ROW() 语法。这允许使用简单的数组而不是复合类型数组,从而提高效率。
内置语法可以简化循环检测。上述查询也可以这样写:
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
) CYCLE id SET is_cycle USING path
SELECT * FROM search_graph;
它将在内部重写为上述形式。CYCLE 子句首先指定用于循环检测的列列表,然后指定一个将显示是否检测到循环的列名称,最后指定一个将跟踪路径的另一个列名称。循环和路径列将隐式添加到 CTE 的输出行中。
循环路径列的计算方式与上一节中显示的深度优先排序列相同。查询可以同时具有 SEARCH 和 CYCLE 子句,但是深度优先搜索规范和循环检测规范会产生冗余计算,因此仅使用 CYCLE 子句并按路径列排序更为高效。如果需要广度优先排序,则同时指定 SEARCH 和 CYCLE 可能很有用。
测试查询时一个有用的技巧是,如果您不确定它们是否可能循环,请在父查询中放置一个 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 查询中,因为这可能会影响 WITH 查询输出的所有用途,而只影响其中一个。被多次引用的 WITH 查询将按原样求值,不会抑制父查询之后可能丢弃的行。(但是,如上所述,如果对查询的引用只要求有限数量的行,求值可能会提前停止。)
但是,如果 WITH 查询是非递归且无副作用的(也就是说,它是包含无易变函数的 SELECT),那么它可以被折叠到父查询中,从而允许两个查询级别联合优化。默认情况下,如果父查询仅引用 WITH 查询一次,则会发生这种情况,但如果它引用 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
对每个表行仅求值一次,而不是两次。
上面的例子只显示了 WITH 与 SELECT 一起使用,但它也可以以同样的方式附加到 INSERT、UPDATE、DELETE 或 MERGE。在每种情况下,它实际上提供了可以在主命令中引用的临时表。
您可以在 WITH 中使用数据修改语句(INSERT、UPDATE、DELETE 或 MERGE)。这允许您在同一个查询中执行几个不同的操作。例如:
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 语句通过其 RETURNING 子句返回指定行,从而从 products 中删除它们;然后主查询读取该输出并将其插入到 products_log 中。
上述示例的一个细微之处在于,WITH 子句附加到 INSERT,而不是 INSERT 中的子 SELECT。这是必需的,因为数据修改语句只允许在附加到顶层语句的 WITH 子句中使用。然而,正常的 WITH 可见性规则适用,因此可以从子 SELECT 中引用 WITH 语句的输出。
WITH 中的数据修改语句通常具有 RETURNING 子句(参见 第 6.4 节),如上面的示例所示。形成可供查询其余部分引用的临时表的,是 RETURNING 子句的输出,而不是数据修改语句的目标表。如果 WITH 中的数据修改语句缺少 RETURNING 子句,那么它不会形成临时表,也无法在查询的其余部分中引用。这样的语句仍然会被执行。一个不特别有用的例子是:
WITH t AS ( DELETE FROM foo ) DELETE FROM bar;
此示例将从表 foo 和 bar 中删除所有行。报告给客户端的受影响行数将仅包括从 bar 中删除的行。
不允许在数据修改语句的 WITH 中进行递归自引用。在某些情况下,可以通过引用递归 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 的求值仅进行到主查询请求其输出的程度。
WITH 中的子语句与彼此以及与主查询并发执行。因此,在使用 WITH 中的数据修改语句时,指定的更新实际发生的顺序是不可预测的。所有语句都使用相同的快照(参见 第 13 章)执行,因此它们不能“看到”彼此对目标表的影响。这减轻了实际行更新顺序不可预测性的影响,并意味着 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 子语句。此语句的效果将是不可预测的。
目前,作为 WITH 中数据修改语句目标的任何表都不能有条件规则、ALSO 规则或扩展为多个语句的 INSTEAD 规则。
如果您在文档中看到任何不正确的内容、与您对特定功能的实际使用经验不符或需要进一步澄清的内容,请使用 此表单 报告文档问题。