PostgreSQL: PostgreSQL子查询与CTE

最后更新:2026-08-26

1. 你将学到


2. 故事

Alice 是一家 SaaS 公司的 HR 系统开发者。公司有 500 多人,组织架构是树形的——CEO 在顶层,下面是 VP,VP 下面是 Director,Director 下面是 Manager,Manager 下面是员工。

产品经理要求:从任意一个节点出发,查出该节点及其所有下级(含多级),并用缩进显示层级。

普通查询只能查一层。Alice 学会了 WITH RECURSIVE 递归 CTE,一条 SQL 就搞定了整棵子树。


3. Concept

(1) 子查询分类

类型 返回 可出现位置 示例
标量子查询 单行单值 SELECT, WHERE, HAVING (SELECT MAX(salary) ...)
列子查询 单列多行 WHERE + IN/ANY/ALL WHERE id IN (SELECT ...)
行子查询 单行多列 WHERE WHERE (a,b) = (SELECT x,y ...)
表子查询 多行多列 FROM FROM (SELECT ...) AS t

▶ 示例:标量子查询在 SELECT

SQL
SELECT
  name,
  salary,
  (SELECT AVG(salary) FROM employees) AS company_avg,
  salary - (SELECT AVG(salary) FROM employees) AS diff
FROM employees
WHERE department_id = 5;
TEXT 📖 仅展示
 name    | salary | company_avg | diff
---------+--------+-------------+-------
 Alice   |  95000 |    72000.00 | 23000
 Bob     |  88000 |    72000.00 | 16000

▶ 示例:标量子查询在 WHERE

SQL
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);

输出:

TEXT 📖 仅展示
  result  
----------
   42.50
(1 row)

▶ 示例:列子查询 + IN

SQL
SELECT order_id, amount
FROM orders
WHERE customer_id IN (
  SELECT customer_id
  FROM customers
  WHERE region = 'NA'
);

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ 示例:行子查询

SQL
SELECT name, department_id, salary
FROM employees
WHERE (department_id, salary) = (
  SELECT department_id, MAX(salary)
  FROM employees
  GROUP BY department_id
  HAVING department_id = employees.department_id
);

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

(2) EXISTS / NOT EXISTS

EXISTS 检查子查询是否有返回行,不关心具体值,只关心"存在与否"。

▶ 示例:EXISTS 找有订单的用户

SQL
SELECT u.user_id, u.name
FROM users u
WHERE EXISTS (
  SELECT 1 FROM orders o
  WHERE o.user_id = u.user_id
);

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)
方式 语法 停止时机 NULL 友好
IN WHERE id IN (SELECT ...) 全部扫描 有 NULL 时需注意
EXISTS WHERE EXISTS (SELECT 1 ...) 找到一行即停 NULL 无影响
JOIN JOIN ... 全部匹配 取决于 JOIN 类型

▶ 示例:NOT EXISTS 找无订单用户

SQL
SELECT u.user_id, u.name
FROM users u
WHERE NOT EXISTS (
  SELECT 1 FROM orders o
  WHERE o.user_id = u.user_id
);

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

NOT EXISTS 比 NOT IN 更安全:NOT IN 遇到子查询结果含 NULL 时,整个结果为空。

▶ 示例:NOT IN 的 NULL 陷阱

SQL
SELECT name FROM customers
WHERE region NOT IN ('NA', 'EU', NULL);
TEXT 📖 仅展示
(0 rows)

因为 x NOT IN (a, b, NULL) 等价于 x <> a AND x <> b AND x <> NULL,而 x <> NULL 为 UNKNOWN,整体为 FALSE。

(3) ANY / ALL

▶ 示例:ANY 找薪资高于某部门任一员工的人

SQL
SELECT name, salary
FROM employees
WHERE salary > ANY (
  SELECT salary FROM employees WHERE department_id = 3
);

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

等价于 > MIN(子查询结果)

▶ 示例:ALL 找薪资高于某部门所有员工的人

SQL
SELECT name, salary
FROM employees
WHERE salary > ALL (
  SELECT salary FROM employees WHERE department_id = 3
);

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

等价于 > MAX(子查询结果)

操作符 含义 等价
> ANY (...) 大于任一个 > MIN(...)
> ALL (...) 大于所有 > MAX(...)
= ANY (...) 等于任一个 IN (...)

▶ 示例:表子查询在 FROM

SQL
SELECT
  department_id,
  avg_salary,
  count
FROM (
  SELECT
    department_id,
    AVG(salary) AS avg_salary,
    COUNT(*)    AS count
  FROM employees
  GROUP BY department_id
) AS dept_stats
WHERE avg_salary > 70000;

输出:

TEXT 📖 仅展示
 count 
-------
     5
(1 row)

4. Key Points

(1) CTE(WITH 子句)

CTE(Common Table Expression)用 WITH 定义命名临时结果集,可多次引用。

▶ 示例:CTE 简化多层嵌套

SQL
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 AVG(total_sales) FROM regional_sales)
)
SELECT
  o.order_id,
  o.amount,
  o.region
FROM orders o
WHERE o.region IN (SELECT region FROM top_regions)
ORDER BY o.amount DESC;

输出:

TEXT 📖 仅展示
  result  
----------
   42.50
(1 row)
方式 可读性 可复用 优化器内联 物化
嵌套子查询
CTE PG 12+ 自动决定 可手动 MATERIALIZED
临时表 写磁盘

▶ 示例:CTE MATERIALIZED 强制物化

SQL
WITH expensive_calc AS MATERIALIZED (
  SELECT customer_id, COUNT(*) AS order_count
  FROM orders
  GROUP BY customer_id
)
SELECT * FROM expensive_calc
UNION ALL
SELECT * FROM expensive_calc;

输出:

TEXT 📖 仅展示
 count 
-------
     5
(1 row)

MATERIALIZED 强制计算一次并缓存,适合多次引用且计算量大的 CTE。

▶ 示例:CTE NOT MATERIALIZED 强制内联

SQL
WITH simple_filter AS NOT MATERIALIZED (
  SELECT * FROM orders WHERE region = 'NA'
)
SELECT * FROM simple_filter WHERE amount > 50000;

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

NOT MATERIALIZED 让优化器内联展开,适合简单谓词下推场景。

(2) 递归 CTE(WITH RECURSIVE)

递归 CTE 是处理树形和图形数据的标准 SQL 方案。

语法结构:

SQL
WITH RECURSIVE cte_name AS (
  base_query        -- anchor: non-recursive seed
  UNION ALL
  recursive_query   -- references cte_name itself
)
SELECT * FROM cte_name;

▶ 示例:从 CEO 查所有下属

SQL
WITH RECURSIVE subordinates AS (
  SELECT
    employee_id,
    name,
    manager_id,
    1 AS level,
    name::text AS path
  FROM employees
  WHERE manager_id IS NULL
    AND name = 'David'
  UNION ALL
  SELECT
    e.employee_id,
    e.name,
    e.manager_id,
    s.level + 1,
    s.path || ' > ' || e.name
  FROM employees e
  INNER JOIN subordinates s ON e.manager_id = s.employee_id
)
SELECT
  level,
  REPEAT('  ', level - 1) || name AS org_chart,
  path
FROM subordinates
ORDER BY path;
TEXT 📖 仅展示
 level |       org_chart        |              path
-------+------------------------+--------------------------------
     1 | David                  | David
     2 |   Alice                | David > Alice
     3 |     Bob                | David > Alice > Bob
     3 |     Charlie            | David > Alice > Charlie
     2 |   Eve                  | David > Eve
     3 |     Frank              | David > Eve > Frank

▶ 示例:限定递归深度(防无限循环)

SQL
WITH RECURSIVE subordinates AS (
  SELECT
    employee_id, name, manager_id, 1 AS level
  FROM employees
  WHERE manager_id IS NULL
  UNION ALL
  SELECT
    e.employee_id, e.name, e.manager_id, s.level + 1
  FROM employees e
  JOIN subordinates s ON e.manager_id = s.employee_id
  WHERE s.level < 5
)
SELECT * FROM subordinates;

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

WHERE s.level < 5 限制递归最多 5 层。

▶ 示例:生成日期序列

SQL
WITH RECURSIVE date_series AS (
  SELECT '2025-01-01'::date AS dt
  UNION ALL
  SELECT dt + INTERVAL '1 day'
  FROM date_series
  WHERE dt < '2025-12-31'
)
SELECT dt FROM date_series;

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

5. Practice

▶ 示例:找出每个部门薪资最高的员工

SQL
SELECT e.name, e.department_id, e.salary
FROM employees e
WHERE e.salary = (
  SELECT MAX(salary)
  FROM employees e2
  WHERE e2.department_id = e.department_id
)
ORDER BY e.department_id;

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ 示例:CTE 计算客户 RFM 分值

SQL
WITH customer_orders AS (
  SELECT
    customer_id,
    MAX(created_at)                           AS last_order_date,
    COUNT(*)                                  AS frequency,
    SUM(amount)                               AS monetary
  FROM orders
  GROUP BY customer_id
)
SELECT
  customer_id,
  frequency,
  monetary,
  NTILE(4) OVER (ORDER BY monetary DESC) AS m_quartile
FROM customer_orders;

输出:

TEXT 📖 仅展示
 count 
-------
     5
(1 row)

▶ 示例:递归 CTE 查产品类别树

SQL
WITH RECURSIVE category_tree AS (
  SELECT
    category_id, parent_id, name, 0 AS depth
  FROM categories
  WHERE parent_id IS NULL
  UNION ALL
  SELECT
    c.category_id, c.parent_id, c.name, ct.depth + 1
  FROM categories c
  JOIN category_tree ct ON c.parent_id = ct.category_id
)
SELECT
  depth,
  REPEAT('──', depth) || name AS tree_view
FROM category_tree
ORDER BY depth, name;

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ 示例:EXISTS 找被所有区域都订购的产品

SQL
SELECT p.product_name
FROM products p
WHERE NOT EXISTS (
  SELECT 1 FROM regions r
  WHERE NOT EXISTS (
    SELECT 1 FROM order_items oi
    JOIN orders o ON oi.order_id = o.order_id
    WHERE oi.product_id = p.product_id
      AND o.region = r.region_code
  )
);

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ 示例:CTE + 窗口函数求 Top-N

SQL
WITH ranked_orders AS (
  SELECT
    customer_id,
    order_id,
    amount,
    ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY amount DESC) AS rn
  FROM orders
)
SELECT customer_id, order_id, amount
FROM ranked_orders
WHERE rn <= 3;

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

6. Comprehensive Example

Alice 的组织架构查询——从任意经理出发查全部下属,带层级缩进、路径、人数统计:

SQL
WITH RECURSIVE org_tree AS (
  SELECT
    employee_id,
    name,
    manager_id,
    1          AS level,
    name::text AS path,
    ARRAY[employee_id] AS subtree_ids
  FROM employees
  WHERE name = 'Alice'
  UNION ALL
  SELECT
    e.employee_id,
    e.name,
    e.manager_id,
    o.level + 1,
    o.path || ' > ' || e.name,
    o.subtree_ids || e.employee_id
  FROM employees e
  JOIN org_tree o ON e.manager_id = o.employee_id
)
SELECT
  o.level,
  REPEAT('    ', o.level - 1) || o.name AS org_chart,
  o.path,
  ARRAY_LENGTH(o.subtree_ids, 1)       AS team_size
FROM org_tree o
ORDER BY o.path;
TEXT 📖 仅展示
 level |         org_chart          |              path               | team_size
-------+----------------------------+---------------------------------+-----------
     1 | Alice                      | Alice                           |         1
     2 |     Bob                    | Alice > Bob                     |         2
     3 |         Diana              | Alice > Bob > Diana             |         3
     3 |         Eve                | Alice > Bob > Eve               |         4
     2 |     Charlie                | Alice > Charlie                 |         5
     3 |         Frank              | Alice > Charlie > Frank         |         6

7. 递归 CTE 执行流程

100%
flowchart TD
    A["Anchor Query<br/>(non-recursive seed)"] --> B["Working Table T₀"]
    B --> C["Recursive Query<br/>(JOIN with T₀)"]
    C --> D{"New rows<br/>produced?"}
    D -->|Yes| E["Working Table T₁"]
    E --> F["Append to result"]
    F --> C
    D -->|No| G["Final Result<br/>(all iterations UNION ALL)"]

    style A fill:#e1f5fe
    style C fill:#fff9c4
    style G fill:#c8e6c9
步骤 操作 说明
1 执行锚点查询 非递归种子,生成初始行
2 放入工作表 T₀ = 锚点结果
3 递归查询 将工作表与原表 JOIN
4 检查新行 有新行则继续,无则终止
5 合并结果 UNION ALL 所有迭代结果

❓ 常见问题

Q CTE 和子查询性能有区别吗?
A PostgreSQL 12+ 会自动决定 CTE 是否内联。简单 CTE 通常被内联,复杂 CTE 可能物化。可用 MATERIALIZED / NOT MATERIALIZED 手动控制。
Q 递归 CTE 会不会无限循环?
A 可能。如果数据有环(如 A→B→A),递归不会停止。防范方法:加 level 限制,或追踪 path 避免重复访问。
Q NOT IN 遇到 NULL 为什么返回空?
A x NOT IN (a, NULL) 等价于 x<>a AND x<>NULLx<>NULL 为 UNKNOWN,AND 链中 UNKNOWN 使整行为 FALSE。用 NOT EXISTS 替代更安全。
Q EXISTS 和 IN 哪个快?
A 取决于数据和索引。通常子查询结果集小时 IN 更快,外层表小时 EXISTS 更快。PostgreSQL 优化器会自动改写,多数场景性能接近。
Q 递归 CTE 能处理图结构吗?
A 可以,但需额外防环逻辑。在递归部分追踪已访问节点(用 ARRAY 或 path 字符串),避免重复访问。
Q CTE 中能否引用前面定义的 CTE?
A 可以。WITH a AS (...), b AS (SELECT ... FROM a) 中,b 可以引用 a,按定义顺序向下引用。

📖 小节


📝 作业

  1. ⭐ 用标量子查询找出薪资高于公司平均值的员工
  2. ⭐ 用 NOT EXISTS 找出从未下过单的客户
  3. ⭐⭐ 用 CTE 重写以下嵌套子查询:找出订单金额高于其客户平均订单金额的订单
  4. ⭐⭐ 用 WITH RECURSIVE 从 employees 表中查询指定经理的 3 层下属,输出层级缩进
  5. ⭐⭐⭐ 用递归 CTE + 聚合实现:从 CEO 出发,统计每个经理的直接下属人数和整个子树的人数,一条 SQL 完成
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏