最近接触了报表业务,里面有各种分组、求和操作。整理大脑中对分组的知识碎片,并查阅相关资料,在这里对group 做一个知识的梳理。
假设有一张表articles, 有 id(主键)、author_id(作者)、type_id(类型)、created_at等字段。
select author_id, count(id) from articles group by author_id;
上面就是一个简单的分组操作,它会输出所有的author_id(不会重复)以及每个author_id拥有的文章数量。类似下面:
author_id count
13455 10
13456 5
可以看出分组是一个聚合操作,按照group by 后面的字段进行聚合,然后可以对聚合后的集合进行分别操作,类似上面的求和。 执行的内容大概是: 先按照author_id 来进行聚合,表中有多少个唯一的author_id就分为多少个组, 不同的列如果author_id相同就归为同一组,不然则为另一组。然后可以对这些组执行一些操作:
select author_id, max(created_at) from articles group by author_id;
求出每个组(每个author)中最大的的创建时间。这样可以查询每个author最近一次发表文章是什么时候。
除了聚合之外,执行分组操作的字段在输出时值是唯一的,不会重复。所以分组大概可以达到三个结果:
主要有这几种方式:1. 临时表 2.索引
先来看看临时表方式,在上面的例子中,MySQL会创建一个临时的中间表,然后遍历全表进行分组操作,每一个分组都会在临时表中先保存一条对应的记录,在处理剩下的记录时,会把对应的行信息更新到临时表相应的记录中,如count计数,这可能会产生大量的磁盘IO。 分组完成后默认会将临时表的数据按照分组字段进行filesort,保证按照分组字段有序输出。
很显然,这种操作,在数据量较大的情况下,性能是非常低下的,除了全表扫描,还有临时表的大量磁盘IO,以及filesort。
当然,使用索引是更好的方案。
在很多情况下,如果分组字段上有索引,是可以避免创建临时表的。现在假设在author_id上创建了索引,分组的情况会有以下改变:
如果,获取的所有数据都可以从覆盖索引中获取的话,性能会有极大的提升,否则,会通过索引回表查询相应的数据,这样也会造成大量的随机IO,和临时表相比值少了临时表数据更新和filesort的消耗,性能还是不够乐观。大多数情况下,分组操作中同时有索引和临时表参与。
在有良好的索引的情况下,不会创建临时表,也较好的性能体验,这里面也主要分为两种:
这里只讨论一下这两者的区别:
根据官方文档,松散索引定义:
This property enables use of lookup groups in an index without having to consider all keys in the index that satisfy all WHERE
conditions. This access method considers only a fraction of the keys in an index, so it is called a loose index scan.
紧凑索引扫描:
A tight index scan may be either a full index scan or a range index scan, depending on the query conditions.
主要的差别在于,前者只扫描了部分少量的索引就完成了分组操作,后者则进行了全索引扫描或范围扫描。或者说,前者中分组操作和范围预测是同时进行的,后者是先进行范围扫描后,再进行分组。这个究竟是个什么意思,在这里我们稍微讨论一下。
紧凑扫描的例子:(有索引 idx(c1,c2,c3),下同)
select c1,c2,c3 from t1 where c1 = ‘a’ group by c2,c3
这里需要先对组合索引进行范围扫描,找到所有的以'a'开头的集合,也就是会扫描以'a'开头的所有索引中的每一个值,利用索引的有序性进行有序扫描,从而实现分组的操作。
松散索引扫描的例子:
select c1,c2 from t1 group by c1,c2;
这里面,索引扫描的个数和分组的数量一样,也就是说,索引扫描次数 = c1,c2组合唯一的数量, 当有多个c1,c2时,比如有这样的组合: 1,2,3 1,2,4 1,2,7 (值的顺序和索引顺序一致,c1,c2,c3) ,有多个c1,c2 的重复,松散模式只读取第一个,不会管后面的重复值,直接跳到去读取下一组不同的c1,c2的值。 也就是说整个过程只扫描了少量的索引,没有进行全索引或范围扫描。
当然,可以看出,松散模式性能更好。 为了更加深入理解,我们假设一种情况,在松散模式下,c1和c2的组合值是唯一的,也就是没有像上面那样重复,那么,以我的理解,它整个过程也相当于扫描了整个索引(不管c3列)。当然如果是这样,也就不用分组了。
MySQL中常用的distinct也能实现部分group by 操作,这两者之间其实是非常类似的。distinct的实现原理与分组几乎一致,也可以使用松散/紧凑索引进行扫描,在不能只依靠索引的时候,也会像分组那样建立临时表。
差别在于,在用临时表的时候,distinct不用排序,也就是不会用filesort。