先看个 SQL:
SELECTCOUNT(*) AS cnt FROM employee WHERE cnt > 3;
-- 报错:Unknown column 'cnt'
cnt 明明就写在上面,为什么 WHERE 说找不到?
书写顺序和执行顺序是两回事
一条完整的 SQL 写出来大概是这样的:
SELECT department, COUNT(*) AS cnt
FROM employee
WHERE age > 25
GROUPBY department
HAVINGCOUNT(*) > 3
ORDERBY cnt DESC
LIMIT10;
我们习惯的书写顺序是 SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT。
但数据库拿到这条 SQL 之后,内部的实际执行顺序完全不是这么走的:
1. FROM → 找到 employee 表,确定数据来源
2. WHERE → 过滤行,把 age <= 25 的行扔掉
3. GROUP BY → 按 department 分组
4. HAVING → 过滤分组,把 COUNT(*) <= 3 的组扔掉
5. SELECT → 选择输出列,计算 COUNT(*),起别名 cnt
6. ORDER BY → 按 cnt 排序
7. LIMIT → 取前 10 条
SELECT 写在最前面,执行时却排在倒数第三;FROM 写在第二位,反而是第一个执行的。
搞懂这个顺序之后,开头那个报错就好理解了:WHERE 在第 2 步执行,SELECT 在第 5 步执行,执行到 WHERE 的时候 SELECT 还没开始跑,cnt 这个别名压根还没出生。
HAVING 为什么能用聚合函数?
HAVING 也在 SELECT 前面执行(第 4 步 vs 第 5 步),那为什么 HAVING 里能写 COUNT(*) > 3?
SELECT department, COUNT(*) AS cnt
FROM employee
GROUPBY department
HAVINGCOUNT(*) > 3;
因为 HAVING 的设计目标就是过滤 GROUP BY 之后的分组结果。数据库对 HAVING 里的聚合函数做了特殊处理,允许在这个阶段计算聚合值。但 HAVING 同样不能用 SELECT 里定义的别名——你写 HAVING cnt > 3 在标准 SQL 里是不合法的。MySQL 对此做了扩展允许这么写,但换个数据库比如 PostgreSQL 就会直接报错。
而 ORDER BY 的情况就不一样了:
SELECT department, COUNT(*) AS cnt
FROM employee
GROUPBY department
ORDERBY cnt DESC;
ORDER BY 在第 6 步执行,SELECT 在第 5 步,别名已经生效,所以 ORDER BY cnt DESC 完全没问题。
WHERE 和 HAVING 到底怎么分工
这俩都能过滤数据,但工作时机完全不同。
WHERE 在 GROUP BY 之前执行,过滤的是原始的行数据;HAVING 在 GROUP BY 之后执行,过滤的是分组后的结果。
SELECT department, AVG(age) AS avg_age
FROM employee
WHEREposition <> '实习生'
GROUPBY department
HAVINGAVG(age) > 30;
这条 SQL 的执行过程:先用 WHERE 把实习生排除掉,然后按部门分组,最后用 HAVING 筛出平均年龄超过 30 的部门。
一句话总结:WHERE 筛行,HAVING 筛组。WHERE 里不能出现聚合函数,因为数据还没分组,COUNT、AVG 根本算不了;HAVING 可以,因为分组已经完成。
JOIN 和 ON 插在哪里
如果 SQL 里带 JOIN,完整的执行顺序变成这样:
FROM → JOIN → ON → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT
JOIN 和 ON 在 FROM 之后、WHERE 之前执行。拿个具体例子看:
SELECT e.name, d.dept_name
FROM employee e
JOIN department d ON e.dept_id = d.id
WHERE e.age > 25;
执行流程:先加载 employee 表,再和 department 表做连接,连接条件是 e.dept_id = d.id;连接完成后再用 WHERE 过滤 age > 25 的行,最后 SELECT 挑选输出列。
在 INNER JOIN 里,把过滤条件写在 ON 里还是 WHERE 里,结果是一样的。但换成 LEFT JOIN,差别就大了:
-- 写法一:条件放在 ON 里
SELECT e.name, d.dept_name
FROM employee e
LEFTJOIN department d ON e.dept_id = d.id AND d.status = 1;
-- 所有员工都会出现,没匹配到部门的显示 NULL
-- 写法二:条件放在 WHERE 里
SELECT e.name, d.dept_name
FROM employee e
LEFTJOIN department d ON e.dept_id = d.id
WHERE d.status = 1;
-- 没有匹配部门的员工直接被 WHERE 干掉了
坑就坑在这里。
ON 在 JOIN 过程中生效,控制的是连接时的匹配规则:不匹配的右表行填 NULL,但左表行还在。WHERE 在 JOIN 完成之后生效,过滤的是连接后的结果集,右表字段为 NULL 的行会被 d.status = 1 直接筛掉。
写法二的 LEFT JOIN 等于白写了,效果跟 INNER JOIN 一模一样。这是个非常常见的坑,Code Review 里碰到 LEFT JOIN 加 WHERE 过滤右表字段的,十有八九是 bug。
怎么验证执行顺序
MySQL 的 EXPLAIN 可以查看查询执行计划:
EXPLAINSELECT department, COUNT(*) AS cnt
FROM employee
WHERE age > 25
GROUPBY department
HAVINGCOUNT(*) > 3
ORDERBY cnt DESC;
执行计划里几个关键列值得注意:type 告诉你访问方式(全表扫描还是走索引),key 显示实际用了哪个索引,rows 是预估扫描行数,Extra 列信息量最大。
Extra 里出现 Using temporary 说明用了临时表,通常是 GROUP BY 或 DISTINCT 触发的;出现 Using filesort 说明排序没法利用索引,ORDER BY 只能在内存或磁盘上额外排序。想深入执行计划细节,可以对照技术文档里的避坑指南。
逻辑顺序 vs 物理顺序
上面说的执行顺序是逻辑执行顺序,属于 SQL 标准定义的语义顺序。但数据库的查询优化器在实际执行时,物理顺序可能跟逻辑顺序完全不一样。
比如优化器发现 WHERE 条件能过滤掉 90% 的数据,它可能会把 WHERE 的过滤下推到 JOIN 之前执行,先把数据量砍下来再做连接,减少 JOIN 的计算量。这种优化涉及不少程序开发与编译优化的底层思路。
但不管优化器怎么调整物理执行顺序,最终输出的结果必须和按逻辑顺序执行的结果完全一致。优化器改的是「怎么跑得更快」,绝不会改「最终跑出什么结果」。
说在最后
理解逻辑执行顺序的意义就在这里:它决定了 SQL 的语义规则。哪些写法合法、哪些写法报错、WHERE 和 HAVING 各自能用什么表达式,全都是由这个逻辑顺序决定的。别再把书写顺序当成执行顺序了。