WITH
查询(公用表表达式) #WITH
提供了一种方法来编写辅助语句,以便在更大的查询中使用。这些语句通常被称为公用表表达式或CTE,可以被认为是定义只为一个查询存在的临时表。WITH
子句中的每个辅助语句可以是 SELECT
、INSERT
、UPDATE
、DELETE
或 MERGE
;而 WITH
子句本身附加到一个主语句,该主语句也可以是 SELECT
、INSERT
、UPDATE
、DELETE
或 MERGE
。
SELECT
在 WITH
中 #SELECT
在 WITH
中的基本价值在于将复杂的查询分解成更简单的部分。一个例子是
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
中的数据修改语句 #您可以在 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
从 products
中删除指定行,通过其 RETURNING
子句返回其内容;然后,主查询读取该输出并将它插入到 products_log
中。
上面的例子一个细微之处是,WITH
子句附加到 INSERT
,而不是附加到 INSERT
中的子 SELECT
。这是必要的,因为数据修改语句只能在附加到顶层语句的 WITH
子句中使用。但是,正常的 WITH
可见性规则适用,因此可以从子 SELECT
中引用 WITH
语句的输出。
WITH
中的数据修改语句通常具有 RETURNING
子句(参见 Section 6.4),如上面的示例所示。它是 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
的执行仅进行到主查询需要其输出为止。
WITH
中的子语句是与彼此以及与主查询并发执行的。因此,在 WITH
中使用数据修改语句时,指定更新实际发生的顺序是不可预测的。所有语句都使用相同的 快照(参见 Chapter 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
规则。
如果您在文档中看到任何不正确的内容、与您对特定功能的体验不符或需要进一步说明,请使用 此表格 报告文档问题。