PostgreSQL WITH 子句

PostgreSQL WITH 子句(CTE)完全指南

WITH 子句 = 公共表表达式(Common Table Expression),是 SQL 的“临时视图”,让复杂查询 可读、可维护、可递归
本文涵盖 普通 CTE、递归 CTE、物化 CTE、性能优化、递归层级控制、JSON/数组应用、替代方案 等全部内容。


一、WITH 基本语法

WITH cte_name AS (
    -- 子查询
    SELECT ...
)
SELECT ... FROM cte_name ...;
类型说明
普通 CTE临时结果集,逻辑清晰
递归 CTEWITH RECURSIVE,处理树形/图结构
物化 CTEMATERIALIZED / NOT MATERIALIZED(PG12+)

二、核心优势

优势说明
提升可读性拆解复杂逻辑
避免重复子查询一次定义,多次使用
支持递归树、图、层级数据
可引用多次比子查询更灵活
调试友好可单独测试 CTE

三、准备测试数据

-- 员工表(含上下级关系)
CREATE TABLE employees (
    id BIGSERIAL PRIMARY KEY,
    name TEXT,
    manager_id BIGINT REFERENCES employees(id),
    department TEXT,
    salary NUMERIC(10,2),
    hire_date DATE
);

-- 插入层级数据
INSERT INTO employees (name, manager_id, department, salary, hire_date) VALUES
('CEO', NULL, 'Executive', 500000, '2020-01-01'),
('CTO', 1, 'IT', 300000, '2020-02-01'),
('CFO', 1, 'Finance', 280000, '2020-02-15'),
('IT Manager', 2, 'IT', 180000, '2021-01-10'),
('Finance Manager', 3, 'Finance', 170000, '2021-03-01'),
('Dev Lead', 4, 'IT', 120000, '2022-01-01'),
('Accountant', 5, 'Finance', 90000, '2022-06-01'),
('Alice', 6, 'IT', 95000, '2023-01-15'),
('Bob', 6, 'IT', 88000, '2023-02-20'),
('Carol', 7, 'Finance', 85000, '2023-04-10');

四、普通 CTE:拆解复杂查询

示例:部门平均薪资 + 高于平均的员工

WITH dept_avg AS (
    SELECT department, AVG(salary) AS avg_salary
    FROM employees
    GROUP BY department
)
SELECT e.name, e.department, e.salary, d.avg_salary
FROM employees e
JOIN dept_avg d ON e.department = d.department
WHERE e.salary > d.avg_salary;

五、多个 CTE:链式处理

WITH 
active_emps AS (
    SELECT * FROM employees WHERE hire_date >= '2023-01-01'
),
it_emps AS (
    SELECT * FROM active_emps WHERE department = 'IT'
),
high_performers AS (
    SELECT *, salary * 1.1 AS bonus
    FROM it_emps WHERE salary > 90000
)
SELECT name, salary, bonus FROM high_performers;

六、递归 CTE:处理层级数据(杀手级!)

语法

WITH RECURSIVE cte_name AS (
    -- 锚点查询(起点)
    SELECT ... WHERE manager_id IS NULL
  UNION [ALL]
    -- 递归部分
    SELECT e.* FROM employees e
    JOIN cte_name c ON e.manager_id = c.id
)
SELECT * FROM cte_name;

示例:查询 CEO 及其所有下属

WITH RECURSIVE org_chart AS (
    -- 锚点:CEO
    SELECT id, name, manager_id, 0 AS level
    FROM employees 
    WHERE manager_id IS NULL

  UNION ALL

    -- 递归:下属
    SELECT e.id, e.name, e.manager_id, oc.level + 1
    FROM employees e
    JOIN org_chart oc ON e.manager_id = oc.id
)
SELECT 
    LPAD(' ', level * 2) || name AS hierarchy,
    level,
    department
FROM org_chart
ORDER BY level, name;

输出

       hierarchy       | level | department
------------------------+-------+------------
 CEO                    |     0 | Executive
   CTO                  |     1 | IT
   CFO                  |     1 | Finance
     IT Manager         |     2 | IT
     Finance Manager    |     2 | Finance
       Dev Lead         |     3 | IT
       Accountant       |     3 | Finance
         Alice          |     4 | IT
         Bob            |     4 | IT
         Carol          |     4 | Finance

七、递归控制:防止无限循环

-- 限制最大深度
WITH RECURSIVE org_chart AS (
    SELECT id, name, manager_id, 0 AS level
    FROM employees WHERE manager_id IS NULL
  UNION ALL
    SELECT e.id, e.name, e.manager_id, oc.level + 1
    FROM employees e
    JOIN org_chart oc ON e.manager_id = oc.id
    WHERE oc.level < 10  -- 最大 10 层
)
SELECT * FROM org_chart;

八、物化 CTE(PG12+):性能控制

选项说明
MATERIALIZED强制物化(存储临时结果)
NOT MATERIALIZED强制内联(不存储)
-- 强制物化:适合大表中间结果
WITH high_salary AS MATERIALIZED (
    SELECT * FROM employees WHERE salary > 100000
)
SELECT department, COUNT(*) FROM high_salary GROUP BY department;

-- 强制内联:适合小表
WITH temp AS NOT MATERIALIZED (
    SELECT id, name FROM employees WHERE id < 10
)
SELECT * FROM temp;

九、CTE vs 子查询 vs 临时表

方式可读性性能适用场景
CTE5 stars中等复杂逻辑、可读性优先
子查询2 stars中等简单嵌套
临时表3 stars超大中间结果、多次使用

十、性能优化

1. 索引支持

-- 为递归连接列建索引
CREATE INDEX idx_emp_manager ON employees(manager_id);

-- 为 WHERE 列建索引
CREATE INDEX idx_emp_dept ON employees(department);

2. 避免重复计算

-- 错误:重复计算
WITH cte1 AS (SELECT expensive_func() FROM t),
     cte2 AS (SELECT expensive_func() FROM t)
SELECT ...;

-- 正确:一次计算
WITH cte AS (SELECT expensive_func() AS val FROM t),
     cte1 AS (SELECT val FROM cte),
     cte2 AS (SELECT val FROM cte)
SELECT ...;

十一、JSON 与数组 CTE 应用

-- 聚合部门信息为 JSON
WITH dept_stats AS (
    SELECT 
        department,
        COUNT(*) AS emp_count,
        AVG(salary) AS avg_salary,
        JSONB_AGG(
            JSONB_BUILD_OBJECT('name', name, 'salary', salary)
        ) AS employees
    FROM employees
    GROUP BY department
)
SELECT * FROM dept_stats;

十二、常见陷阱与解决方案

陷阱说明解决方案
CTE 被多次执行每个引用都重新计算MATERIALIZED 或临时表
递归死循环环路数据level 限制
CTE 引用顺序错误后定义先用按依赖顺序写
物化导致内存爆炸中间结果太大NOT MATERIALIZED 或临时表

十三、最佳实践 Checklist

项目建议
复杂查询必用 CTE提升可读性
递归加 level 限制防止死循环
大结果用 MATERIALIZED避免重复计算
小结果用 NOT MATERIALIZED减少 I/O
建连接索引manager_id
命名清晰dept_stats, org_chart
单独测试 CTEWITH cte AS (...) SELECT * FROM cte;

十四、一键组织架构报表

WITH RECURSIVE org AS (
    SELECT id, name, manager_id, department, salary, 0 AS level
    FROM employees WHERE manager_id IS NULL
  UNION ALL
    SELECT e.id, e.name, e.manager_id, e.department, e.salary, o.level + 1
    FROM employees e
    JOIN org o ON e.manager_id = o.id
    WHERE o.level < 20
),
dept_summary AS (
    SELECT 
        department,
        COUNT(*) AS headcount,
        SUM(salary) AS payroll,
        AVG(salary) AS avg_salary
    FROM org
    GROUP BY department
)
SELECT 
    LPAD(' ', level * 3) || name AS employee,
    department,
    salary,
    level
FROM org
UNION ALL
SELECT 
    '--- ' || department || ' SUMMARY' AS employee,
    department,
    payroll,
    NULL
FROM dept_summary
ORDER BY department, level NULLS FIRST, employee;

十五、快速实战:5 分钟掌握 WITH

-- 1. 普通 CTE:IT 部门平均薪资
WITH it_avg AS (
    SELECT AVG(salary) AS avg_sal FROM employees WHERE department = 'IT'
)
SELECT name, salary FROM employees, it_avg WHERE salary > avg_sal;

-- 2. 递归 CTE:列出 Dev Lead 的所有下属
WITH RECURSIVE team AS (
    SELECT id, name, 0 AS level FROM employees WHERE name = 'Dev Lead'
  UNION ALL
    SELECT e.id, e.name, t.level + 1
    FROM employees e
    JOIN team t ON e.manager_id = t.id
)
SELECT name, level FROM team;

-- 3. 物化 CTE
WITH high_paid AS MATERIALIZED (
    SELECT * FROM employees WHERE salary > 100000
)
SELECT department, COUNT(*) FROM high_paid GROUP BY department;

现在就动手
employees 表中:

  1. 用 CTE 统计 每个部门的员工数和平均薪资
  2. 用递归 CTE 列出 CTO 的完整下属树(带层级)
  3. MATERIALIZED 优化一个大查询
  4. 建索引并用 EXPLAIN 验证递归性能

十六、性能对比(EXPLAIN)

-- 慢:无索引递归
EXPLAIN ANALYZE WITH RECURSIVE ... SELECT * FROM org_chart;

-- 快:有索引
CREATE INDEX idx_emp_manager ON employees(manager_id);
EXPLAIN ANALYZE WITH RECURSIVE ... SELECT * FROM org_chart;

需要我生成:

  • 10万员工层级数据 + 递归压力测试?回复 测试数据
  • 动态 CTE 生成(Python)?回复 Python CTE
  • 无限层级菜单系统?回复 菜单系统
  • EXPLAIN 可视化分析?回复 EXPLAIN 分析

随时告诉我!

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注