告别套娃式子查询:SQL WITH 语句(CTE)深度实战指南

SQL WITH / CTE 整理笔记

学习目标:理解 WITH / CTE 是什么,知道它和子查询的关系,并能用它完成"先求中间结果,再继续查询"的两类典型练习。

1. 核心概念

  • WITH 语句也叫 CTE,全称是 Common Table Expression,中文常译为"公用表表达式"。
  • 可以把它理解为"给一段子查询结果起名字",这样主查询里就能像使用普通表一样去引用它。
  • 它不是永久表,也不是课程里机器翻译常说的 width classweight loss。这些都是误译,正确说法是 WITH clauseCTE
  • CTE 只在紧跟着它的那一条主语句中有效,语句执行结束后,这个名字就失效了。
  • 从学习和书写角度,可以把 CTE 理解成"先准备中间结果,再写主查询";实际执行时数据库优化器可能会调整执行计划,所以不要机械理解为"CTE 一定会被单独物化成真实临时表"。

2. 基本语法

2.1 单个 CTE

sql 复制代码
WITH cte_name (column1, column2, ...) AS (
    SELECT ...
)
SELECT ...
FROM cte_name;

说明:

  • cte_name 是这段中间结果的名字。
  • (column1, column2, ...) 是可选的列名列表,不写也可以。
  • AS (...) 里的查询负责产出中间结果。
  • 后面的主查询可以直接引用这个 cte_name

2.2 多个 CTE

sql 复制代码
WITH cte_1 AS (
    SELECT ...
),
cte_2 AS (
    SELECT ...
    FROM cte_1
)
SELECT ...
FROM cte_2;

说明:

  • 多个 CTE 用逗号分隔。
  • 后定义的 CTE 可以引用前面已经定义好的 CTE。
  • 这正是 WITH 很适合拆分复杂查询的原因。

3. 课程里的理解方式

这节课强调的不是"WITH 只能做什么",而是"什么时候它比嵌套子查询更清楚"。

可以先记住一个实用理解:

  1. 先把某个中间结果写出来,并起一个有意义的名字。
  2. 如果后续还要继续基于这个结果做筛选、聚合或关联,就在主查询里继续引用它。
  3. 当同一段子查询要重复使用,或者嵌套层次已经开始变乱时,WITH 往往比直接嵌套更好读。

4. 案例 1:找出薪资高于平均薪资的员工

4.1 题目

从员工表 emp 中筛选出薪资高于全表平均薪资的员工。

4.2 样例数据

sql 复制代码
DROP TABLE IF EXISTS emp;

CREATE TABLE emp (
    emp_id   INT,
    emp_name VARCHAR(50),
    salary   INT
);

INSERT INTO emp VALUES (101, 'Mohan', 40000);
INSERT INTO emp VALUES (102, 'James', 50000);
INSERT INTO emp VALUES (103, 'Robin', 60000);
INSERT INTO emp VALUES (104, 'Carol', 70000);
INSERT INTO emp VALUES (105, 'Alice', 80000);
INSERT INTO emp VALUES (106, 'Jimmy', 90000);

4.3 拆解思路

  1. 先求整张表的平均薪资。
  2. 再筛出 salary > 平均薪资 的员工。

4.4 子查询写法

sql 复制代码
SELECT *
FROM emp
WHERE salary > (
    SELECT CAST(AVG(salary) AS SIGNED)
    FROM emp
);

说明:

  • 内层子查询先算出平均薪资。
  • 外层查询再拿员工工资去比较。
  • 这个需求本身很简单,用子查询完全可以解决。

4.5 WITH 写法

sql 复制代码
WITH avg_sal (avg_salary) AS (
    SELECT CAST(AVG(salary) AS SIGNED)
    FROM emp
)
SELECT e.*
FROM emp e
JOIN avg_sal av
    ON e.salary > av.avg_salary;

4.6 对比结论

  • 这个例子主要是为了熟悉 WITH 的语法和引用方式。
  • 因为中间结果只用了一次,所以它不一定非得写成 WITH
  • 但通过这个例子可以先建立一个基本感觉:CTE 就像一个名字清晰的"中间结果表"。

5. 案例 2:找出销售额高于所有门店平均销售额的门店

5.1 题目

sales 表中找出总销售额高于所有门店平均销售额的门店。

5.2 样例数据

sql 复制代码
DROP TABLE IF EXISTS sales;

CREATE TABLE sales (
    store_id   INT,
    store_name VARCHAR(50),
    product    VARCHAR(50),
    quantity   INT,
    cost       INT
);

INSERT INTO sales VALUES
(1, 'Apple Originals 1', 'iPhone 12 Pro', 1, 1000),
(1, 'Apple Originals 1', 'MacBook pro 13', 3, 2000),
(1, 'Apple Originals 1', 'AirPods Pro', 2, 280),
(2, 'Apple Originals 2', 'iPhone 12 Pro', 2, 1000),
(3, 'Apple Originals 3', 'iPhone 12 Pro', 1, 1000),
(3, 'Apple Originals 3', 'MacBook pro 13', 1, 2000),
(3, 'Apple Originals 3', 'MacBook Air', 4, 1100),
(3, 'Apple Originals 3', 'iPhone 12', 2, 1000),
(3, 'Apple Originals 3', 'AirPods Pro', 3, 280),
(4, 'Apple Originals 4', 'iPhone 12 Pro', 2, 1000),
(4, 'Apple Originals 4', 'MacBook pro 13', 1, 2500);

说明:本例完全沿用课程脚本,直接使用 SUM(cost) 计算门店销售额。在真实业务里,如果 cost 表示单价,通常还要结合 quantity 计算,比如 SUM(quantity * cost)

5.3 拆解思路

这题比案例 1 更适合使用 WITH,因为它天然可以拆成三步:

  1. 先求每个门店的总销售额。
  2. 再求"所有门店总销售额"的平均值。
  3. 最后把"每个门店的总销售额"与"所有门店平均销售额"进行比较。

5.4 先求每个门店的总销售额

sql 复制代码
SELECT store_id,
       SUM(cost) AS total_sales_per_store
FROM sales
GROUP BY store_id;

根据课程样例数据,结果分别是:

  • 1 号门店:3280
  • 2 号门店:1000
  • 3 号门店:5380
  • 4 号门店:3500

5.5 再求所有门店平均销售额

sql 复制代码
SELECT CAST(AVG(total_sales_per_store) AS SIGNED) AS avg_sale_for_all_store
FROM (
    SELECT store_id,
           SUM(cost) AS total_sales_per_store
    FROM sales
    GROUP BY store_id
) x;

这里的平均值是:

text 复制代码
(3280 + 1000 + 5380 + 3500) / 4 = 3290

5.6 纯子查询写法

sql 复制代码
SELECT *
FROM (
    SELECT store_id,
           SUM(cost) AS total_sales_per_store
    FROM sales
    GROUP BY store_id
) total_sales
JOIN (
    SELECT CAST(AVG(total_sales_per_store) AS SIGNED) AS avg_sale_for_all_store
    FROM (
        SELECT store_id,
               SUM(cost) AS total_sales_per_store
        FROM sales
        GROUP BY store_id
    ) x
) avg_sales
    ON total_sales.total_sales_per_store > avg_sales.avg_sale_for_all_store;

问题在于:

  • SUM(cost) ... GROUP BY store_id 这段逻辑被写了两次。
  • 嵌套层级变深后,可读性会明显下降。
  • 后续如果逻辑再复杂一点,维护和排错都会更麻烦。

5.7 WITH 写法

sql 复制代码
WITH total_sales AS (
    SELECT store_id,
           SUM(cost) AS total_sales_per_store
    FROM sales
    GROUP BY store_id
),
avg_sales AS (
    SELECT CAST(AVG(total_sales_per_store) AS SIGNED) AS avg_sale_for_all_store
    FROM total_sales
)
SELECT *
FROM total_sales ts
JOIN avg_sales av
    ON ts.total_sales_per_store > av.avg_sale_for_all_store;

5.8 对比结论

  • 这才是本课最典型的 WITH 使用场景。
  • total_sales 先把"每店总销售额"这个中间结果命名出来。
  • avg_sales 再直接基于 total_sales 做下一步计算,不必重复写同一段子查询。
  • 主查询只需要表达"把两个中间结果关联起来,并做比较",意图会清晰很多。

最终结果应当是:

  • 3 号门店
  • 4 号门店

6. MySQL 易错点:CAST(... AS int) 不兼容

老师演示时使用了把平均值转成整数的思路,这个思路本身没问题,但如果你在 MySQL 中直接写:

sql 复制代码
CAST(AVG(salary) AS int)

就可能报语法错误,因为 MySQL 的 CAST() 不支持把目标类型直接写成 int

在 MySQL 中更稳妥的写法是:

sql 复制代码
CAST(AVG(salary) AS SIGNED)

或者:

sql 复制代码
CAST(AVG(salary) AS UNSIGNED)

常见可用类型包括:

  • SIGNED
  • UNSIGNED
  • DECIMAL(M,D)
  • CHAR
  • DATE
  • DATETIME
  • TIME

如果你的目的只是"去掉或控制小数部分",也可以考虑:

  • ROUND()
  • FLOOR()
  • TRUNCATE()

7. 什么时候适合用 WITH

7.1 适合使用的场景

  • 同一个子查询结果要被复用多次。
  • 查询很长、很绕,嵌套子查询已经开始影响阅读。
  • 你想把大问题拆成几个小步骤,每一步都有明确名字。
  • 你希望先筛出一批中间结果,后续再继续聚合、关联或过滤。

7.2 不一定非要使用的场景

  • 只是一个非常短、非常直观、只用一次的子查询。
  • 用普通子查询写出来已经很清楚,没有重复逻辑。

7.3 关于性能的更稳妥理解

  • 课程里强调了 WITH 有助于避免重复书写和重复计算,这个方向是对的。
  • 但在真实数据库里,性能是否更好,还要看数据库版本、优化器行为和具体 SQL 写法。
  • 所以更稳妥的结论是:WITH 常常能提升可读性与维护性,在部分复用或先过滤后复用的场景里,也可能带来更好的执行效果,但不能简单理解为"只要用了 CTE 就一定更快"。

8. CTE 列名列表什么时候有用

课程里提到,WITH avg_sal (avg_salary) AS (...) 里的 (avg_salary) 可以不写,因为很多时候 SQL 能自动继承子查询里的列名。

但下面这些场景,显式写列名会更清楚:

  • 你想让中间结果的输出列一眼就能看懂。
  • 子查询里有聚合函数、表达式或复杂计算列。
  • 你在多表关联中遇到了重复列名。
  • 你写的是递归 CTE,需要把输出列先定义清楚。

示例:

sql 复制代码
WITH dept_avg (dept_id, avg_salary) AS (
    SELECT dept_id,
           AVG(salary)
    FROM emp
    GROUP BY dept_id
)
SELECT *
FROM dept_avg;

9. 延伸补充:递归 CTE

这节课的重点不是递归查询,但要知道:

  • 普通 CTE 主要用于拆分复杂查询。
  • WITH RECURSIVE 还能处理树形结构、层级结构和路径展开等问题。

例如:

sql 复制代码
WITH RECURSIVE subordinates AS (
    SELECT emp_id, emp_name, manager_id
    FROM emp
    WHERE emp_name = 'CEO'

    UNION ALL

    SELECT e.emp_id, e.emp_name, e.manager_id
    FROM emp e
    JOIN subordinates s
        ON e.manager_id = s.emp_id
)
SELECT *
FROM subordinates;

这部分先知道用途即可,不是本节整理笔记的重点。

10. 本节笔记结论

  • WITH 就是给中间结果命名,让 SQL 能分步骤表达。
  • 它最核心的价值是:复用、拆解复杂逻辑、提高可读性。
  • 简单查询不一定非要用 WITH,但复杂查询往往会因为 WITH 变得更清楚。
  • 在 MySQL 中要特别注意 CAST(... AS int) 的兼容性问题,优先使用 SIGNEDUNSIGNED
  • 这节课最值得反复练习的,是第二个门店销售额案例,因为它最能体现 CTE 的真实价值。
相关推荐
流觞 无依2 小时前
DedeCMS plus/vote.php SQL注入漏洞修复教程
sql·php
zzh0812 小时前
PG数据库日常应用
数据库·oracle
阿维的博客日记2 小时前
MySQL中type字段解析
数据库·mysql
Trouvaille ~2 小时前
【MySQL篇】表的操作:数据的容器
linux·数据库·mysql·oracle·xshell·ddl·表的操作
黑牛儿2 小时前
从0开始实现Mysql主从配置实战
服务器·数据库·后端·mysql
爱学习的小囧2 小时前
vSphere 9.0 API 实操教程 —— 轻松检索 vGPU 与 DirectPath 配置文件
linux·运维·服务器·网络·数据库·esxi·vmware
麦聪聊数据2 小时前
数据库安全与运维管控(一):MySQL、PG与Oracle原生审计机制对比
运维·数据库·mysql·oracle
ZHENGZJM2 小时前
后端基石:Go 项目初始化与数据库模型设计
开发语言·数据库·golang
小小程序员.¥2 小时前
oracle--plsql块、存储过程、存储函数
数据库·sql·oracle