SQL WITH / CTE 整理笔记
学习目标:理解
WITH/CTE是什么,知道它和子查询的关系,并能用它完成"先求中间结果,再继续查询"的两类典型练习。
1. 核心概念
WITH语句也叫CTE,全称是Common Table Expression,中文常译为"公用表表达式"。- 可以把它理解为"给一段子查询结果起名字",这样主查询里就能像使用普通表一样去引用它。
- 它不是永久表,也不是课程里机器翻译常说的
width class、weight loss。这些都是误译,正确说法是WITH clause或CTE。 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 只能做什么",而是"什么时候它比嵌套子查询更清楚"。
可以先记住一个实用理解:
- 先把某个中间结果写出来,并起一个有意义的名字。
- 如果后续还要继续基于这个结果做筛选、聚合或关联,就在主查询里继续引用它。
- 当同一段子查询要重复使用,或者嵌套层次已经开始变乱时,
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 拆解思路
- 先求整张表的平均薪资。
- 再筛出
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,因为它天然可以拆成三步:
- 先求每个门店的总销售额。
- 再求"所有门店总销售额"的平均值。
- 最后把"每个门店的总销售额"与"所有门店平均销售额"进行比较。
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)
常见可用类型包括:
SIGNEDUNSIGNEDDECIMAL(M,D)CHARDATEDATETIMETIME
如果你的目的只是"去掉或控制小数部分",也可以考虑:
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)的兼容性问题,优先使用SIGNED或UNSIGNED。 - 这节课最值得反复练习的,是第二个门店销售额案例,因为它最能体现
CTE的真实价值。