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

group by 按某一列归堆,每堆出一行。 这是把「明细」变成「汇总表」的那一步。 Demo 里 14 张订单按状态归成 4 堆:已完成 8、已发货 3、已付款 2、已取消 1; group by class_id, gender 按组合归成 6 堆。 group_concat(列, 分隔符) 把一堆值拼成一个字符串; 混着没分组的列选,这里给「随便一行」,MySQL 8 直接报错。

📝 本章和前面几章的关系

上一篇的聚合是「整表一个数」,这一篇把它拆成「每组一个数」。 这是全书最常用的一个子句:日报、月报、分类统计、Top N,全都要先 group by 再算。

一、group by 在干什么

一句话:按某个(或某几个)列的值把行分堆,堆内做聚合,一堆出一行。 group by class_id 就是「同一个班级的行放一起」。 三个班各 4 人,所以三行都是 4。 换个分组列,结果的信息量完全不同。 换成 status 之后,14 张订单归成 8、3、2、 1——这种分布才是业务上想看的东西。

二、分组之后,聚合的边界变了

这是 group by 最核心的一句:聚合函数不再算全表,只算「当前这一堆」。 select status, count(*), sum(amount) ... group by status 里, count(*) 是这堆的行数,sum(amount) 是这堆的金额合计。 所以「分组 + 聚合」是一套组合拳:分组定边界,聚合在边界内算数。 少了分组,聚合就是全表一个数;少了聚合,分组就没意义。

三、多列分组:按组合归堆

group by class_id, gender 的堆是「班级 + 性别」的组合, 不是一个列一个列地分。 3 个班乘 2 种性别,最多 6 堆。 组合里没有人的那些不出现——不是给一行 0,是压根没这一行。 做报表时这一条很重要:缺的组要由程序补 0,SQL 不会替你补。 列的顺序有影响吗:对结果集来说只影响输出顺序, 但写成 (class_id, gender) 这种顺序,能顺着索引走,快很多。

四、group_concat:把一堆值拼成一个字符串

group_concat(name, ' / ') 把这一堆里的名字串起来,默认分隔符是逗号,第二个参数可以换。 它解决的是「一对多怎么塞进一行」:一个班里有谁、一个订单里有哪些商品, 用聚合拼成一列比查两次方便。 但有个坑:MySQL 的 group_concat_max_len 默认 1024 字节, 超了直接截断,不报错。 要拼长文本得先 SET 这个变量。

五、最常踩的一脚:select 里出现没分组的列

select class_id, name ... group by class_id 能跑,但 name 是「这一堆里随便挑的一个」。 这次挑到张伟、李娜、陈静,下次换个索引可能就换人了。 结果不稳定,是这个写法最坏的地方—— 它不是错,是不可靠。 MySQL 8 默认拒绝这种写法(ONLY_FULL_GROUP_BY), 报错说 name 不在 GROUP BY 里。 这条报错是在救你,别去关它。 真需要「每组里的某一行」,用窗口函数或子查询,别指望 group by 顺手帮你挑。

六、动手 Demo:把 14 张订单归成 4 堆

Demo 六段:先按班级归堆看基本形状,再换成订单状态看真实分布,然后分组求和、多列分组、拼字符串。 最后一段故意写错一次,看「随便挑一行」是怎么发生的。

import _q46 as Q
c = Q.con()

# 一类一行:三个班各 4 人
c.execute("SELECT class_id, COUNT(*) AS n FROM students GROUP BY class_id")
# 换个分组列:订单状态 8 / 3 / 2 / 1
c.execute("SELECT status, COUNT(*) AS n FROM orders GROUP BY status")
# 分组之后照样能算:每堆内部求和
c.execute("SELECT status, COUNT(*) AS n, SUM(amount) AS money "
          "FROM orders GROUP BY status ORDER BY money DESC")
# 多列分组:按组合归堆
c.execute("SELECT class_id, gender, COUNT(*) AS n FROM students "
          "GROUP BY class_id, gender")
# group_concat:把一堆值拼成字符串
c.execute("SELECT class_id, COUNT(*) AS n, "
          "group_concat(name, ' / ') AS names "
          "FROM students GROUP BY class_id")
# 混着没分组的列选:给「随便一行」
c.execute("SELECT class_id, name FROM students GROUP BY class_id")

实跑输出:

--------------------------------------------------------------------
第十七课:group by,把行按某个标准归堆
--------------------------------------------------------------------

  最基本的用法:一类一行
  SQL> SELECT class_id, COUNT(*) AS n FROM students GROUP BY class_id
  class_id     n
  ----------------------
  1            4
  2            4
  3            4
  (3 行)

  **`group by class_id` 的意思是「按班级归堆,每堆出一行」。** **三个班各 4 人,所以三行都是 4。**

  换个分组列,分布就有意思了
  SQL> SELECT status, COUNT(*) AS n FROM orders GROUP BY status
  status     n
  ------------------
  已付款     2
  已发货     3
  已取消     1
  已完成     8
  (4 行)

  **14 张订单按状态归成 4 堆:已完成 8、已发货 3、已付款 2、已取消 1。**
  **分组的意义就在这——它把「一堆明细」变成「一张汇总表」。**

  分组之后照样能算:求和、平均
  SQL> SELECT status, COUNT(*) AS n, SUM(amount) AS money FROM orders GROUP BY status
       ORDER BY money DESC
  status     n     money
  -------------------------
  已完成     8     1262.20
  已发货     3     412.50
  已取消     1     156.00
  已付款     2     138.40
  (4 行)

  **聚合函数在分组之后,算的是「每一堆内部」的数,不再是全表。**
  **这是 group by 最核心的一句:聚合的边界被分组改了。**

  多列分组:按组合归堆
  SQL> SELECT class_id, gender, COUNT(*) AS n FROM students GROUP BY class_id, gender
  class_id     gender     n
  ---------------------------------
  1            女         1
  1            男         3
  2            女         3
  2            男         1
  3            女         2
  3            男         2
  (6 行)

  **3 个班乘 2 种性别,最多 6 堆,这次 6 堆都有人。**
  **组合里有空的那些不出现——不是给 0 行,是压根没这一行。**

  group_concat:把一堆值拼成一个字符串
  SQL> SELECT class_id, COUNT(*) AS n, group_concat(name, ' / ') AS names FROM students
       GROUP BY class_id
  class_id     n     names
  ---------------------------------------------
  1            4     张伟 / 王芳 / 杨帆 / 周涛
  2            4     李娜 / 刘洋 / 赵敏 / 吴倩
  3            4     陈静 / 黄磊 / 徐明 / 孙悦
  (3 行)

  **`group_concat(列, 分隔符)` 把这一堆里的值串起来,默认分隔符是逗号。**
  **做「这个班里都有谁」这种展示时特别顺手。**
  **它有个默认长度上限**(MySQL 的 `group_concat_max_len` 默认 1024 字节),
  **超了会被截断,而且不报错。**

  最常踩的一脚:混着没分组的列选
  SQL> SELECT class_id, name FROM students GROUP BY class_id
  class_id     name
  ----------------------
  1            张伟
  2            李娜
  3            陈静
  (3 行)

  **每班只出了一行,`name` 是「随便挑的某一个」。**
  **这次挑到张伟、李娜、陈静,下次换个索引可能就变了。**
  **MySQL 8 默认拒绝这种写法**(`ONLY_FULL_GROUP_BY`),
  **报错信息里会说 `name` 不在 `GROUP BY` 里。** **这条报错是在救你,别去关它。**

第一段三个班。 每班 4 人,三行都是 4——整齐,但没什么信息量。 第二段订单状态。 8、3、2、1,这才叫分布。 第三段分组求和。 已完成 1262.20 占了大头,聚合的边界被 group by 改成了「每堆」。 第四段多列分组。 6 行,班级与性别的组合。 第五段 group_concat。 一行里装下班里四个人,用 / 隔开。 第六段混列。 给了三行「1 张伟 / 2 李娜 / 3 陈静」, 这个 name 不是「班长的名字」,是「碰巧排第一的那个」。

30 秒实验

  1. GROUP BY class_id 换成 GROUP BY gender,能出几行?
  2. 把 group_concat(name, ' / ') 的分隔符换成 , 再换成空串,结果长什么样?
  3. SELECT status, COUNT(*) FROM orders GROUP BY status 与 SELECT status FROM orders 各出几行? 差在哪?

常见误区

  1. 以为 group by 之后 select 里能随便写列。 没分组的列给的是「随便一行」的值,结果不可靠, MySQL 8 还会直接报错。
  2. 以为分组会把没数据的分组补成 0。 组合里没人的那些压根不出行,要补 0 得自己补。
  3. 用 group_concat 拼长文本不看长度上限。 超过 group_concat_max_len 会被静默截断, 报表上少一截都没人发现。
自测三问(点开对答案) 1. `group by` 和聚合函数是什么关系? —— `group by` 定边界(把行分堆),聚合在边界内算数(每堆出一个值)。没有分组时聚合算整表。 2. `GROUP BY class_id, gender` 分出来的是什么? —— 班级与性别的组合,最多 6 堆;组合里没有人的不会出现。 3. 为什么 `select` 里不该出现没分组的列? —— 它给的是堆里随便一行的值,结果不稳定;MySQL 8 默认会直接报错拒绝。

分组能让每堆出一行, 但「只留行数过 3 的堆」这种条件,where 做不到。 下一篇讲专门筛组的 having。

相关笔记