SQL 和 SPL 的等值分组对比

【摘要】
    把集合中具有相同属性的成员分配到同一个组,这就是分组运算。比如员工表根据部门分组,每组的员工都具有相同的部门;销售表可以根据销售年份分组,每组都是同一个年份的销售记录等等。SQL 和 SPL 是大家比较熟悉的程序语言,本文将探讨对于分组问题,这两种语言的解决方案和基本原理。如何简便快捷的处理分组运算,这里为你全程解析,并提供 SQL 和 SPL 示例代码。SQL 和 SPL 的等值分组对比

一. 分组汇总

有时候我们需要将数据按不同类型进行统计,这就要用到分组汇总。分组汇总是指根据一定的规则将数据进行分组,然后针对每个分组进行聚合运算。

 

统计每位学生的总分:

分组

汇总

..

使用过 SQL 或 SPL 的朋友对分组汇总都不会陌生。在 SQL 中,GROUP BY 语句可以用来分组汇总。GROUPY BY 经常和聚合函数 SUM、COUNT 等一起出现,用来将查询结果按照某些字段进行归类分组,并汇总统计。SPL 则提供了函数 groups() 用于分组汇总。

 

【例 1】 根据销售表,查询 2014 年每个月的总销售额。部分数据如下:

ID

CUSTOMERID

ORDERDATE

SELLERID

PRODUCTID

AMOUNT

10400

EASTC

2014/01/01

1

27

3063.0

10401

HANAR

2014/01/01

1

17

3868.6

10402

ERNSH

2014/01/02

8

70

2713.5

10403

ERNSH

2014/01/03

4

42

1005.9

10404

MAGAA

2014/01/03

2

74

1675.0

 

 

SQL的解决方案:

    select

      extract (month from ORDERDATE) MONTH, sum(AMOUNT) AMOUNT

   from

      SALES

   where

      extract (year from ORDERDATE)=2014

   group by 

      extract (month from ORDERDATE)

   order by

      MONTH

 

    在 SQL 语句中,GROUP BY 语句用于根据 BY 语句的分组字段进行分组汇总。GROUP BY 语句中 SELECT 指定的字段必须是分组字段,其他字段若想出现在 SELECT 中则必须包含在聚合函数中。可以使用 WHERE 语句,在分组汇总前对数据进行过滤。

另外,这里是以 ORACLE 为例,其他数据库计算年份和月份时,可以使用函数 YEAR 和 MONTH。

 

SPL的解决方案:

    在 SPL 中提供了函数 A.groups() 用于分组汇总。


A

1

=T("Sales.csv").select(year(ORDERDATE)==2014)

2

=A1.groups(month(ORDERDATE):MONTH; sum(AMOUNT):AMOUNT)

A1:从文件中导入销售表,并选出 2014 年的记录。

A2:使用函数 A.groups() 按月份分组汇总每个月的总销售额。

 

SPL同样也支持从数据库中读取数据表,比如A1可以改为:


A

1

=connect("db").query("select * from SALES where extract (year from ORDERDATE)=2014")

   

 

【例 2】 查询在纽约州平均年龄低于 40 岁的部门。部分数据如下:

ID

NAME

BIRTHDAY

STATE

DEPT

SALARY

1

Rebecca

1974/11/20

California

R&D

7000

2

Ashley

1980/07/19

New   York

Finance

11000

3

Rachel

1970/12/17

New   Mexico

Sales

9000

4

Emily

1985/03/07

Texas

HR

7000

5

Ashley

1975/05/13

Texas

R&D

16000

 

SQL的解决方案:

    select

      DEPT, avg(TRUNC(months_between(sysdate, BIRTHDAY)/12)) AVGAGE

   from

      EMPLOYEE

   where

      STATE='New York'

   group by

      DEPT

   having

      avg(TRUNC(months_between(sysdate, BIRTHDAY)/12))<40

 

    在 SQL 语句中,分组汇总前的选出需要使用 WHERE 语句,分组汇总后的选出需要使用 HAVING 语句。

 

SPL的解决方案:


A

1

=T("Employee.csv").select(STATE=="New York")

2

=A1.groups(DEPT; avg(age(BIRTHDAY)):AVGAGE)

3

=A2.select(AVGAGE<40)

A1:导入员工表,并选出纽约州的记录。

A2:使用函数 A.groups() 按部门分组统计平均年龄。

A3:在分组汇总后的结果集中选出平均年龄小于 40 的记录。

   

SPL中无论在分组前后,都可以使用函数 A.select()进行选出。 SPL 提供了函数 age(),可以直接根据员工出生日期计算出年龄。

 

【例 3】 根据销售表,统计 2014 年客户排进每月单笔销售额前三名的次数。部分数据如下:

ID

CUSTOMERID

ORDERDATE

SELLERID

PRODUCTID

AMOUNT

10400

EASTC

2014/01/01

1

27

3063.0

10401

HANAR

2014/01/01

1

17

3868.6

10402

ERNSH

2014/01/02

8

70

2713.5

10403

ERNSH

2014/01/03

4

42

1005.9

10404

MAGAA

2014/01/03

2

74

1675.0

 

SQL的解决方案:

    select

      CUSTOMERID, count(*) COUNT

   from

      (select

         CUSTOMERID,

         ROW_NUMBER()OVER(PARTITION BY extract (month from ORDERDATE) ORDER BY AMOUNT DESC) MONTH_RANK

      from SALES

      where extract (year from ORDERDATE)=2014)

   where MONTH_RANK<=3

   group by CUSTOMERID

   order by CUSTOMERID

 

    在分组时计算排名是很常用的需求,但是 SQL 的 GROUP BY 语句并不支持排名函数。所以我们需要使用 ROW_NUMBER,RANK,DENSE_RANK 等函数,进行分组后的排名计算,不能使用 GROUP BY 语句。

 

SPL的解决方案:


A

1

=T("Sales.csv").select(year(ORDERDATE)==2014)

2

=A1.groups(month(ORDERDATE):MONTH; top(-3;AMOUNT):TOP3)

3

=A2.conj(TOP3).groups(CUSTOMERID; count(~):COUNT)

A1:导入销售表,并选出 2014 年的记录。

A2:使用函数 A.groups() 按月份分组汇总前三名。

A3:将每月前三名的记录合并后,使用函数 A.groups() 按客户分组统计次数。

   

在 SPL 中,函数 A.groups() 不仅支持 sum,count,avg,max,min 等 SQL 支持的聚合函数,还支持一些比较常用,但是 SQL 的 GROUP BY 语句并不支持的聚合函数:top(取前 N 名 / 后 N 名)、iterate(迭代函数)、icount(唯一值计数)、median(中位数,部分数据库支持)等等。

 

【例 4】 统计每个班级各科的得分中位数和不及格(低于 60 分)人数。部分数据如下:

CLASS

STUDENTID

SUBJECT

SCORE

1

1

English

84

1

1

Math

77

1

1

PE

69

1

2

English

81

1

2

Math

80

 

SQL的解决方案:

    select

      t1.CLASS,t1.SUBJECT,t1.MEDIAN_SCORE,

      nvl(t2.FAIL_COUNT,0) FAIL_COUNT

   from

      (select CLASS, SUBJECT, median(SCORE) MEDIAN_SCORE

      from SCORES

      group by CLASS, SUBJECT

      order by CLASS, SUBJECT) t1

      left join

      (select CLASS, SUBJECT, count(*) FAIL_COUNT

      from SCORES

      where SCORE<60

      group by CLASS, SUBJECT) t2

   on t1.CLASS=t2.CLASS and t1.SUBJECT=t2.SUBJECT

 

    在 SQL 中统计不及格人数时,需要在 WHERE 语句中选出低于 60 分的记录,再通过函数 COUNT 来计数。这就导致计算不及格人数和计算得分的中位数不能同时进行。

另外这里还是以 ORACLE 为例,ORACLE 有函数 MEDIAN 可以用于计算中位数,但是有些数据库是并不支持计算中位数的。这种情况就有些麻烦了,我们可以利用排序后的行号来计算中位数。

 

SPL的解决方案:


A

1

=T("Scores.csv")

2

=A1.groups(CLASS,SUBJECT; median(,SCORE):MEDIAN_SCORE, count(SCORE<60):FAIL_COUNT)

A1:导入成绩表。

A2:使用函数 A.groups() 按班级和科目分组汇总中位数和不及格人数。

   

在 SPL 中,函数 count() 可以直接使用参数“SCORE<60”来统计不及格人数,不需要先选出不及格的记录,再进行计数。这样就可以很方便的同时计算中位数和统计人数了。

 

二. 分组子集

分组运算的实质是将一个集合按照某种规则拆分成若干个子集,也就是说,返回值应当是一个由集合构成的集合。对于每个成员集合,我们称为分组子集

严格来说,分组汇总是两个独立的动作,但在 SQL 中总是一起出现,从而给人一种两者必须同时使用的假象。事实上,这种组合是对分组操作的一种局限,或者说分组之后,能够进行的计算远不止 SQL 中的几种聚合函数。

在 SPL 中提供了函数 group(),我们可以用它来实现真正的分组,分组后的结果集是由多个分组子集组成的集合:

 

按学生分组:

分组

分组子集

..

   

    通常人们对分组子集的聚合值更感兴趣,因此分组运算常常伴随着对子集的进一步汇总计算。但是,我们仍然有对这些分组子集而不是聚合值更感兴趣的时候。

 

【例 5】 根据员工表,找出有相同出生日期的员工。部分数据如下:

ID

NAME

BIRTHDAY

STATE

DEPT

SALARY

1

Rebecca

1974/11/20

California

R&D

7000

2

Ashley

1980/07/19

New   York

Finance

11000

3

Rachel

1970/12/17

New Mexico

Sales

9000

4

Emily

1985/03/07

Texas

HR

7000

5

Ashley

1975/05/13

Texas

R&D

16000

 

SQL的解决方案:

    在 SQL 中并不支持真正的分组,无法保留分组子集。我们只能先按照出生日期分组统计每组的人数,选出人数大于 1 的生日。再从员工表重新选出记录,使用内连接与之前选出的生日来过滤。本来只需要一次分组就可以解决的问题,却又多了一次查询和连接过滤。SQL 语句如下:

   select *

   from EMPLOYEE t1

   inner join

   (select BIRTHDAY

   from EMPLOYEE

   group by BIRTHDAY

   having count(*)>1) t2

   on t1.BIRTHDAY=t2.BIRTHDAY

 

SPL的解决方案:

SPL提供了函数 A.group() 用于真正的分组。


A

1

=T("Employee.csv")

2

=A1.group(BIRTHDAY)

3

=A2.select(~.len()>1).conj()

A1:导入员工表。

A2:使用了函数 A.group() 按出生日期分组。

A3:选择分组子集的成员数量大于 1 的分组,即有相同出生日期的。再将分组子集合并。

 

    与 SQL 相比,SPL 的脚本十分简洁。这是因为 SPL 的分组,是真正的分组,每一个分组子集是具有相同出生日期的员工记录的集合。

   

【例 6】 根据员工表,查询年龄低于部门平均年龄的员工。部分数据如下:

ID

NAME

BIRTHDAY

STATE

DEPT

SALARY

1

Rebecca

1974/11/20

California

R&D

7000

2

Ashley

1980/07/19

New   York

Finance

11000

3

Rachel

1970/12/17

New   Mexico

Sales

9000

4

Emily

1985/03/07

Texas

HR

7000

5

Ashley

1975/05/13

Texas

R&D

16000

 

SQL的解决方案:

    使用 SQL 分组汇总时,只支持 SUM、COUNT、AVG、MAX、MIN 等聚合函数,除此之外不能进行其他复杂运算。我们只能先使用 GROUP BY 语句分组汇总每个部门的平均年龄,再查询员工表,通过内连接选出低于平均年龄的员工。

 

   select *

   from EMPLOYEE t1

   inner join

      (select

         DEPT, avg(TRUNC(months_between(sysdate, BIRTHDAY)/12)) AVG_AGE

      from EMPLOYEE

      group by DEPT) t2

   on t1.DEPT=t2.DEPT and

   TRUNC(months_between(sysdate, t1.BIRTHDAY)/12)<t2.AVG_AGE

 

SPL的解决方案:

前面已经介绍过,在 SPL 中函数 A.group()用于分组。我们可以在函数 A.group() 中,定义在分组后对每个分组子集的运算。不限于 SQL 支持的 SUM、COUNT 等聚合运算,可以定义一些复杂运算。


A

1

=T("Employee.csv")

2

=A1.group(DEPT; (a=~.avg(age(BIRTHDAY)), ~.select(age(BIRTHDAY)<a)):YOUNG)

3

=A2.conj(YOUNG)

A1:导入员工表。

A2:按部门分组,并在每个分组中选出年龄低于平均年龄的记录。在函数 A.group() 的聚合运算中,我们可以使用临时变量,使得运算更加简单易懂。

A3:将选出的记录合并。

 

【例 7】 根据员工表,选出员工人数超过 50 人的州,查询这些州各部门的平均工资。部分数据如下:

ID

NAME

BIRTHDAY

STATE

DEPT

SALARY

1

Rebecca

1974/11/20

California

R&D

7000

2

Ashley

1980/07/19

New   York

Finance

11000

3

Rachel

1970/12/17

New   Mexico

Sales

9000

4

Emily

1985/03/07

Texas

HR

7000

5

Ashley

1975/05/13

Texas

R&D

16000

 

SQL的解决方案:

    按照正常的逻辑,我们应该先按州进行分组,选出员工人数超过 50 的州。再将每个州的员工按部门进行分组汇总平均工资。但是由于 SQL 的 GROUP BY 语句无法保留分组子集,实现起来就变得非常复杂。我们先将员工按州分组,选出人数大于 50 人的州。再将员工按州和部门两个字段进行分组,统计每个州各部门的平均工资。最后将两个结果集内连接,选出大于 50 人的州且年龄低于部门平均年龄的记录。SQL 语句如下:

 

   select

      t1.STATE,DEPT,AVG_SALARY

   from

      (select

      STATE,DEPT,AVG(SALARY) AVG_SALARY

      from EMPLOYEE

      group by STATE,DEPT

      order by STATE,DEPT) t1

   inner join

      (select

         STATE, COUNT(*)

      from EMPLOYEE

      group by STATE

      HAVING COUNT(*)>50) t2

   on t1.STATE=t2.STATE

 

SPL的解决方案:

分组本身是复杂运算,保留分组子集可重复使用,提高运行效率。在 SPL 中,函数 A.group() 在分组后可以保留分组子集,可以针对分组子集再次进行分组汇总等运算。


A

1

=T("Employee.csv")

2

=A1.group(STATE).select(~.len()>50)

3

=A2.conj(~.groups(DEPT; avg(SALARY):AVG_SALARY).derive(A2.~.STATE:STATE))

A1:导入员工表。

A2:按州进行分组,并选出员工数大于 50 人的分组子集。

A3:对每个州的分组子集再按部门分组汇总平均工资,再把这些结果集合并。

 

总结

SPL的分组与 SQL 的分组有着本质上的区别。 SQL 的分组除了只能得到分组汇总的结果,查询时也只能选出分组时使用的字段和聚合结果。而 SPL 的分组是真正的分组,将具有相同属性的记录分在一组,分组子集中保留了数据的全部信息。分组本身是复杂运算,保留分组子集可重复使用,提高运行效率。

    另外,当查询比较复杂时,SQL 语句的复杂程度会成倍增加。比如经常要用到临时表、嵌套查询等等,使得 SQL 语句的编写和维护难度提升。而 SPL 只要按自然思维去组织计算逻辑,逐行书写出简洁的代码。

esProc 是专业的数据计算引擎,基于有序集合设计,同时提供了完善的分组运算,相当于 Java 和 SQL 优势的结合。在 SPL 的支持下,分组运算会非常容易。


Sales.csv

Employee.csv

Scores.csv

SQL 与 SPL 对比系列:
SQL 和 SPL 的集合运算对比
SQL 和 SPL 的选出运算对比
SQL 和 SPL 的有序运算对比
SQL 和 SPL 的等值分组对比
SQL 和 SPL 的非等值分组对比
SQL 和 SPL 的有序分组对比
SQL 和 SPL 的一对一和一对多连接对比
SQL 和 SPL 的多对一连接对比
SQL 和 SPL 的多对多连接对比
SQL 和 SPL 的基本静态转置对比
SQL 和 SPL 的复杂静态转置对比
SQL 和 SPL 的动态转置对比
SQL 和 SPL 的递归对比 Salescsv

ScorescsvEmployeecsv