MariaDB [test]> select * from Student;
+------+----------+------+--------+
| sid | name | age | class |
+------+----------+------+--------+
| 1 | chenyi | 22 | Java |
| 2 | huanger | 23 | Python |
| 3 | zhangsan | 21 | Java |
| 4 | lisi | 20 | C# |
| 5 | wangwu | 21 | Python |
| 6 | zhaoliu | 19 | Java |
| 7 | qianqi | 22 | C |
| 8 | sunba | 20 | C++ |
| 9 | yangjiu | 24 | Java |
+------+----------+------+--------+
现在按照class进行分组。下面是分组后经过我加工的表结构:
其中第一列是分组得到的结果,我把它和原表的数据结合在一起了。注意,这是一个不符合关系模型范式要求的结构。
在分组前,关系引擎会对sid、name、age和class列的每一行进行筛选。但是分组后,关系引擎只看得到第一列,也就是class列,而sid、name和age列被直接忽略,因此无法引用它们。
关于GROUP BY,有以下两个问题:
1.为什么分组之后涉及到对组的操作时只允许返回标量值?
标量值即单个值,比如聚合函数返回的值就是标量值。在分组之后,组将成为表的工作中心,一个组将成为一个整体,所有涉及到分组的查询,将以组作为操作对象。组的整体是重要的,组中的个体不重要,甚至可以理解为分组后只有组的整体,即上图中左边加粗的部分,而组中的个体是透明的。
以上图中的第一条记录举一个通俗的例子。在分组以前,知道了该学生的姓名"chenyi"之后,关注点可能要转化为它的主键列sid值"1",因为主键列唯一标识每一行,知道了主键值就知道了该行的所有信息。而在分组之后,关注的中心只有分组列class,无论是知道姓名"chenyi"还是学号"1"都不是关注的重点,重点是该行记录(集合的元素)是属于"Java"班级的。
这就能解释为什么只能以组作为操作对象并返回标量值。例如,在分组之后进行SUM汇总,将以"Java"班作为一个汇总对象,以"Python"班作为另一个汇总对象,汇总的将是每个分组的总值,而不是整个表的总值,并且汇总的值是一个标量值,不会为组中的每行都返回这个汇总值。否则就违反了关系模型的范式。
2.为什么分组之后只能使用GROUP BY列表中的列,如果不在GROUP BY列表中,就必须进行聚合?
分组后分组列成为表的工作中心,以后的操作都必须只能为组这个整体返回一个标量值。
如果使用了非分组列表的列,将不能保证这个标量值。例如,分组后对"Java"班返回了一个汇总值,假如同时要使用sid列和name列,因为这两列没有被聚合或分组,因此只能为这两列的每个值返回一行,也就是说在返回汇总标量值的同时还要求返回"Java"班组中的每一行,要实现这样的结果,需要整合为如上图所示的结果,但在关系表中这是违反规范的。正如前文介绍的DISTINCT一样,ORDER BY的排序列只能使用DISTINCT去重的select_list列表。
因此,分组后只能使用分组列表中的列。如果要使用非分组列表中的列,应该让它们也返回一个标量值,只有这样才能实现分组列和非分组列结果的整合。例如,下面的语句将会产生错误,因为select_list在GROUP BY阶段后执行,且select_list中的列没有包含在GROUP BY中,也没有使用聚合函数。
SELECT sid,name FROM Student GROUP BY class;
事实上从严格意义上看待这条语句,它没有实现分组的意义:既然不返回分组列的分组结果,那为什么还要进行分组呢?
其实,无论是标准SQL还是MySQL、mariadb,执行group by子句时都会表扫描并创建一个临时表(此处为了说明group by的特性,不考虑group by使用索引优化的情况),这个临时表中只有group by的分组列,没有那些非分组列。这也是前面说group by之后,关系引擎的目光从行转为组的真正原因。由此,已经足够说明为什么select_list中不能使用非group by的分组列。
1.10 关于MySQL/MariaDB的GROUP BY