公用表表达式(CTE)是一个由 WITH name AS (SELECT ...) 引入的命名子查询。你可以从主查询(以及后续的 CTE)中引用它,就像引用一张表一样。CTE 让复杂查询更易读:每个 CTE 都有自己的名称和任务,主查询从上到下阅读起来像一个故事。PostgreSQL 还支持递归 CTE(WITH RECURSIVE),这使你可以只用 SQL 遍历树、图和链。
本章要点
使用 WITH name AS (...) 定义 CTE
在一个查询中串联多个 CTE
数据修改 CTE:在 WITH 中使用 INSERT、UPDATE、DELETE
用递归 CTE 处理层级结构和图
性能方面的权衡(MATERIALIZED / NOT MATERIALIZED)
示例
示例 38.1公用表表达式(WITH)
-- A single CTE
WITHspendAS(SELECTcustomer_id,sum(total)AStotal_spentFROMorders_exampleGROUPBYcustomer_id)SELECTc.name,COALESCE(s.total_spent,0)ASspentFROMcustomers_examplecLEFTJOINspendsONs.customer_id=c.idORDERBYc.id;-- Multiple CTEs, each named and easy to read
WITHper_customerAS(SELECTcustomer_id,sum(total)AStotal_spent,count(*)ASorder_countFROMorders_exampleGROUPBYcustomer_id),big_spendersAS(SELECT*FROMper_customerWHEREtotal_spent>30)SELECTc.name,b.total_spent,b.order_countFROMbig_spendersbJOINcustomers_examplecONc.id=b.customer_idORDERBYb.total_spentDESC;-- Data-modifying CTE: archive then return what was archived
DROPTABLEIFEXISTSorders_archive;CREATETABLEorders_archive(LIKEorders_exampleINCLUDINGALL);WITHarchivedAS(DELETEFROMorders_exampleWHEREtotal<20RETURNING*)INSERTINTOorders_archiveSELECT*FROMarchivedRETURNINGid;-- RECURSIVE CTE: walk an employee hierarchy
WITHRECURSIVEreportsAS(-- anchor: top-level employees (no manager)
SELECTid,name,manager_id,1ASdepthFROMemployees_exampleWHEREmanager_idISNULLUNIONALL-- recursive step: rows whose manager appears in the previous level
SELECTe.id,e.name,e.manager_id,r.depth+1FROMemployees_exampleeJOINreportsrONe.manager_id=r.id)SELECTdepth,nameFROMreportsORDERBYdepth,id;
递归 CTE 由 UNION ALL 组合成两个部分:一个生成初始行的锚点查询,以及一个引用 CTE 名称来添加更多行的递归查询。PostgreSQL 会持续迭代,直到不再产生新行。可以用它们处理层级结构(员工 → 经理)、图遍历(查找所有传递依赖)和序列生成器(数字、日期)。
运行示例
运行方式(需要前面课程中的表):
$ psql -f source/cte.sql postgres
name | spent
-------+-------
Alice | 80.00
Bob | 0.00
Carol | 0.00
(3 rows)
...
depth | name
-------+-------
1 | Alice
2 | Bob
2 | Carol
3 | Dave
3 | Eve
(5 rows)
常见陷阱
在 PostgreSQL 12 及更高版本中,一个只引用一次、无副作用的非递归 SELECT CTE 通常会折叠到其父查询中——规划器会像处理子查询一样处理它。如果你想要较早版本“始终物化”的行为,请写成 WITH x AS MATERIALIZED (...)。
WITH RECURSIVE 同时接受 UNION ALL 和 UNION。UNION 会消除完整行的重复项,包括此前生成的行;UNION ALL 则会保留它们。根据递归方式及其终止条件作出选择。
不会收敛的递归 CTE(递归步骤持续添加行)会形成无限循环。查询可能无限运行、溢写到磁盘,或失败;work_mem 不是执行限制。拿不准时,请添加终止条件(例如 WHERE depth < 100)。
WITH 中的数据修改 CTE 彼此之间的求值顺序未指定——不要依赖它们之间的副作用。主语句与它们共享同一快照,且主语句与这些 CTE 之间的修改顺序没有保证。应通过 RETURNING 传递数据,而不要依赖可见的副作用。
写一个 CTE 计算订单总额的平均值,然后在主查询中显示每个订单与该平均值的差值。然后试试用递归 CTE 生成 1 到 10 的数字(WITH RECURSIVE t(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM t WHERE n < 10) SELECT * FROM t;)。