分组
分组概念
人类认识世界,很多情况下是通过分类的方式来区别加以管理。在数据库中,分类表现为分组的方式。分组运算的实质,即将一个集合按某种规则拆分成若干子集。返回值应当是一个由集合构成的集合。用泛型来表达 List<List<T>>
分组是针对集合的分组,那么就会有分组键的选择,分组的划分方式,分组后的处理这几个关键步骤。
分组键
分组是针对集合的分组,那么分组可以采用字段,或字段计算的表达式,设置是与字段无关的数组或条件进行分组。用于分组的字段或表达式,称为分组键,所以分组键可以是字段,也可以是计算的表达式。
分组划分方式
等值分组
将分组键相等的成员分到一个组,这种分组称为等值分组。等值分组要求原集合的所有成员都在且只在唯一的组中;满足等值划分的数学上称为完全划分。
不等值分组
比如分组中可能有空集存在,也可能有些原集合的成员不在任何一个组中,或者在多个组中。这种被称为不完全划分。
分组后集合的处理
分组之后,可能针对分组后的集合进行过滤,或者针对分组的子集进行处理。
分组的方式
分组运算的本意是将一个大集合按某种规则拆成若干个子集合,关系代数中没有数据类型能够表示集合的集合,于是强迫在分组后做聚合运算。
离散数据集中允许集合的集合,可以表示合理的分组运算结果,分组和分组后的聚合被拆分成相互独立的两步运算,这样可以针对分组子集再进行更复杂的运算。
关系代数中只有一种等值分组,即按分组键值划分集合,等值分组是个完全划分。
离散数据集认为任何拆分大集合的方法都是分组运算,除了常规的等值分组外,还提供了与有序性结合的有序分组,以及可能得到不完全划分结果的对位分组。)
把集合中具有相同属性的成员分配到同一个组,这就是分组运算
以集合中的某一列计算条件来进行划分整个集合。划分的方式可以非常灵活,下面就举例说明划分的多种方式
以选择列中不同元素的来进行划分整个集合
划分的时候,默认将该列中的元素按照是否相同进行合并分组。如果元素不一样,就产生一个新组。这种划分方式与sql的groupby实现方式一致,这个最常用,最简单。
以选择列中元素变化来划分整个集合
依次扫描整个序列,当分组键值和上一个成员的分组键相同时,则将该成员加入到当前的分组子集,如果分组键值发生变化了,则产生一个新的分组子集并加入当前成员,扫描完之后就得到一批分组子集,从而完成分组运算。
工程实现的时候,group@o
以选择列中元素参与计算表达式来划分整个集合
//按出生年份分组;对birthday这一列,采用year函数计算之后进行划分
g1=employee.group(year(birthday))
以选择列中元素参与计算表达式为真来划分整个集合
这时候分组方式是个逻辑表达式,每当计算出 true 时则产生一个新的分组子集。
@o 还有一个变种 @i,这时候分组键时是个逻辑表达式,每当计算出 true 时则产生一个新的分组子集。
我们回顾一下之前做过的计算股票最长连续上涨天数的问题。可以换一种思路,将交易数据按日期排序(经常本来就是有序的),然后依次扫描这些数据进行分组,如果某天价格比前一天上涨了,则将这一天继续和前一天分到同一组;如果没上涨,则产生一个新的分组。等扫描完之后会得到一些分组子集,在每个子集中股价都是连续上涨的,然后我们只要看哪个成员最多的子集就行了。
外部输入的序列对选择列中元素进行划分。
先罗列出一个基准集合,然后将待分组集合成员的某个键值与基准集合成员比较,相同者则分到一个子集中,最后拆分出来的子集数量和基准集合成员数是相同的。这种分组在SPL中称为对位分组
如我们要统计男女员工数量
SELECT gender,COUNT(*) FROM employee GROUP BY gender
希望返回的结果集有2条记录,并保证顺序。但如果公司员工全是男性或女性,这个运算结果就只有一行了,那可能就不是我们想要的了
SPL 中提供了对位分组运算,用来统计男女员工数量可以写成这样:
a=[Male,Female] // 基准集合
g=employee.align(a,gender) // 函数align实现对位分组,拆分集合
g.new(a(#),~.len()) // 用分组子集计算汇总
这种对位分组在日常统计中是很常见的,比如按地区、按部门统计,都可以事先把基准集合列出来,而且我们经常还要求结果集必须按基准集合的次序出现,等值分组就不能保证这个次序,还要再排序,而排序时还是要提供这个基准集合,因为原集合成员属性中没有这个信息。
对位分组可能出现空子集,它也不能保证任何原集合的成员都被拆到某个子集中(比如有些不重要的成员没有被列入基准集合),不过对位分组能保证每个成员最多只出现在一个子集中。
外部输入的表达式对选择列中元素进行划分
把对位分组推广成更一般的枚举分组。
枚举分组是指,事先指定一组条件,将待分组集合的成员作为参数计算这批条件,条件成立者都被划分到与该条件对应的一个子集中,分组结果中的子集和事先指定的条件一一对应。
比如,将员工按年龄段分组统计人数,SPL 中可以这样写:
a=[?<=30,?<=40,?>40] // 用?表示要代入的参数
g=employee.enum(a,age) // 设计函数enum实现枚举分组,拆分集合
....
显然,枚举分组在日常业务中也是不少见的。
枚举分组和对位分组很象,都需要先列出一个基准集合,事实上,对位分组就是一种特殊的枚举分组。不过,不同的是,枚举分组可能制造出有重复成员的子集,也就是可重分组。
a=[?<=30,?>20 && ?<=40,?>50] // 条件有重叠
g=employee.enum@r(a,age)
可重分组在实际业务中相对罕见一些,不过了解一下也有助于再次理解分组运算的实质。
表面上看,对位分组和枚举分组和 SQL 的 GROUP BY 差别很大,但理解了分组运算的本质后,就会明白它们其实是一回事:把某个集合拆分成若干子集。只是拆分的方法各有不同。
分组键为表达式,age(BIRTHDAY)
分组方式采用对位的方式
从这个可以看出enum与Group的分组方式不同点
Group,是针对本表中的某列或对某列使用表达式来,针对默认的本列的计算值进行比较计算分组条件
Enum,是针对本表中的某列或对某列使用表达式,然后在对应其他序列进行比较分组条件
分组后运算
分组运算的实质是将一个集合按照某种规则拆分成若干个子集,也就是说,返回值应当是一个由集合构成的集合。对分拆的结果,需要进行进一步计算。在Java中,分组会出现Key,value的结构。在SPL中,分组之后是List<List<T>>的结构。即集合分组之后,也是小的集合,不是Key,Value的结构。
对分组进行过滤
例1 根据分组键进行过滤
根据子集中的特性进行过滤,类似sql中having
例2: 根据子集的条件来筛选分组
想找出公司里有哪些员工和其他员工会在同一天过生日,很简单的思路是将员工按生日分组,然后找出成员数大于 1 的分组子集,再做个并集。这时候就不是只对聚合值(分组子集的成员数)感兴趣,而是对分组子集本身进行合并计算。
SQL的表达比较啰嗦,需要用子查询,并且要遍历两次原集合
SELECT * FROM employee WHERE birthday IN
( SELECT birthday FROM employee GROUP BY birthday HAVING COUNT(*)>1 )
采用spl的表达式如下
=T("Employee.csv")
=A1.group(BIRTHDAY)
=A2.select(~.len()>1).conj()
//SPL code from SPL:获取分组子集后再统计 - 乾学院
A1:导入员工表。
A2:使用了函数 A.group() 按出生日期分组。
A3:选择成员数量大于 1 的分组,即有相同出生日期的子集。再将这些子集合并。
严格来说,分组和汇总是两个独立的动作,但在 SQL 中总是一起出现,从而给人一种两者必须同时使用的假象。事实上,这种组合是对分组操作的一种局限,或者说分组之后,能够进行的计算远不止 SQL 中的几种聚合函数。
分组之后,以分组子集的统计值来筛选分组
注:A1.group的含义是,针对A1的集合进行group操作
对子集进行聚合计算
类似sql中group by 前的select 的sum/count等5大计算
在SQL中,有 GROUP BY 子句时,SELECT 部分除了分组字段外,就只能写入聚合运算表达式了。聚合表达式有5个,sum,count,max,min,avg
由于 SQL 的离散性问题,无法返回集合的集合,只能强迫实施聚合运算了,在group的时候,必须要同时返回聚合值。
有时候,需要对这些分组子集而不是聚合值更感兴趣。在需要采用Java等高级语言来进行处理。
标准 SQL 中提供了五种最常用的聚合运算:SUM/COUNT/AVG/MIN/MAX。观察这几个运算,我们发现它们都可以看成是一个以集合为参数返回单值的函数,我们就先把这个共同点理解为聚合运算的定义,把集合变成单值,多个值变成一个值,也就是发生了 " 聚合“,所以叫聚合运算。
在SPL中的处理方式
对子集进行其他计算
有时候我们关心的不是结果数值本身,而是与结果数值相关的信息。
例一:从日志表中找出某个用户第一次登录时用的 IP 地址,而不是登录时刻。
标准 SQL 写这个运算大概是这样:
SELECT ip_address FROM LogTable WHERE user=? AND logintime=
(SELECT MIN(logintime) FROM LogTable WHERE user=?)
用子查询先计算出该用户的第一次登录的时刻,再查找出该时刻时用到的 IP 地址,这要把数据集遍历两次。
采用spl之后
LogTable.group(user).(~.minp(logintime))
在SQL中无法处理,因为分组与计算是关联在一起。
例二:根据分组子集的结果来进一步处理分组子集
这个时候,分组不变
如top,或者满足条件的count
=LogTable.groups(user;(a=~.top(logintime,-2),a(2)-a(1))) //每个用户最后的两次登录时间间隔
对分组子集的动作,可以分为sql的5种聚合,sql以外的其他聚合,也可以是非聚合的方式(如选择子集的部分内容)。总之是对子集进行的动作。
【例 2】查询年龄低于部门平均年龄的员工。
前面已经介绍过,在 SPL 中函数 A.group()用于分组。我们可以在函数 A.group() 中,定义在分组后对每个分组子集的运算。不限于 SUM、COUNT 等聚合运算,可以定义一些复杂运算。
SPL脚本如下:
=T("Employee.csv")
=A1.group(DEPT; (a=~.avg(age(BIRTHDAY)), ~.select(age(BIRTHDAY)<a)):YOUNG)
=A2.conj(YOUNG)
//SPL code from SPL:获取分组子集后再统计 - 乾学院
A1:导入员工表。
A2:按部门分组,并在每个分组中选出年龄低于平均年龄的记录。在函数 A.group() 的聚合运算中,我们可以使用临时变量,使得运算更加简单易懂。
A3:将选出的记录合并。
例三将该数据集按照任务编号进行分组,然后每个子集中选择version最大的记录
分组之后的合并.conj();找一个例子
Group 之后获取子集,然后进行进一步计算
合并的时候,一定要采用new的方式,获取到所有的成员,参与后续的合并。
采用conj,可以得到一个正常的表。但是它还是一个key,value
然后进行和合并的时候,就是只有一个成员。
分组计算中的符号
每个函数有它作用的对象。A 是个集合,A.select 当然只会过滤 A(的成员),它就不知道 A 的成员是不是集合。
要过滤 A 的成员子集,那就对着这些子集去做过滤。A.(~.select(…) ),这才是明确知道 A 的成员是集合,并再使用过滤动作。
特别注意:A.select 与A.(~.select(..))
1、对分组后的子集进行过滤。以这个过滤条件来筛选 分组。
实现方式,采用分组后的,使用 A2.select()或A2.count(),类似sql中的having。
2、对分组后的子集进行过滤,以这个过滤条件 仅仅筛选 分组子集,但是分组不变。
A2。
比如:在一个雇员的表中,group 城市。然后针对每个城市中的人员根据 select
注:
去重可以用A.group@1 (x1,…),如果是按所有字段去重则可以写成A.group@1 (~.array())。
top(-2) 如果本组只有 1 条则结果就只有一条。