找回密码
立即注册
搜索
发回帖 发新帖

4823

积分

0

好友

621

主题
发表于 昨天 23:33 | 查看: 2| 回复: 0

先看个 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 各自能用什么表达式,全都是由这个逻辑顺序决定的。别再把书写顺序当成执行顺序了。




上一篇:SQL NULL 为什么不能用等号判断?三值逻辑与 NOT IN 陷阱解析
下一篇:Ubuntu 26.10 Beta 发布:GNOME 51 加持,核心工具全面转向 Rust,Linux 7.3 同步登场
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-5 00:20 , Processed in 0.077619 second(s), 38 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表