告别套娃式子查询: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 的真实价值。
相关推荐
zh1570233 小时前
JavaScript中WorkerThreads解决服务端计算瓶颈
jvm·数据库·python
代码AI弗森3 小时前
一文理清楚“算力申请 / 成本测算 / 并发评估”
java·服务器·数据库
摇滚侠4 小时前
expdp 查看帮助
java·数据库·oracle
流年似水~4 小时前
MCP协议实战:从零搭建一个让Claude能“看见“数据库的工具服务
数据库·人工智能·程序人生·ai·ai编程
2401_871492855 小时前
Vue.js监听器watch利用回调函数处理级联下拉框数据联动
jvm·数据库·python
志栋智能5 小时前
超自动化安全:构建智能安全运营的核心引擎
大数据·运维·服务器·数据库·安全·自动化·产品运营
zhoutongsheng6 小时前
C#怎么实现Swagger文档 C#如何在ASP.NET Core中集成Swagger自动生成API文档【框架】
jvm·数据库·python
WinterKay6 小时前
【开源】我写了一个轻量级本地数据库浏览工具,支持 MySQL/Redis 只读查询
数据库·mysql·开源
zxrhhm7 小时前
Oracle 索引完整指南
数据库·oracle
程序猿乐锅8 小时前
【Tilas|第三篇】多表SQL语句
数据库·经验分享·笔记·学习·mysql