🪟 第8章 SQL数据查询基础
📄 一句话概括

执行顺序是 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 秒实验

  1. 写一条 SELECT cat, gname, COUNT(*) FROM tb_goods GROUP BY cat,看它报不报错。 SQL Server 会报「列名无效」,SQLite 会给个随便挑的值——两家的严格程度不同。
  2. 把一个能在 WHERE 里写的条件挪到 HAVING 里,跑一次执行计划。 处理的行数会变多——因为筛得晚了。
  3. 想一想:为什么 ORDER BY 能用 SELECT 里起的别名? 因为它在第 6 步,而 SELECT 在第 5 步。 别名已经算出来了。

常见误区

  1. 「WHERE 和 HAVING 差不多,随便用哪个」。WHERE 筛行(分组前), HAVING 筛组(分组后)。能用 WHERE 的别用 HAVING——筛得早,后面处理的数据少。
  2. 「GROUP BY 之后 SELECT 想写什么都行」。只能写分组列和聚合函数。 写别的在 SQL Server 上直接报错,MySQL 会给个随便挑的值——那个值换个执行计划就变了。
  3. 「不写 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 被移除了,教材还留着是因为老系统里能看到——新代码别用。

相关笔记