PostgreSQL 中 CTE 的使用
PostgreSQL 中 CTE 的语法。CTE(Common Table Expression,通用表表达式)是一个非常强大且常用的功能,它极大地提高了复杂查询的可读性和可维护性。
核心概念
CTE 可以理解为一个临时的、命名的结果集,它只在一条 SQL 语句的执行范围内存在。你可以把它看作是在查询中定义的临时视图。
基本语法结构
WITH [RECURSIVE] cte_name [ (column_name [, ...]) ] AS (-- CTE 的定义(一个 SELECT 语句)SELECT ...
)
-- 主查询,使用上面定义的 CTE
SELECT *
FROM cte_name;
关键组成部分:
WITH
关键字: 标志着 CTE 的开始。RECURSIVE
关键字(可选): 如果 CTE 是递归的,则必须加上此关键字。cte_name
: 你为这个临时结果集起的名字。(column_name [, ...])
(可选): 显式指定 CTE 结果集的列名。如果省略,则列名来自SELECT
语句中的列。AS (
...)
: 括号内是定义 CTE 的SELECT
语句。主查询: 在定义完所有 CTE 后,编写主
SELECT
(或INSERT
,UPDATE
,DELETE
)语句来使用这些 CTE。
示例 1:简单的非递归 CTE(提高可读性)
场景: 我们想找出销售额高于平均销售额的产品。
不使用 CTE 的写法(较难读):
SELECT product_id, SUM(amount) as total_sales
FROM sales
GROUP BY product_id
HAVING SUM(amount) > (SELECT AVG(sales_sum) FROM (SELECT SUM(amount) as sales_sum FROM sales GROUP BY product_id) AS avg_sales);
使用 CTE 的写法(清晰易懂):
WITH product_sales AS (-- CTE 1: 计算每个产品的总销售额SELECT product_id, SUM(amount) AS total_salesFROM salesGROUP BY product_id
),
average_sales AS (-- CTE 2: 计算所有产品的平均销售额SELECT AVG(total_sales) AS avg_salesFROM product_sales -- 可以引用之前定义的 CTE!
)
-- 主查询
SELECT ps.product_id, ps.total_sales
FROM product_sales ps, average_sales av
WHERE ps.total_sales > av.avg_sales;
优势: 将复杂的子查询拆解成有意义的命名模块(product_sales
, average_sales
),逻辑一目了然。
示例 2:在 DML 语句中使用 CTE
CTE 不仅可用于 SELECT
,还可用于 INSERT
, UPDATE
, DELETE
。
INSERT
with CTE:
WITH top_products AS (SELECT product_idFROM salesGROUP BY product_idORDER BY SUM(amount) DESCLIMIT 10
)
INSERT INTO popular_products (product_id)
SELECT product_id
FROM top_products;
UPDATE
with CTE:
WITH old_records AS (SELECT idFROM eventsWHERE created_at < NOW() - INTERVAL '1 year'
)
UPDATE events
SET archived = true
WHERE id IN (SELECT id FROM old_records);
DELETE
with CTE:
WITH duplicates AS (SELECT MIN(ctid) as min_ctid, -- ctid 是 PostgreSQL 的系统列,代表行的物理位置user_id, emailFROM usersGROUP BY user_id, emailHAVING COUNT(*) > 1
)
DELETE FROM users
WHERE (user_id, email) IN (SELECT user_id, email FROM duplicates)AND ctid NOT IN (SELECT min_ctid FROM duplicates);
示例 3:递归 CTE(处理层次结构或序列)
递归 CTE 是 CTE 最强大的功能之一,用于处理树形结构、图数据或生成序列。其语法有固定结构。
基本递归结构:
WITH RECURSIVE cte_name AS (-- 1. 非递归项(锚点成员)SELECT ... FROM ... WHERE ...UNION [ALL]-- 2. 递归项(递归成员)SELECT ... FROM cte_name, ... WHERE ...
)
SELECT * FROM cte_name;
经典示例:生成 1 到 10 的数字序列
WITH RECURSIVE numbers AS (-- 锚点成员:起始值SELECT 1 AS nUNION ALL-- 递归成员:基于前一个值 +1,直到条件不满足SELECT n + 1FROM numbersWHERE n < 10 -- 终止条件
)
SELECT n FROM numbers;
结果: 1, 2, 3, ..., 10
经典示例:遍历员工-经理层级关系
假设表 employees(emp_id, emp_name, manager_id)
WITH RECURSIVE employee_hierarchy AS (-- 锚点成员:找出所有最高层级的员工(没有经理)SELECT emp_id, emp_name, manager_id,1 AS level,ARRAY[emp_id] AS path -- 用于记录从根到当前节点的路径FROM employeesWHERE manager_id IS NULLUNION ALL-- 递归成员:连接下属员工SELECT e.emp_id, e.emp_name, e.manager_id,eh.level + 1,eh.path || e.emp_id -- 将当前员工ID追加到路径中FROM employees eINNER JOIN employee_hierarchy eh ON e.manager_id = eh.emp_id
)
SELECT emp_id, emp_name, level,path
FROM employee_hierarchy
ORDER BY path;
这个查询会输出整个公司的汇报层级结构。
重要特性和注意事项
多个 CTE: 一个
WITH
子句可以定义多个 CTE,用逗号分隔。WITH cte1 AS (...), cte2 AS (...), cte3 AS (...) SELECT ... FROM cte1, cte2, cte3 ...
作用域: CTE 只在紧随其后的主查询中有效。你不能在同一个
WITH
子句中先定义的 CTE 中引用后定义的 CTE。性能: CTE 在 PostgreSQL 中通常被优化为代码内联。但对于递归 CTE 或与外部查询有复杂交互的情况,它可能会产生自己的执行计划。有时为了强制实现物化(将结果临时存储),可以使用
MATERIALIZED
关键字(PostgreSQL 12+):WITH cte_name AS MATERIALIZED ( ... )
递归 CTE 的终止: 必须确保递归部分最终能返回空结果,否则查询将进入无限循环。
总结
特性 | 描述 |
---|---|
目的 | 创建临时命名结果集,简化复杂查询,实现递归查询。 |
基本语法 | WITH cte_name AS (SELECT ...) SELECT ... FROM cte_name; |
递归语法 | WITH RECURSIVE cte_name AS (锚点 UNION ALL 递归) SELECT ... |
适用语句 | SELECT , INSERT , UPDATE , DELETE 。 |
主要优势 | 提高可读性、模块化复杂逻辑、实现递归查询。 |
CTE 是每个 PostgreSQL 使用者都必须掌握的核心特性,它能让你写出更清晰、更强大、更易于维护的 SQL 代码。