扩展GROUP BY概述
在本章的开头已经简单描述了扩展GROUP BY的应用场景,Oracle扩展GROUP BY允许使用SQL语句对数据汇总结果进行多维展现,从而生成复杂的报表,为决策者提供有效的数据支持。最重要的是,Oracle9i之后,扩展GROUP BY的功能已经趋于完善,能够满足大部分多维数据分析统计的工作。
主要表现在:
ROLLUP、CUBE、GROUPING SETS扩展GROUP BY子句提供了不同多维分组统计功能。 3个扩展分组函数:GROUPING、GROUPING_ID、GROUP_ID提供扩展GROUP BY的辅助功能:提供区别结果行属于哪个分组级别,区分NULL值,建立有意义的报表,对汇总结果排序,过滤结果行等功能。 对扩展GROUP BY允许按重复列分组、组合列分组、部分分组、连接分组等复杂功能,另外GROUPING SETS可以接受CUBE、ROLLUP操作作为参数,这些功能使扩展GROUP BY更加强大。
本章的表DEPT和EMP都来自于SCOTT用户下,虽然表比较简单,但是足以能说明扩展GROUP BY的功能。下面开始进入主要内容,探讨强大的扩展GROUP BY功能。
ROLLUP
2.1 UNION ALL实现ROLLUP功能
假设有这样的需求:
1)统计每个部门每个职位的薪水和 2) 统计每个部门所有职位的薪水小计 3)统计所有部门所有职位的薪水合计 4)需要显示部门名、职位名和累加后的薪水值
--需求1实现
SELECT a.dname,b.job,SUM(b.sal) sum_sal
FROM dept a,emp b
WHERE a.deptno = b.deptno
GROUP BY a.dname,b.job
UNION ALL
--需求2实现
SELECT a.dname,NULL, SUM(b.sal) sum_sal
FROM dept a,emp b
WHERE a.deptno = b.deptno
GROUP BY a.dname
UNION ALL
--需求3实现
SELECT NULL,NULL, SUM(b.sal) sum_sal
FROM dept a,emp b
WHERE a.deptno = b.deptno;
DNAME JOB SUM_SAL
---------------------------- ------------------ ----------
SALES MANAGER 2850
SALES CLERK 950
ACCOUNTING MANAGER 2450
ACCOUNTING PRESIDENT 5000
ACCOUNTING CLERK 1300
SALES SALESMAN 5600
RESEARCH MANAGER 2975
RESEARCH ANALYST 3000
RESEARCH CLERK 800
ACCOUNTING 8750
RESEARCH 6775
SALES 9400
24925
已选择13行。
----------------------------------------------------------
Plan hash value: 3113041979
------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 17 | 423 | 13 (70)| 00:00:01 |
| 1 | UNION-ALL | | | | | |
| 2 | HASH GROUP BY | | 12 | 336 | 5 (20)| 00:00:01 |
| 3 | NESTED LOOPS | | 12 | 336 | 4 (0)| 00:00:01 |
| 4 | TABLE ACCESS FULL | EMP | 12 | 180 | 3 (0)| 00:00:01 |
| 5 | TABLE ACCESS BY INDEX ROWID| DEPT | 1 | 13 | 1 (0)| 00:00:01 |
|* 6 | INDEX UNIQUE SCAN | PK_DEPT | 1 | | 0 (0)| 00:00:01 |
| 7 | HASH GROUP BY | | 4 | 80 | 5 (20)| 00:00:01 |
| 8 | NESTED LOOPS | | 12 | 240 | 4 (0)| 00:00:01 |
| 9 | TABLE ACCESS FULL | EMP | 12 | 84 | 3 (0)| 00:00:01 |
| 10 | TABLE ACCESS BY INDEX ROWID| DEPT | 1 | 13 | 1 (0)| 00:00:01 |
|* 11 | INDEX UNIQUE SCAN | PK_DEPT | 1 | | 0 (0)| 00:00:01 |
| 12 | SORT AGGREGATE | | 1 | 7 | | |
|* 13 | TABLE ACCESS FULL | EMP | 12 | 84 | 3 (0)| 00:00:01 |
------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
6 - access("A"."DEPTNO"="B"."DEPTNO")
11 - access("A"."DEPTNO"="B"."DEPTNO")
13 - filter("B"."DEPTNO" IS NOT NULL)
从执行计划可以看出,为了实现这样的需求,需要多次访问EMP、DEPT表以及DEPT表的索引,这里的测试数据很少而且表结构简单,实际应用中表结构可能很复杂,经常是多表关联,数据量可能达到百万级,千万级,甚至上亿,那么使用UNION ALL,明显性能低下,如果使用WITH子句将常规分组的结果固定下来,然后在此基础上再聚合,效率可能比单纯UNION ALL好,但是还是不够完美,现在的需求是对2列进行多维分析,如果是很多列呢?必然增加语句复杂度,类似地,CUBE,GROUPING SETS用UNION ALL改写也有此类问题,何况扩展GROUP BY还提供了很多复杂功能,用UNION ALL改写就更加麻烦了。
试想对于上面的需求,如果Oracle能提供一个这样的分组功能就好了:
直接把分组的列按顺序写在一起,提供一个简单的语法结构。 此语法结构先进行全分组:标准分组。 然后这个语法结构从右到左递减列,做对应维度的分组,实现小计和合计。
2.2 ROLLUP分组
SELECT … GROUP BY ROLLUP(grouping_column_reference_list)
SELECT a.dname,b.job, SUM(b.sal) sum_sal
FROM dept a,emp b
WHERE a.deptno = b.deptno
GROUP BY ROLLUP(a.dname,b.job);
DNAME JOB SUM_SAL
---------------------------- ------------------ ----------
SALES CLERK 950
SALES MANAGER 2850
SALES SALESMAN 5600
SALES 9400
RESEARCH CLERK 800
RESEARCH ANALYST 3000
RESEARCH MANAGER 2975
RESEARCH 6775
ACCOUNTING CLERK 1300
ACCOUNTING MANAGER 2450
ACCOUNTING PRESIDENT 5000
ACCOUNTING 8750
24925
已选择13行。
----------------------------------------------------------
Plan hash value: 503922295
-----------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 12 | 336 | 5 (20)| 00:00:01 |
| 1 | SORT GROUP BY ROLLUP | | 12 | 336 | 5 (20)| 00:00:01 |
| 2 | NESTED LOOPS | | 12 | 336 | 4 (0)| 00:00:01 |
| 3 | TABLE ACCESS FULL | EMP | 12 | 180 | 3 (0)| 00:00:01 |
| 4 | TABLE ACCESS BY INDEX ROWID| DEPT | 1 | 13 | 1 (0)| 00:00:01 |
|* 5 | INDEX UNIQUE SCAN | PK_DEPT | 1 | | 0 (0)| 00:00:01 |
-----------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
5 - access("A"."DEPTNO"="B"."DEPTNO")
ROLLUP分组具有方向性,从上面的结果看出,ROLLUP(a.dname,b.job)分组的过程是这样的:
标准分组:GROUP BY(a.dname,b.job),对每个部门每个职位进行分组(我分析的时候经常对GROUP BY加上括号,其实没有必要,只是为了更直观而已)。 从右到左递减:GROUP BY(a.dname,NULL),其实这个NULL没有必要使用,这里只是为了方便分析。这个过程是对上个级别分组的小计,也就是对每个dname值,计算横跨所有job的小计。 最后合计:相当于GROUP BY (NULL,NULL)。
分组级别 | 描述 |
a,b,c | 标准分组 |
a,b | 对于每个a,b列值,计算横跨c列的小计 |
a | 对于每个a列值,计算横跨b,c列的小计 |
合计汇总 | 合计 |
另外提一下,其实ROLLUP操作,如果使用HINT: expand_gset_to_union,那么则优化器会将ROLLUP转为对应的UNION ALL操作,其它的GROUPING SETS、CUBE也可以,有兴趣的可以试一下。
ROLLUP语法简单,而且具体处理过程也很简单,除了第1个是标准分组,然后就是列从右到左递减的分组,最后合计。下面实现需求:
计算每个入职时间(年)、部门、职位的标准分组的薪水和。 计算每个入职时间(年)、部门的所有职位的薪水小计。 计算每个入职时间(年)的所有部门所有职位的薪水小计。 最后合计薪水,显示入职时间(年)、部门名、职位名。
SELECT to_char(b.hiredate,'yyyy') hire_year,a.dname,b.job, SUM(sal) sum_sal
FROM dept a,emp b
WHERE a.deptno = b.deptno
GROUP BY ROLLUP(to_char(b.hiredate,'yyyy'),a.dname,b.job);
HIRE_YEA DNAME JOB SUM_SAL
-------- ---------------------------- ------------------ ----------
1980 RESEARCH CLERK 800
1980 RESEARCH 800
1980 800
1981 SALES CLERK 950
1981 SALES MANAGER 2850
1981 SALES SALESMAN 5600
1981 SALES 9400
1981 RESEARCH ANALYST 3000
1981 RESEARCH MANAGER 2975
1981 RESEARCH 5975
1981 ACCOUNTING MANAGER 2450
1981 ACCOUNTING PRESIDENT 5000
1981 ACCOUNTING 7450
1981 22825
1982 ACCOUNTING CLERK 1300
1982 ACCOUNTING 1300
1982 1300
24925
相关行 | 描述 |
第7行 | 对第4、5、6行的所有JOB进行小计 |
第14行 | 对第7行、第13行的所有DNAME、JOB进行小计,当然也相当于对4-6、8-12行所有DNAME、JOB的小计。 |
最后一行 | 对所有入职日期(精确到年)、DNAME、JOB进行合计 |
因为ROLLUP分组过程具有方向性,所以通过改变ROLLUP中列的顺序就可以达到改变报表结果和含义的目的。比如将前面的ROLLUP(dname,job)改为ROLLUP(job,dname)则含义就发生了变化,现在需要查询的就是标准分组、计算每个job的所有部门的小计、最后合计,这里就两个列,也就是小计的含义发生了变化,请看:
SELECT b.job,a.dname, SUM(b.sal) sum_sal
FROM dept a,emp b
WHERE a.deptno = b.deptno
GROUP BY ROLLUP(b.job,a.dname);
JOB DNAME SUM_SAL
------------------ ---------------------------- ----------
CLERK SALES 950
CLERK RESEARCH 800
CLERK ACCOUNTING 1300
CLERK 3050
ANALYST RESEARCH 3000
ANALYST 3000
MANAGER SALES 2850
MANAGER RESEARCH 2975
MANAGER ACCOUNTING 2450
MANAGER 8275
SALESMAN SALES 5600
SALESMAN 5600
PRESIDENT ACCOUNTING 5000
PRESIDENT 5000
24925
已选择15行。
2.3 部分ROLLUP分组
SELECT to_char(b.hiredate,'yyyy') hire_year,a.dname,b.job, SUM(sal) sum_sal
FROM dept a,emp b
WHERE a.deptno = b.deptno
GROUP BY to_char(b.hiredate,'yyyy'),a.dname,ROLLUP(b.job);
ROLLUP总结

本文作者:丁 俊(上海新炬中北团队)
本文来源:“IT那活儿”公众号





