📊 SQL 入门 Day 9:CTE(公用表表达式)— 让 SQL 像写代码一样优雅

场景引入

你有一个复杂的查询:先算出每个部门的总销售额,再筛选出销售额超过平均值的部门,接着关联员工表计算奖金。如果用子查询嵌套,代码会变成这样:

sql 复制代码
SELECT e.name, d.total_sales,
       d.total_sales * 0.1 AS bonus
FROM employees e
JOIN (
    SELECT dept_id, SUM(amount) AS total_sales
    FROM sales
    WHERE sale_date >= '2026-01-01'
    GROUP BY dept_id
) d ON e.dept_id = d.dept_id
WHERE d.total_sales > (
    SELECT AVG(total_sales)
    FROM (
        SELECT dept_id, SUM(amount) AS total_sales
        FROM sales
        WHERE sale_date >= '2026-01-01'
        GROUP BY dept_id
    ) avg_tbl
);

问题:同一段子查询写了两次,可读性极差,维护就是噩梦。

你需要的是一张臨时的"命名查询"------CTE(公用表表达式)


一、基本 CTE 语法

语句结构

sql 复制代码
WITH cte_name AS (
    -- 这里写你的子查询
    SELECT column1, column2, ...
    FROM table
    WHERE conditions
)
SELECT *
FROM cte_name
WHERE ...;

CTE 创建的临时结果集只在当前 SQL 语句中有效,查询结束后自动销毁,不会写入磁盘。

场景示例:部门销售汇总

sql 复制代码
WITH dept_sales AS (
    SELECT dept_id,
           SUM(amount) AS total_sales,
           COUNT(*)    AS order_count
    FROM sales
    WHERE sale_date >= '2026-01-01'
    GROUP BY dept_id
)
SELECT d.dept_name, s.total_sales, s.order_count
FROM dept_sales s
JOIN departments d ON s.dept_id = d.dept_id
ORDER BY s.total_sales DESC;

执行结果示意

dept_name total_sales order_count
技术部 1,280,000 342
市场部 950,000 215
运营部 620,000 178

对比开头的三层嵌套版本,CTE 版本逻辑清晰得像在读一篇文章。


二、多 CTE --- 一个 WITH 搞定多条"临时表"

一个 WITH 后面可以跟多个 CTE,用逗号分隔。每个 CTE 可以引用前面定义的 CTE。

场景示例:先算部门销售,再算奖金

sql 复制代码
WITH dept_sales AS (
    SELECT dept_id, SUM(amount) AS total_sales
    FROM sales
    WHERE sale_date >= '2026-01-01'
    GROUP BY dept_id
),
avg_sales AS (
    SELECT AVG(total_sales) AS avg_amount
    FROM dept_sales
),
high_performers AS (
    SELECT ds.dept_id, ds.total_sales
    FROM dept_sales ds
    CROSS JOIN avg_sales av
    WHERE ds.total_sales > av.avg_amount
)
SELECT e.name, e.dept_id, hp.total_sales,
       ROUND(hp.total_sales * 0.08, 2) AS bonus
FROM employees e
JOIN high_performers hp ON e.dept_id = hp.dept_id;

执行结果示意

name dept_id total_sales bonus
张三 101 1,280,000 102,400
李四 101 1,280,000 102,400
王五 102 950,000 76,000

要点

  • 多个 CTE 之间用逗号分隔,最后一个 CTE 后面没逗号
  • CTE 可以链式引用high_performers 引用了 dept_salesavg_sales
  • 最终主查询引用最后一个 CTE(或任意一个)

三、CTE vs 子查询 --- 性能和可读性对比

可读性对比

sql 复制代码
-- ❌ 子查询嵌套(地狱级阅读体验)
SELECT name FROM employees
WHERE dept_id IN (
    SELECT dept_id FROM (
        SELECT dept_id, RANK() OVER (ORDER BY sales DESC) AS rk
        FROM dept_stats
    ) ranked WHERE rk <= 3
);

-- ✅ CTE 版本(线性逻辑,一目了然)
WITH ranked_depts AS (
    SELECT dept_id, RANK() OVER (ORDER BY sales DESC) AS rk
    FROM dept_stats
)
SELECT name FROM employees
WHERE dept_id IN (
    SELECT dept_id FROM ranked_depts WHERE rk <= 3
);

性能对比

对比维度 子查询 CTE
执行计划 每次出现都独立执行(非物化) 大多数数据库会内联展开
是否物化 ❌ 不物化(除非使用 MATERIALIZED 关键字) PostgreSQL 支持 WITH AS MATERIALIZED
多次引用 子查询写多次 → 执行多次 写一次,但数据库可能内联多次
索引利用 取决于优化器 取决于优化器,CTE 本身不额外消耗

⚠️ CTE 不是性能银弹 。 数据库查询优化器通常会把 CTE 内联到主查询中,所以不一定比子查询快。 但如果 CTE 被主查询多次引用(例如 JOIN 了两次),数据库可能会执行 CTE 两次。 PostgreSQL 12+ 提供 MATERIALIZED / NOT MATERIALIZED 关键字来显式控制。

sql 复制代码
-- PostgreSQL 显式控制物化
WITH dept_sales AS MATERIALIZED (
    SELECT dept_id, SUM(amount) AS total
    FROM sales GROUP BY dept_id
)
SELECT * FROM dept_sales WHERE total > 100000;

什么时候选 CTE?

  1. 同一个子查询被引用多次 → 用 CTE 避免重复代码
  2. 查询逻辑复杂需要拆解 → 用 CTE 分步表达
  3. 需要递归查询(下一节) → 必须用 CTE
  4. 子查询只用一次且逻辑简单 → 直接用子查询更短

四、递归 CTE --- 处理树形结构(拓展)

CTE 最大的杀手锏是支持递归,子查询做不到。

语法结构

sql 复制代码
WITH RECURSIVE cte_name AS (
    -- 锚点:非递归的初始查询
    SELECT ... FROM table WHERE condition
    UNION ALL
    -- 递归部分:引用 CTE 自身
    SELECT ... FROM table JOIN cte_name ON ...
)
SELECT * FROM cte_name;

场景:组织架构树

sql 复制代码
WITH RECURSIVE org_tree AS (
    -- 锚点:找到 CEO(没有上级)
    SELECT id, name, manager_id, 1 AS level
    FROM employees
    WHERE manager_id IS NULL

    UNION ALL

    -- 递归:找到每个下属,层级 +1
    SELECT e.id, e.name, e.manager_id, ot.level + 1
    FROM employees e
    JOIN org_tree ot ON e.manager_id = ot.id
)
SELECT id, name, level,
       REPEAT('  ', level - 1) || name AS tree_display
FROM org_tree
ORDER BY level, id;

执行结果示意

id name level tree_display
1 陈总 1 陈总
2 张总监 2 张总监
3 李经理 3 李经理
4 王组长 4 王组长
5 赵经理 3 赵经理

注意事项

  • 必须有 RECURSIVE 关键字
  • 递归部分必须用 UNION ALL(不能是 UNION
  • 必须有终止条件 (JOIN 条件中的 ot.id),否则死循环
  • MySQL 5.7 及更低版本不支持递归 CTE

五、实际应用场景

1. 数据分页 + 排名

sql 复制代码
WITH user_rank AS (
    SELECT id, name, score,
           ROW_NUMBER() OVER (ORDER BY score DESC) AS rn
    FROM users
)
SELECT *
FROM user_rank
WHERE rn BETWEEN 11 AND 20;

2. 多步聚合:先分组汇总,再排序

sql 复制代码
WITH monthly_stats AS (
    SELECT DATE_TRUNC('month', order_date) AS month,
           SUM(amount) AS revenue
    FROM orders
    GROUP BY DATE_TRUNC('month', order_date)
)
SELECT month, revenue,
       revenue - LAG(revenue) OVER (ORDER BY month) AS mom_change
FROM monthly_stats;

3. 数据清洗中的中间步骤

sql 复制代码
WITH cleaned AS (
    SELECT id, email, TRIM(name) AS name,
           COALESCE(phone, '未知') AS phone
    FROM raw_users
    WHERE email IS NOT NULL
),
duplicated AS (
    SELECT id, name, email,
           ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn
    FROM cleaned
)
SELECT id, name, email, phone
FROM duplicated WHERE rn = 1;

注意事项

  1. CTE 名在当前语句中必须唯一,不能重名
  2. CTE 不是临时表 --- 它不会写入磁盘,无法被其他会话访问
  3. MySQL 5.6 及之前不支持 CTE(5.7 开始支持非递归,8.0 支持递归)
  4. SQLite 3.34+ 支持 CTE 和递归 CTE
  5. Oracle 早在 9i 就支持了,语法略有不同(使用 WITH ... AS (SELECT ...) SELECT ... 一致)
  6. SQL Server 支持,CTE 后必须紧跟使用它的语句(不能分号结束再查询)
  7. 调试时可以用 SELECT * FROM cte_name 分段验证每个 CTE 的结果

今日总结

知识点 一句话
基本 CTE WITH name AS (query) SELECT ... FROM name
多 CTE 逗号分隔、可链式引用
CTE vs 子查询 CTE 提升可读性,性能各有优劣
递归 CTE WITH RECURSIVE 处理树形结构
适用场景 分步复杂查询、多次引用、递归树形

一句话口诀:子查询让你活下来,CTE 让你活得优雅。

相关推荐
运维行者_1 小时前
如何查看每个IP的带宽使用情况?NetFlow 技术实战指南
开发语言·网络·分布式·后端·架构·带宽
虹科网络安全1 小时前
艾体宝新闻|从 SQL 注入到服务器接管:CVE-2026-57517 暴露 Web 管理面板的供应链与安全编码风险
服务器·前端·sql
济*沧*海1 小时前
如何判断接口的返回值?
后端
杨充2 小时前
5.多用组合和少继承
后端
杨充2 小时前
4.接口而非实现编程
java·后端·架构
一缕清烟在人间2 小时前
HarmonyOS应用开发实战:萌宠日记 - 生命周期在萌宠日记中的实践
后端
Conan在掘金2 小时前
鸿蒙 ArkUI 组件深水区:Span 富文本嵌套,一行 Text 塞下「红蓝斜下大」五样式 + 动态高亮
后端
卷无止境2 小时前
Python 类型注解与运行时反射:从原理到工程实践
后端·python
耀耀_很无聊2 小时前
13_Spring Boot 3.5.8 + Redisson 3.45.1 导致 Sa-Token 登录 StackOverflowError
spring boot·后端·bootstrap