重新学一遍 SQL 执行顺序:FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
为什么"执行顺序"值得重新学一遍?
很多人写 SQL 是这样的:脑子里想着"我要查什么",手就直接写 SELECT 开头的语句。结果一遇到复杂查询就翻车------
- WHERE 里用别名报错,为什么?
- WHERE 里不能写聚合函数,凭什么?
- LEFT JOIN 明明连了表,结果却比预想少?
- HAVING 和 WHERE 到底什么时候该用哪个?
一、书写顺序 ≠ 执行顺序
你写出来的 SQL,从上到下是这样的(书写顺序):
sql
SELECT -- 3. 想查哪些列
FROM -- 1. 从哪张表
JOIN -- 2. 连哪张表
WHERE -- 4. 过滤哪些行
GROUP BY -- 5. 怎么分组
HAVING -- 6. 过滤哪些组
ORDER BY -- 7. 怎么排序
LIMIT -- 8. 取前几条
但数据库实际执行的顺序是这样的:
sql
FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
核心认知:数据库拿到数据之后,是一步一步"加工"的------每一步吃进上一步的结果,吐出自己的结果,像一个工厂流水线。
这条流水线可以一句话记住:
先拿数据 → 再过滤 → 再分组 → 再算列 → 最后排序和分页
二、八步流水线逐段拆解
第 1 步:FROM ------ 打开数据源头
从哪张表取数据,就从哪开始。此时产生的是原始行集合。
sql
FROM orders
如果是子查询,子查询会在这里先执行完,产出一个临时表供后面使用:
sql
FROM (SELECT id FROM orders WHERE status = 'PAID') t
第 2 步:JOIN ------ 把表拼起来
JOIN 发生在 FROM 之后、WHERE 之前。这一步把多张表按 ON 条件拼接,产出一个连接后的宽表。
sql
FROM customers c
JOIN orders o ON o.customer_id = c.id
这一步之后,后面所有步骤看到的都是"客户 × 订单"拼接后的完整行。
高频易错点①(ON vs WHERE 的坑) :
对
LEFT JOIN来说,右表的条件写在 ON 里 还是 WHERE 里,结果完全不同。
- 写在 ON:保留左表所有行,右表不匹配的置 NULL
- 写在 WHERE:不匹配的行直接被过滤掉,LEFT JOIN 效果退化成 INNER JOIN
这是"没记录也要返回"类需求(比如统计每个客户,没订单也要显示)最经典的翻车点。
第 3 步:WHERE ------ 行级过滤
WHERE 对连接后的每一行 做判断,不满足条件的行直接丢掉。这一步过滤的是"行",还没分组、还没算列。
由此推出两个高频易错点:
高频易错点②(WHERE 不能用别名) :
SELECT 里的别名要等到第 6 步才"出生",WHERE 在第 3 步执行,此时别名还不存在。
sql-- 报错:Unknown column 'total' SELECT amount * 0.9 AS total FROM orders WHERE total > 100; -- ❌ 别名此时还不存在 -- 正确:写原始表达式 WHERE amount * 0.9 > 100; -- ✅
高频易错点③(WHERE 不能用聚合函数) :
聚合函数(COUNT/SUM/MAX...)要等 GROUP BY 分组后才有意义,WHERE 在第 3 步、分组在第 4 步,所以 WHERE 里写
SUM(x) > 100会直接报错------这是 HAVING 的地盘。
第 4 步:GROUP BY ------ 分组
把上面过滤完的行,按指定列的值分成若干组,每组一堆。
sql
GROUP BY c.name
分组之后有个重要约束:SELECT 里只能出现"分组列"和"聚合函数"。
sql
SELECT c.name, -- ✅ 分组列
COUNT(*) AS cnt, -- ✅ 聚合函数
c.address -- ❌ 非分组列、非聚合 → MySQL 默认模式会报错
FROM customers c
JOIN orders o ON o.customer_id = c.id
GROUP BY c.name;
为什么?分组后每组有多行,非分组列的"值"是不确定的(取哪一行?),数据库只能让你用"每组唯一的值"(分组列)或"把多行合成一个值"(聚合函数)。
第 5 步:HAVING ------ 组级过滤
GROUP BY 分组完成后,HAVING 对每一组做判断,淘汰不满足条件的组。
sql
GROUP BY c.name
HAVING SUM(o.amount) >= 1000 -- 只保留总金额 ≥ 1000 的组
高频易错点④(HAVING vs WHERE 的区别)------面试几乎必问:
维度 WHERE HAVING 时机 分组前 分组后 对象 过滤行 过滤组 聚合函数 ❌ 不能用 ✅ 可以用 别名 ❌ 不能用 MySQL 里可以用(标准 SQL 不建议) 一句话:WHERE 管行,HAVING 管组。
第 6 步:SELECT ------ 算列、起别名
到这里才开始真正"算你要的列":投影需要的字段、做表达式计算、给列起别名。别名在这一步才诞生,所以它只对后面的 ORDER BY 可见,对前面的 WHERE 不可见。
sql
SELECT c.name, SUM(o.amount) AS total
顺便一提:DISTINCT 去重也发生在这个阶段之后。
第 7 步:ORDER BY ------ 排序
对整个结果集排序。因为发生在 SELECT 之后,所以 ORDER BY 可以使用别名:
sql
ORDER BY total DESC
也可以排未出现在 SELECT 里的列(某些数据库下不推荐,但语法允许)。
第 8 步:LIMIT ------ 分页收尾
最后一步,按排序结果截取前 N 行(或分页)。
sql
LIMIT 20 -- 前 20 条
LIMIT 10, 20 -- MySQL:跳过 10 条,取 20 条
高频易错点⑤ :LIMIT 是关键字,不能加引号 ,也不能写
LIMIT = 20。它一定在 ORDER BY 之后,所以"先排序再取前 N 条"永远是对的。
三、一个完整例子,跟着流水线走一遍
假设有两张表:
customers(客户)
| id | name | level |
|---|---|---|
| 1 | 张三 | VIP |
| 2 | 李四 | NORMAL |
| 3 | 王五 | VIP |
orders(订单)
| id | customer_id | amount | status | created_at |
|---|---|---|---|---|
| 1 | 1 | 600 | PAID | 2026-06-10 |
| 2 | 1 | 500 | PAID | 2026-06-20 |
| 3 | 2 | 2000 | PAID | 2026-06-15 |
| 4 | 3 | 300 | PAID | 2026-06-18 |
| 5 | 3 | 900 | PAID | 2026-05-01 |
| 6 | 1 | 100 | PENDING | 2026-06-25 |
sql
SELECT c.name, SUM(o.amount) AS total
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE c.level = 'VIP'
AND o.status = 'PAID'
AND o.created_at >= '2026-06-01'
AND o.created_at < '2026-07-01'
GROUP BY c.name
HAVING SUM(o.amount) >= 1000
ORDER BY total DESC
LIMIT 2;
数据库实际怎么做:
| 步骤 | 做了什么 | 中间结果 |
|---|---|---|
| 1. FROM | 打开 customers、orders 两张表 | 两堆原始行 |
| 2. JOIN | 按 customer_id 拼接成宽表 | 6 行(客户×订单) |
| 3. WHERE | 过滤:VIP 客户 + 已支付 + 6 月订单 | 剩 3 行:张三600、张三500、王五900(李四非VIP去掉、王五5月单去掉、张三PENDING去掉) |
| 4. GROUP BY | 按客户名分组 | 组1: 张三(2行),组2: 王五(1行) |
| 5. HAVING | 总金额 ≥ 1000 | 张三(1100) 保留;王五(900) 淘汰 |
| 6. SELECT | 算列:SUM、起别名 total | 张三 1100 |
| 7. ORDER BY | total 倒序 | 张三 1100 |
| 8. LIMIT | 取前 2 条 | 张三 1100(不足 2 条则全返) |
最终结果:
| name | total |
|---|---|
| 张三 | 1100 |
看到没?王五的 5 月订单在 WHERE 就被过滤了,没进分组;王五 6 月只有 900,在 HAVING 被淘汰;整个过程一步不差。
四、高频易错点速查表(背下来)
| # | 易错点 | 原因 | 正确做法 |
|---|---|---|---|
| 1 | WHERE 用 SELECT 别名 | 别名第 6 步才诞生,WHERE 在第 3 步 | WHERE 写原始表达式 |
| 2 | WHERE 用聚合函数 | 聚合要等分组(第 4 步)后才有 | 聚合条件放 HAVING |
| 3 | LEFT JOIN 右表条件放 WHERE | 不匹配行被过滤,退化成 INNER JOIN | 右表条件放 ON |
| 4 | SELECT 出现非分组列 | 分组后该值不确定 | 只写分组列 + 聚合函数 |
| 5 | LIMIT 加引号 / 加等号 | LIMIT 是关键字、不是变量 | 写 LIMIT 20 |
| 6 | 分不清 HAVING 和 WHERE | 混淆了行过滤和组过滤 | 行用 WHERE,组用 HAVING |
五、记忆口诀
"从哪来、怎么拼、滤行、分组、滤组、算列、排序、截取"
对应:
- 从哪来 → FROM
- 怎么拼 → JOIN
- 滤行 → WHERE
- 分组 → GROUP BY
- 滤组 → HAVING
- 算列 → SELECT
- 排序 → ORDER BY
- 截取 → LIMIT
再配一句灵魂总结:
SELECT 是"最后才算账"的------它是流水线的后半段,不是起点。