分组操作group by 和分组的强化(rollup)
分组操作和分组函数的使用,对于编写SQL语句的人来说,是最基本的概念。
我们来看下面的例子:
在这里我们使用员工表EMP
scott@DB01> select * from emp;
EMPNO
ENAME
JOB
MGR
HIREDATE
SAL
COMM
DEPTNO
---------- ---------- --------- ---------- ------------------- ---------- ---------- ----------
7369 SMITH CLERK 7902 1980-12-17 00:00:00 800 20
7499 ALLEN SALESMAN 7698 1981-02-20 00:00:00 1600 300 30
7521 WARD SALESMAN 7698 1981-02-22 00:00:00 1250 500 30
7566 JONES MANAGER 7839 1981-04-02 00:00:00 2975 20
7654 MARTIN SALESMAN 7698 1981-09-28 00:00:00 1250 1400 30
7698 BLAKE MANAGER 7839 1981-05-01 00:00:00 2850 30
7782 CLARK MANAGER 7839 1981-06-09 00:00:00 2450 10
7788 SCOTT ANALYST 7566 1987-04-19 00:00:00 3000 20
7839 KING PRESIDENT 1981-11-17 00:00:00 5000 10
7844 TURNER SALESMAN 7698 1981-09-08 00:00:00 1500 0 30
7876 ADAMS CLERK 7788 1987-05-23 00:00:00 1100 20
7900 JAMES CLERK 7698 1981-12-03 00:00:00 950 30
7902 FORD ANALYST 7566 1981-12-03 00:00:00 3000 20
7934 MILLER CLERK 7782 1982-01-23 00:00:00 1300 10
14 rows selected.
在员工表中有14条记录,即14个员工,我们可以看到,这14个员工分别属于3个部门(10,20,30),我们可以提出求EMP表中,每个部门的员工薪水总和
scott@DB01> select deptno,sum(sal) tsal
2 from emp
3 group by deptno;
DEPTNO TSAL
---------- ----------
30 9400
20 10875
10 8750
在这里稍微需要注意的是:select 列表里如果出现列的话,那在group by语句中同样需要列名,并且只能是列名本身,不能是列的别名。group by语句可以说是oracle语句里最严格的语句,后面只能跟列的真名,别名、位置号、函数、表达式、子查询 都不被允许。当然如果只考虑实现这里已经做到了,如果我们深入了解一点的话,分组对于数据库来说是要消耗资源的,比如cpu、内存
在oracle9i之前 ,分组操作内部主要通过排序来实现,10刚开始,采用hash的算法实现,我们看一下10g下,让面语句的执行计划
scott@DB01> set autotrace trace exp
scott@DB01> /
Execution Plan
----------------------------------------------------------
Plan hash value: 4067220884
---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 14 | 364 | 4 (25)| 00:00:01 |
| 1 | HASH GROUP BY | | 14 | 364 | 4 (25)| 00:00:01 |
| 2 | TABLE ACCESS FULL| EMP | 14 | 364 | 3 (0)| 00:00:01 |
---------------------------------------------------------------------------
Note
-----
- dynamic sampling used for this statement
其实在有些情况下,我们可以避免hash或是sort的发生,也可以实现分组查询的效果,比如说通过索引,当然这需要你有适当的索引存在。
我们来看下面的演示:
scott@DB01> set autotrace off
scott@DB01> create table s_test(id number,name varchar2(10),sal number);
Table created.
scott@DB01> begin
2 for i in 1..20000 loop
3 insert into s_test values(i,i||'name',i*10);
4 end loop;
5 commit;
6 end;
7 /
PL/SQL procedure successfully completed.
scott@DB01> /
PL/SQL procedure successfully completed.
scott@DB01> /
PL/SQL procedure successfully completed.
scott@DB01> select count(*) from s_test;
COUNT(*)
----------
60000
我在这里建了一张表s_test,分3次往表里插入数据1-20000,现在我的需求是,找到表里100-120的记录,以及他们出现的次数
scott@DB01> select id,name,count(*) from s_test where id>=100 and id<=120 group by id,name;
ID NAME COUNT(*)
---------- ---------- ----------
115 115name 3
101 101name 3
103 103name 3
106 106name 3
109 109name 3
118 118name 3
105 105name 3
114 114name 3
102 102name 3
104 104name 3
112 112name 3
116 116name 3
100 100name 3
110 110name 3
113 113name 3
117 117name 3
119 119name 3
107 107name 3
108 108name 3
111 111name 3
120 120name 3
21 rows selected.
我们来看一下语句的执行计划
scott@DB01> set autotrace trace exp
scott@DB01> /
Execution Plan
----------------------------------------------------------
Plan hash value: 752916570
-----------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 163 | 3260 | 58 (6)| 00:00:01 |
| 1 | HASH GROUP BY | | 163 | 3260 | 58 (6)| 00:00:01 |
|* 2 | TABLE ACCESS FULL| S_TEST | 163 | 3260 | 57 (4)| 00:00:01 |
-----------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter("ID">=100 AND "ID"<=120)
Note
-----
- dynamic sampling used for this statement
在执行计划中我们发现,成本Cost是58,还有cpu的消耗,在执行计划的第2步,我们发现为了实现分组,oracle做了hash。接下来我们建一个组合索引看看
scott@DB01> create index s_id_n_idx on s_test(id,name);
Index created.
scott@DB01> select id,name,count(*) from s_test where id>=100 and id<=120 group by id,name;
Execution Plan
----------------------------------------------------------
Plan hash value: 826362002
-----------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 63 | 1260 | 2 (0)| 00:00:01 |
| 1 | SORT GROUP BY NOSORT| | 63 | 1260 | 2 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | S_ID_N_IDX | 63 | 1260 | 2 (0)| 00:00:01 |
-----------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("ID">=100 AND "ID"<=120)
filter("ID">=100 AND "ID"<=120)
Note
-----
- dynamic sampling used for this statement
在第一步中,应该做的排序并没有做 SORT GROUP BY NOSORT,这样就节省了cpu。
当然在这个例子当中,我们发现了一个重要的问题,就是语句的成本急剧下降,当然,这是通过索引,改变了数据的访问方法造成的,以后有机会在讨论索引的时候,我们会展开来说。
我们接下来看这样一个需求,根据表里的deptno和job求分组,得到每个job下的薪水综合,然后在部门级别做汇总,求小计,在整张表汇总,求总计
scott@DB01> select deptno,job,empno,ename,sal from emp order by deptno,job;
DEPTNO JOB EMPNO ENAME SAL
---------- --------- ---------- ---------- ----------
10 CLERK 7934 MILLER 1300
10 MANAGER 7782 CLARK 2450
10 PRESIDENT 7839 KING 5000
20 ANALYST 7788 SCOTT 3000
20 ANALYST 7902 FORD 3000
20 CLERK 7876 ADAMS 1100
20 CLERK 7369 SMITH 800
20 MANAGER 7566 JONES 2975
30 CLERK 7900 JAMES 950
30 MANAGER 7698 BLAKE 2850
30 SALESMAN 7654 MARTIN 1250
30 SALESMAN 7521 WARD 1250
30 SALESMAN 7499 ALLEN 1600
30 SALESMAN 7844 TURNER 1500
其实需求本身很简单,如果仅仅是为了实现的话,使用集合并运算符union就可以了,不过union的效率在这里是非常的低。
scott@DB01> select deptno,job,sum(sal) tsal from emp group by deptno,job
2 union
3 select deptno,to_char(null),sum(sal) from emp group by deptno
4 union
5 select to_number(null),to_char(null),sum(sal) from emp;
DEPTNO JOB TSAL
---------- --------- ----------
10 CLERK 1300
10 MANAGER 2450
10 PRESIDENT 5000
10 8750
20 ANALYST 6000
20 CLERK 1900
20 MANAGER 2975
20 10875
30 CLERK 950
30 MANAGER 2850
30 SALESMAN 5600
30 9400
29025
13 rows selected.
为了得到比较高效的sql,我们可以借助于oracle分组里面的rollup来实现,我们可以得到同样的效果
scott@DB01> select deptno,job,sum(sal) tsal from emp group by rollup(deptno,job);
DEPTNO JOB TSAL
---------- --------- ----------
10 CLERK 1300
10 MANAGER 2450
10 PRESIDENT 5000
10 8750
20 CLERK 1900
20 ANALYST 6000
20 MANAGER 2975
20 10875
30 CLERK 950
30 MANAGER 2850
30 SALESMAN 5600
30 9400
29025
13 rows selected.
第一直观的表现,使用rollup要比使用分组再union的方法语句简单很多,更重要的是,我们只对emp访问了一次。
为了进一步比较,我们来看一下语句的执行计划
scott@DB01> set autotrace trace exp
scott@DB01> select deptno,job,sum(sal) tsal from emp group by deptno,job
2 union
3 select deptno,to_char(null),sum(sal) from emp group by deptno
4 union
5 select to_number(null),to_char(null),sum(sal) from emp;
Execution Plan
----------------------------------------------------------
Plan hash value: 3412076862
-----------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 29 | 825 | 14 (79)| 00:00:01 |
| 1 | SORT UNIQUE | | 29 | 825 | 14 (79)| 00:00:01 |
| 2 | UNION-ALL | | | | | |
| 3 | HASH GROUP BY | | 14 | 448 | 5 (40)| 00:00:01 |
| 4 | TABLE ACCESS FULL| EMP | 14 | 448 | 3 (0)| 00:00:01 |
| 5 | HASH GROUP BY | | 14 | 364 | 5 (40)| 00:00:01 |
| 6 | TABLE ACCESS FULL| EMP | 14 | 364 | 3 (0)| 00:00:01 |
| 7 | SORT AGGREGATE | | 1 | 13 | 4 (25)| 00:00:01 |
| 8 | TABLE ACCESS FULL| EMP | 14 | 182 | 3 (0)| 00:00:01 |
-----------------------------------------------------------------------------
Note
-----
- dynamic sampling used for this statement
scott@DB01> select deptno,job,sum(sal) tsal from emp group by rollup(deptno,job);
Execution Plan
----------------------------------------------------------
Plan hash value: 52302870
-----------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 14 | 448 | 4 (25)| 00:00:01 |
| 1 | SORT GROUP BY ROLLUP| | 14 | 448 | 4 (25)| 00:00:01 |
| 2 | TABLE ACCESS FULL | EMP | 14 | 448 | 3 (0)| 00:00:01 |
-----------------------------------------------------------------------------
Note
-----
- dynamic sampling used for this statement
通过比较发现,两个语句的成本cost会差出很多14vs4。所以,如果我们以后有上面类似的需求的话,可以考虑使用rollup。
注:rollup语法
select a,b,组函数
from 表
group by rollup(a,b);
这个语法相当于 group by a,b union group a union group by null的sql语句的组合
---------- ---------- --------- ---------- ------------------- ---------- ---------- ----------
7369 SMITH CLERK 7902 1980-12-17 00:00:00 800 20
7499 ALLEN SALESMAN 7698 1981-02-20 00:00:00 1600 300 30
7521 WARD SALESMAN 7698 1981-02-22 00:00:00 1250 500 30
7566 JONES MANAGER 7839 1981-04-02 00:00:00 2975 20
7654 MARTIN SALESMAN 7698 1981-09-28 00:00:00 1250 1400 30
7698 BLAKE MANAGER 7839 1981-05-01 00:00:00 2850 30
7782 CLARK MANAGER 7839 1981-06-09 00:00:00 2450 10
7788 SCOTT ANALYST 7566 1987-04-19 00:00:00 3000 20
7839 KING PRESIDENT 1981-11-17 00:00:00 5000 10
7844 TURNER SALESMAN 7698 1981-09-08 00:00:00 1500 0 30
7876 ADAMS CLERK 7788 1987-05-23 00:00:00 1100 20
7900 JAMES CLERK 7698 1981-12-03 00:00:00 950 30
7902 FORD ANALYST 7566 1981-12-03 00:00:00 3000 20
7934 MILLER CLERK 7782 1982-01-23 00:00:00 1300 10
14 rows selected.
在员工表中有14条记录,即14个员工,我们可以看到,这14个员工分别属于3个部门(10,20,30),我们可以提出求EMP表中,每个部门的员工薪水总和
scott@DB01> select deptno,sum(sal) tsal
2 from emp
3 group by deptno;
DEPTNO TSAL
---------- ----------
30 9400
20 10875
10 8750
在这里稍微需要注意的是:select 列表里如果出现列的话,那在group by语句中同样需要列名,并且只能是列名本身,不能是列的别名。group by语句可以说是oracle语句里最严格的语句,后面只能跟列的真名,别名、位置号、函数、表达式、子查询 都不被允许。当然如果只考虑实现这里已经做到了,如果我们深入了解一点的话,分组对于数据库来说是要消耗资源的,比如cpu、内存
在oracle9i之前 ,分组操作内部主要通过排序来实现,10刚开始,采用hash的算法实现,我们看一下10g下,让面语句的执行计划
scott@DB01> set autotrace trace exp
scott@DB01> /
Execution Plan
----------------------------------------------------------
Plan hash value: 4067220884
---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 14 | 364 | 4 (25)| 00:00:01 |
| 1 | HASH GROUP BY | | 14 | 364 | 4 (25)| 00:00:01 |
| 2 | TABLE ACCESS FULL| EMP | 14 | 364 | 3 (0)| 00:00:01 |
---------------------------------------------------------------------------
Note
-----
- dynamic sampling used for this statement
其实在有些情况下,我们可以避免hash或是sort的发生,也可以实现分组查询的效果,比如说通过索引,当然这需要你有适当的索引存在。
我们来看下面的演示:
scott@DB01> set autotrace off
scott@DB01> create table s_test(id number,name varchar2(10),sal number);
Table created.
scott@DB01> begin
2 for i in 1..20000 loop
3 insert into s_test values(i,i||'name',i*10);
4 end loop;
5 commit;
6 end;
7 /
PL/SQL procedure successfully completed.
scott@DB01> /
PL/SQL procedure successfully completed.
scott@DB01> /
PL/SQL procedure successfully completed.
scott@DB01> select count(*) from s_test;
COUNT(*)
----------
60000
我在这里建了一张表s_test,分3次往表里插入数据1-20000,现在我的需求是,找到表里100-120的记录,以及他们出现的次数
scott@DB01> select id,name,count(*) from s_test where id>=100 and id<=120 group by id,name;
ID NAME COUNT(*)
---------- ---------- ----------
115 115name 3
101 101name 3
103 103name 3
106 106name 3
109 109name 3
118 118name 3
105 105name 3
114 114name 3
102 102name 3
104 104name 3
112 112name 3
116 116name 3
100 100name 3
110 110name 3
113 113name 3
117 117name 3
119 119name 3
107 107name 3
108 108name 3
111 111name 3
120 120name 3
21 rows selected.
我们来看一下语句的执行计划
scott@DB01> set autotrace trace exp
scott@DB01> /
Execution Plan
----------------------------------------------------------
Plan hash value: 752916570
-----------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 163 | 3260 | 58 (6)| 00:00:01 |
| 1 | HASH GROUP BY | | 163 | 3260 | 58 (6)| 00:00:01 |
|* 2 | TABLE ACCESS FULL| S_TEST | 163 | 3260 | 57 (4)| 00:00:01 |
-----------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter("ID">=100 AND "ID"<=120)
Note
-----
- dynamic sampling used for this statement
在执行计划中我们发现,成本Cost是58,还有cpu的消耗,在执行计划的第2步,我们发现为了实现分组,oracle做了hash。接下来我们建一个组合索引看看
scott@DB01> create index s_id_n_idx on s_test(id,name);
Index created.
scott@DB01> select id,name,count(*) from s_test where id>=100 and id<=120 group by id,name;
Execution Plan
----------------------------------------------------------
Plan hash value: 826362002
-----------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 63 | 1260 | 2 (0)| 00:00:01 |
| 1 | SORT GROUP BY NOSORT| | 63 | 1260 | 2 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | S_ID_N_IDX | 63 | 1260 | 2 (0)| 00:00:01 |
-----------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("ID">=100 AND "ID"<=120)
filter("ID">=100 AND "ID"<=120)
Note
-----
- dynamic sampling used for this statement
在第一步中,应该做的排序并没有做 SORT GROUP BY NOSORT,这样就节省了cpu。
当然在这个例子当中,我们发现了一个重要的问题,就是语句的成本急剧下降,当然,这是通过索引,改变了数据的访问方法造成的,以后有机会在讨论索引的时候,我们会展开来说。
我们接下来看这样一个需求,根据表里的deptno和job求分组,得到每个job下的薪水综合,然后在部门级别做汇总,求小计,在整张表汇总,求总计
scott@DB01> select deptno,job,empno,ename,sal from emp order by deptno,job;
DEPTNO JOB EMPNO ENAME SAL
---------- --------- ---------- ---------- ----------
10 CLERK 7934 MILLER 1300
10 MANAGER 7782 CLARK 2450
10 PRESIDENT 7839 KING 5000
20 ANALYST 7788 SCOTT 3000
20 ANALYST 7902 FORD 3000
20 CLERK 7876 ADAMS 1100
20 CLERK 7369 SMITH 800
20 MANAGER 7566 JONES 2975
30 CLERK 7900 JAMES 950
30 MANAGER 7698 BLAKE 2850
30 SALESMAN 7654 MARTIN 1250
30 SALESMAN 7521 WARD 1250
30 SALESMAN 7499 ALLEN 1600
30 SALESMAN 7844 TURNER 1500
其实需求本身很简单,如果仅仅是为了实现的话,使用集合并运算符union就可以了,不过union的效率在这里是非常的低。
scott@DB01> select deptno,job,sum(sal) tsal from emp group by deptno,job
2 union
3 select deptno,to_char(null),sum(sal) from emp group by deptno
4 union
5 select to_number(null),to_char(null),sum(sal) from emp;
DEPTNO JOB TSAL
---------- --------- ----------
10 CLERK 1300
10 MANAGER 2450
10 PRESIDENT 5000
10 8750
20 ANALYST 6000
20 CLERK 1900
20 MANAGER 2975
20 10875
30 CLERK 950
30 MANAGER 2850
30 SALESMAN 5600
30 9400
29025
13 rows selected.
为了得到比较高效的sql,我们可以借助于oracle分组里面的rollup来实现,我们可以得到同样的效果
scott@DB01> select deptno,job,sum(sal) tsal from emp group by rollup(deptno,job);
DEPTNO JOB TSAL
---------- --------- ----------
10 CLERK 1300
10 MANAGER 2450
10 PRESIDENT 5000
10 8750
20 CLERK 1900
20 ANALYST 6000
20 MANAGER 2975
20 10875
30 CLERK 950
30 MANAGER 2850
30 SALESMAN 5600
30 9400
29025
13 rows selected.
第一直观的表现,使用rollup要比使用分组再union的方法语句简单很多,更重要的是,我们只对emp访问了一次。
为了进一步比较,我们来看一下语句的执行计划
scott@DB01> set autotrace trace exp
scott@DB01> select deptno,job,sum(sal) tsal from emp group by deptno,job
2 union
3 select deptno,to_char(null),sum(sal) from emp group by deptno
4 union
5 select to_number(null),to_char(null),sum(sal) from emp;
Execution Plan
----------------------------------------------------------
Plan hash value: 3412076862
-----------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 29 | 825 | 14 (79)| 00:00:01 |
| 1 | SORT UNIQUE | | 29 | 825 | 14 (79)| 00:00:01 |
| 2 | UNION-ALL | | | | | |
| 3 | HASH GROUP BY | | 14 | 448 | 5 (40)| 00:00:01 |
| 4 | TABLE ACCESS FULL| EMP | 14 | 448 | 3 (0)| 00:00:01 |
| 5 | HASH GROUP BY | | 14 | 364 | 5 (40)| 00:00:01 |
| 6 | TABLE ACCESS FULL| EMP | 14 | 364 | 3 (0)| 00:00:01 |
| 7 | SORT AGGREGATE | | 1 | 13 | 4 (25)| 00:00:01 |
| 8 | TABLE ACCESS FULL| EMP | 14 | 182 | 3 (0)| 00:00:01 |
-----------------------------------------------------------------------------
Note
-----
- dynamic sampling used for this statement
scott@DB01> select deptno,job,sum(sal) tsal from emp group by rollup(deptno,job);
Execution Plan
----------------------------------------------------------
Plan hash value: 52302870
-----------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 14 | 448 | 4 (25)| 00:00:01 |
| 1 | SORT GROUP BY ROLLUP| | 14 | 448 | 4 (25)| 00:00:01 |
| 2 | TABLE ACCESS FULL | EMP | 14 | 448 | 3 (0)| 00:00:01 |
-----------------------------------------------------------------------------
Note
-----
- dynamic sampling used for this statement
通过比较发现,两个语句的成本cost会差出很多14vs4。所以,如果我们以后有上面类似的需求的话,可以考虑使用rollup。
注:rollup语法
select a,b,组函数
from 表
group by rollup(a,b);
这个语法相当于 group by a,b union group a union group by null的sql语句的组合