执行顺序是 FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY,记住它就不用背规则。
WHERE 筛行、HAVING 筛组;WHERE 里不能用聚合函数,HAVING 里可以。
GROUP BY 之后,
SELECT 里只能出现「分组列」和「聚合函数」——写别的在 SQL Server 上直接报错。
这一节是第 8 章最核心的一节。四个子句各管一段,合起来才是完整的查询。
教材 8.1b 按 WHERE、GROUP BY、HAVING、ORDER BY 的顺序讲,
但执行顺序跟讲解顺序不一样——这个差别正是很多「为什么这里报错」的答案。
演示里用七行商品数据,把四个子句各跑一遍,最后给出完整的执行顺序表,再用它解释两件事:
为什么 WHERE 里不能用别名、为什么 HAVING 里能用聚合函数。
一、WHERE:先筛行
WHERE 是「一行一行判断」,符合条件的留下。 它在分组之前执行——
这一点决定了它不能用聚合函数。
SELECT gname, cat, price FROM tb_goods WHERE price >= 100
-- 下面这句会报错:
SELECT cat FROM tb_goods WHERE SUM(price) > 100 GROUP BY cat
为什么 WHERE SUM(price) 不行? 因为执行到 WHERE 的时候还没分组,SUM 不知道对谁求和。
要筛「分组之后的结果」,得用 HAVING。
WHERE 还有一个性能上的意义:它越早筛掉不需要的行,后面的步骤要处理的数据越少。
所以「能写在 WHERE 里的条件别写在 HAVING 里」——这不是风格问题,是效率问题。
二、GROUP BY:把行压成组
SELECT cat AS 类别, COUNT(*) AS 商品数,
ROUND(AVG(price),2) AS 均价, SUM(stock) AS 总库存
FROM tb_goods GROUP BY cat
这里有一条硬规则,必须记住:GROUP BY 之后,SELECT 里只能出现「分组列」和「聚合函数」。
为什么? 一个类别下有多个商品名,
分组之后只剩一行——
数据库不知道该显示哪一个。
所以 SELECT gname, COUNT(*) ... GROUP BY cat 在 SQL Server 上直接报错。
MySQL 默认允许这种写法,它会「随便挑一个」。 这一点两家不一样——
而且 MySQL 的行为是个坑:换个执行计划,挑的那个就变了。
分组列可以不止一个。 GROUP BY cat, gname 就是「类别 + 商品名」的组合分组——
组合里有几对不同的值,结果就有几行。
三、HAVING:筛分组之后的结果
SELECT cat AS 类别, SUM(stock) AS 总库存
FROM tb_goods GROUP BY cat HAVING SUM(stock) > 100
HAVING 和 WHERE 的区别就一句话:WHERE 筛行,HAVING 筛组。
其他的区别都是从这一条推出来的。
HAVING 里能用聚合函数,因为它执行在 GROUP BY 之后——那时候组已经分好了,SUM、
AVG 都知道该对谁算了。
HAVING 里也能用普通条件,比如 HAVING cat = '电脑'——但这么写是浪费,
因为这个条件在 WHERE 里就能筛掉,何必等分组。能提前筛的,就别留到后面。
四、ORDER BY:最后一步
SELECT cat, gname, price FROM tb_goods
ORDER BY cat ASC, price DESC
ORDER BY 是唯一能保证结果顺序的子句。 不写它,
数据库返回的顺序不保证——今天按主键排,明天加了索引可能就变了。
这种问题特别难查,因为它「昨天还好好的」。 而根因往往就是「依赖了返回顺序」。
所以只要顺序重要,就写 ORDER BY。
多列排序从左往右依次生效。 上面那行是「先按类别升序,
同类别里按价格降序」——第一个列分不开的,才轮到第二个列。
ORDER BY 还能用列序号,比如 ORDER BY 3 DESC。 能用但不建议——
列顺序一改,这条语句就悄悄换了排序依据,而且不报错。
五、执行顺序:一张表解释所有规则
| 顺序 | 子句 | 干什么 |
|---|---|---|
| 1 | FROM |
确定数据从哪来 |
| 2 | WHERE |
筛掉不要的行 |
| 3 | GROUP BY |
把行压成组 |
| 4 | HAVING |
筛掉不要的组 |
| 5 | SELECT |
挑列、算别名、算聚合 |
| 6 | ORDER BY |
排序 |
这张表能解释三条看起来没道理的规则:
第一条:WHERE 里不能用别名。 因为 SELECT 在第 5 步才算别名,
而 WHERE 在第 2 步就要用了——那时候别名还不存在。
第二条:WHERE 里不能用聚合函数。 因为 GROUP BY 在第 3 步才分组,
WHERE 在第 2 步——那时候还没有「组」这个东西。
第三条:ORDER BY 能用别名、也能用聚合函数。 因为它在最后一步,前面该算的都算完了。
所以 ORDER BY 金额 DESC 和 ORDER BY SUM(price) 都是合法的。
六、动手 Demo:四个子句各跑一遍
这一段用七行商品数据把四个子句各跑一次:WHERE 筛行、GROUP BY 压组、HAVING 筛组、
ORDER BY 多列排序,最后给出完整执行顺序表。
import sqlite3
c = sqlite3.connect(u":memory:")
c.execute(u"CREATE TABLE tb_goods (id INTEGER PRIMARY KEY, gname TEXT,"
u" cat TEXT, price REAL, stock INT)")
c.executemany(u"INSERT INTO tb_goods VALUES (?,?,?,?,?)", [
(1, '笔记本', '电脑', 4999.0, 12), (2, '键盘', '电脑', 199.0, 40),
(3, '鼠标', '电脑', 89.5, 0), (4, '显示器', '电脑', 1299.0, 7),
(5, '打印纸', '办公', 25.0, 200), (6, '订书机', '办公', 18.5, 60),
(7, '文件夹', '办公', 6.0, 150)])
print(c.execute(u"SELECT gname, price FROM tb_goods WHERE price >= 100").fetchall())
print(c.execute(u"""SELECT cat, COUNT(*), ROUND(AVG(price),2), SUM(stock)
FROM tb_goods GROUP BY cat""").fetchall())
print(c.execute(u"""SELECT cat, SUM(stock) FROM tb_goods
GROUP BY cat HAVING SUM(stock) > 100""").fetchall())
print(c.execute(u"""SELECT cat, gname, price FROM tb_goods
ORDER BY cat ASC, price DESC""").fetchall())
实跑输出:
--------------------------------------------------------------------
WHERE、GROUP BY、HAVING、ORDER BY
--------------------------------------------------------------------
① WHERE:先筛行
--------------------------------------------------------------------
gname cat price
笔记本 电脑 4999.0
键盘 电脑 199.0
显示器 电脑 1299.0
WHERE 在分组之前执行——它筛的是「行」,一行一行判断。
所以 WHERE 后面不能写聚合函数:WHERE SUM(price) > 100 会报错,
因为那时候还没分组,SUM 不知道对谁求和。要筛分组结果,得用 HAVING。
② GROUP BY:把行压成组
--------------------------------------------------------------------
按类别分组,算每类的商品数、均价、总库存:
办公 3 16.5 410
电脑 4 1646.63 59
关键规则:SELECT 里只能出现「分组列」和「聚合函数」。
写了 SELECT gname, COUNT(*) ... GROUP BY cat,gname 会报错——
因为一个类别下有多个 gname,它不知道显示哪一个。
MySQL 默认允许这种写法(随便挑一个),SQL Server 严格禁止——这一点两家不一样。
③ HAVING:筛分组之后的结果
--------------------------------------------------------------------
只要总库存超过 100 的类别:
办公 410
HAVING 和 WHERE 的区别就一句:WHERE 筛行,HAVING 筛组。
执行顺序是 FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY。
所以 HAVING 里可以用聚合函数,WHERE 里不行——这不是规定,是执行顺序决定的。
能写在 WHERE 里的条件别写在 HAVING 里:先筛掉不需要的行,分组的数据量就小,更快。
④ ORDER BY:最后一步
--------------------------------------------------------------------
按类别升序、同类别按价格降序:
办公 打印纸 25.0
办公 订书机 18.5
办公 文件夹 6.0
电脑 笔记本 4999.0
电脑 显示器 1299.0
电脑 键盘 199.0
电脑 鼠标 89.5
ASC 是升序(默认,可以不写),DESC 是降序。多列排序从左往右依次生效。
ORDER BY 是唯一能保证结果顺序的子句。不写它,数据库返回的顺序不保证——
今天按主键排,明天加了索引可能就变了。这种「昨天还好好的」问题都出在这里。
ORDER BY 还能用列序号:ORDER BY 3 DESC 表示按第 3 列排。能用但不建议——
列顺序一改,这条语句就悄悄换了排序依据,而且不报错。
⑤ 四个子句的执行顺序
--------------------------------------------------------------------
| 顺序 | 子句 | 干什么 |
|---|---|---|
| 1 | FROM | 确定数据从哪来 |
| 2 | WHERE | 筛掉不要的行 |
| 3 | GROUP BY | 把行压成组 |
| 4 | HAVING | 筛掉不要的组 |
| 5 | SELECT | 挑出要显示的列、算别名 |
| 6 | ORDER BY | 排序 |
这张表解释了为什么「WHERE 里不能用别名」——SELECT 在第 5 步才算别名,
而 WHERE 在第 2 步就要用了,那时候别名还不存在。
ORDER BY 在第 6 步,所以它能用别名:ORDER BY 金额 DESC 是合法的。
第一行返回三个:笔记本、键盘、显示器。 鼠标 89.5 不够 100,
被筛掉了——注意这是「筛行」,四行电脑还剩三行。
第二行是按类别分组的结果:电脑 4 个、均价 1646.63、总库存 59;
办公 3 个、均价 16.5、总库存 410。
注意总库存:电脑只有 59,办公有 410——因为办公用品单价低、库存大。
第三行只剩「办公」——因为电脑的总库存 59 不到 100。这就是 HAVING 做的事:
它筛的是「组」,不是「行」。
第四行是排序。 先按类别(办公在前、电脑在后,因为拼音 b 在 d 前),
同类别里按价格降序——办公那一组是打印纸 25、订书机 18.5、文件夹 6。
30 秒实验
- 写一条
SELECT cat, gname, COUNT(*) FROM tb_goods GROUP BY cat,看它报不报错。 SQL Server 会报「列名无效」,SQLite 会给个随便挑的值——两家的严格程度不同。 - 把一个能在
WHERE里写的条件挪到HAVING里,跑一次执行计划。 处理的行数会变多——因为筛得晚了。 - 想一想:为什么
ORDER BY能用SELECT里起的别名? 因为它在第 6 步,而SELECT在第 5 步。 别名已经算出来了。
常见误区
- 「
WHERE和HAVING差不多,随便用哪个」。WHERE筛行(分组前),HAVING筛组(分组后)。能用WHERE的别用HAVING——筛得早,后面处理的数据少。 - 「
GROUP BY之后SELECT想写什么都行」。只能写分组列和聚合函数。 写别的在 SQL Server 上直接报错,MySQL 会给个随便挑的值——那个值换个执行计划就变了。 - 「不写
ORDER BY,返回顺序就是主键顺序」。不保证。 加了索引、改了执行计划,顺序就可能变。顺序重要就写ORDER BY——这是唯一能保证的办法。
自测三问(点开对答案)
1. 四个子句的执行顺序是什么? —— **`FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY`。**记住这个顺序, 就能推出「哪里能用别名」「哪里能用聚合函数」——**不用背规则。** 2. 为什么 `WHERE` 里不能用聚合函数? —— **因为 `WHERE` 在 `GROUP BY` 之前执行——那时候还没有「组」。**`SUM`、`AVG` 不知道对谁算。 要筛分组结果,得用 `HAVING`。 3. `GROUP BY cat` 之后,`SELECT gname` 为什么报错? —— **因为一个类别下有多个 `gname`,分组之后只剩一行,数据库不知道显示哪一个。** `SELECT` 里只能出现分组列和聚合函数。下一篇 8.1c 讲 DISTINCT、TOP、WITH TIES、UNION。
其中 COMPUTE 子句已经在 2012 被移除了,教材还留着是因为老系统里能看到——新代码别用。
- ☐ (本篇完,下一篇 8.1c COMPUTE、DISTINCT、TOP、WITH TIES 与 UNION)