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 秒实验
GROUP BY class_id换成GROUP BY gender,能出几行?- 把
group_concat(name, ' / ')的分隔符换成,再换成空串,结果长什么样? SELECT status, COUNT(*) FROM orders GROUP BY status与SELECT status FROM orders各出几行? 差在哪?
常见误区
- 以为
group by之后select里能随便写列。 没分组的列给的是「随便一行」的值,结果不可靠, MySQL 8 还会直接报错。 - 以为分组会把没数据的分组补成 0。 组合里没人的那些压根不出行,要补 0 得自己补。
- 用
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。
- ☐ (本篇完,下一篇 4.4 指定条件:having)