🦏 第4章 数据统计
📄 一句话概括

where 筛行,having 筛组。 一句话分清楚:条件里出现聚合函数,就必须写 having。 Demo 里 4 堆订单加 having n > 2 变 2 堆; 同一个条件写进 where 会报 misuse of aggregate。 having 能用 select 的别名,where 不能; 两个一起用时是「先筛行 → 归堆 → 再筛组」,14 行变 2 堆。

📝 本章和前面几章的关系

上一篇把行归成了堆,这一篇决定哪些堆留下。 where 和 having 最容易混,因为两者都写条件、都用运算符。 分界线只有一条:看条件里有没有聚合函数。

一、分界线:条件里有没有聚合函数

where 跑在分组之前,那时还没有「堆」,所以它只认单行的列。 having 跑在分组之后, 堆已经在了,所以它能认 count(*)。 判断方法简单到不用想:条件里出现 count/sum/avg/max/min,就是 having; 全是单行能判断的条件,就是 where。 where count(*) > 2 必报错——Demo 里给的是 misuse of aggregate。 不是语法错,是「这时候还没堆可数」。

二、having 能用别名,where 不能

having n > 2 和 having count(*) > 2 结果一样, 因为 having 执行时 select 那一步算的别名已经在了。 这一点在第 3 章撞过:where double_h > 340 在 MySQL 上报 Unknown column。 同一个别名,having 和 order by 能用,where 不能用。 不过这里有个反例值得记住:按执行顺序,select 排在 having 后面, 严格说 having 也拿不到别名。 MySQL 额外做了别名解析,所以它认; 标准 SQL 不保证。 跨引擎的代码,having 里写聚合函数本身最稳。

三、两个一起用:先筛行,再筛组

where status <> '已取消' group by status having money > 200 分三步: 剔掉已取消(14 行变 13 行)→ 归成 3 堆 → 筛掉金额不到 200 的那堆。 顺序不能反,而且两个筛子拦的东西不一样: where 拦不住「已取消 156.00」这种过了金额门槛的堆; having 拦不住「已付款 138.40」这种单行正常、只是合计不够的堆。 所以「先 where 后 having」不只是语法规定,是逻辑必需:能早点剔掉的行,越早剔越好。

四、能写进 where 的都写进 where

having status = '已完成' 能给对结果,但白分组了:先把 14 行归成 4 堆,再扔掉 3 堆。 改成 where status = '已完成' 是先筛成 8 行再一次归堆, 参与分组的行少了,排序和临时表都省了。 规矩:筛「行」用 where,筛「组的计算结果」用 having。 凡是用 having 能写的、where 也能写的条件,一律挪到 where。

五、动手 Demo:从 4 堆筛到 2 堆,再撞一次报错

Demo 六段:先看 4 堆,加 having 筛成 2 堆,再换成聚合函数写法。 第四段故意把聚合塞进 where,看它报什么错; 后两段是两个筛子一起用,和 having 筛分组列的浪费写法。

import _q46 as Q
c = Q.con()

# 没有 having:四堆全靠
c.execute("SELECT status, COUNT(*) AS n FROM orders GROUP BY status "
          "ORDER BY n DESC")
# 加 having:只留 3 张以上的堆
c.execute("SELECT status, COUNT(*) AS n FROM orders GROUP BY status "
          "HAVING n > 2 ORDER BY n DESC")
# 写成聚合函数也行
c.execute("SELECT status, COUNT(*) AS n FROM orders GROUP BY status "
          "HAVING COUNT(*) > 2")
# where 里放聚合:报错
c.execute("SELECT status, COUNT(*) FROM orders WHERE COUNT(*) > 2 "
          "GROUP BY status")
# 两个一起用:先筛行再筛组
c.execute("SELECT status, COUNT(*) AS n, SUM(amount) AS money "
          "FROM orders WHERE status <> '已取消' GROUP BY status "
          "HAVING money > 200 ORDER BY money DESC")
# having 筛分组列:能过,但白分组
c.execute("SELECT status, COUNT(*) AS n FROM orders GROUP BY status "
          "HAVING status = '已完成'")

实跑输出:

--------------------------------------------------------------------
第十八课:having,分组之后再筛一遍
--------------------------------------------------------------------

  先看没有 having 时的四堆
  SQL> SELECT status, COUNT(*) AS n FROM orders GROUP BY status ORDER BY n DESC
  status     n
  ------------------
  已完成     8
  已发货     3
  已付款     2
  已取消     1
  (4 行)

  **四堆:8、3、2、1。** 现在想只留「3 张以上」的堆。

  加 having:筛掉整堆
  SQL> SELECT status, COUNT(*) AS n FROM orders GROUP BY status HAVING n > 2 ORDER BY n DESC
  status     n
  ------------------
  已完成     8
  已发货     3
  (2 行)

  **从 4 行变 2 行。`having n > 2` 说的是「这一堆的行数要大于 2」,不满足的整堆被丢掉。**

  having 里能用别名,也能直接写聚合
  SQL> SELECT status, COUNT(*) AS n FROM orders GROUP BY status HAVING COUNT(*) > 2
  status     n
  ------------------
  已发货     3
  已完成     8
  (2 行)

  **`having n > 2` 和 `having count(*) > 2` 结果一样。**
  **MySQL 允许 `having` 用 `select` 里的别名,`where` 不允许**——下一节用执行顺序解释这件事。

  where 里不能放聚合:这是两者的分界线
  SQL> SELECT status, COUNT(*) FROM orders WHERE COUNT(*) > 2 GROUP BY status
  报错:misuse of aggregate: COUNT()

  **`where` 跑的时候还没分组,`count(*)` 根本无从算起,所以这条一定报错。**
  **判据只有一句:条件里出现聚合函数,就必须写 `having`。**

  两个一起用:先筛行,再筛组
  SQL> SELECT status, COUNT(*) AS n, SUM(amount) AS money FROM orders WHERE status <> '已取消'
       GROUP BY status HAVING money > 200 ORDER BY money DESC
  status     n     money
  -------------------------
  已完成     8     1262.20
  已发货     3     412.50
  (2 行)

  **`where` 先把「已取消」那行剔掉(14 行变 13 行)**,`group by` 再把剩下的归成 3 堆。
  **`having` 最后筛掉金额不到 200 的那堆。** **三行变两行,走掉的是「已付款 138.40」。**
  **顺序不能反:`where` 管行,`group by` 管堆,`having` 管堆的去留。**

  having 筛分组列也行,但通常该用 where
  SQL> SELECT status, COUNT(*) AS n FROM orders GROUP BY status HAVING status = '已完成'
  status     n
  ------------------
  已完成     8
  (1 行)

  **能给对结果,但白分组了**——先把 14 行归成 4 堆再扔掉 3 堆,
  **不如 `where status = '已完成'` 先筛成 8 行再一次归堆。**
  **规矩:筛「行」用 `where`,筛「组的计算结果」用 `having`。**
  **能写进 `where` 的都写进 `where`。**

第一段 4 堆。 8、3、2、1。 第二段筛成 2 堆。 having n > 2 只留已完成和已发货。 第三段换成 having count(*) > 2。 结果一模一样,但输出顺序反了——having 里没了别名, 优化器走了另一条路。 第四段报错。 misuse of aggregate: COUNT(),说明 where 执行时聚合还没算。 第五段两个筛子。 3 行变 2 堆,走掉的是已付款 138.40。 第六段 having status = '已完成'。 给 1 行,结果对,但白分了 4 堆。

30 秒实验

  1. 把 having n > 2 改成 having n >= 2,会出几行?
  2. HAVING COUNT(*) > 2 与 WHERE COUNT(*) > 2 差别在哪一步?
  3. having money > 200 里的 200 换成 150,结果会多几行?

常见误区

  1. 把聚合条件写进 where。 报 misuse of aggregate,而且报错位置在 where 上, 容易以为是写错了函数名。
  2. 该用 where 的用成 having。 结果正确,但分组和排序白做了一遍,数据量大时能差出几倍时间。
  3. 依赖 having 用别名写跨引擎代码。 MySQL 认,标准 SQL 不保证,换个库就报 Unknown column。
自测三问(点开对答案) 1. `where` 和 `having` 的分界线是什么? —— 条件里有没有聚合函数。有就是 `having`,没有就用 `where`。 2. 为什么 `where count(*) > 2` 一定报错? —— `where` 在分组之前执行,那时还没有「堆」可以数,聚合无从算起。 3. 两个一起用时谁先谁后?为什么? —— `where` 先、`having` 后。先剔掉不要的行,参与分组的行就少,分组和排序都省。

到这里 select/where/group by/having/order by/limit 都见过了。 下一篇把七个位置一次排清楚——书写顺序和执行顺序还不是一回事。

相关笔记