12.1 变换结果变成一行
你想 把若干行数据重新组合成一个新数据,元素数据的每一行变换后作为新数据的一列出现。如下。统计每个部门的员工人数。
DEPTNO CNT------ ----------10 320 530 6
将其变成如下
DEPTNO_10 DEPTNO_20 DEPTNO_30--------- ---------- ----------3 5 6
解决方案
使用case表达式和聚合函数SUM实现结果集变换
select sum(case when deptno=10 then 1 else 0 end) as deptno_10,sum(case when deptno=20 then 1 else 0 end) as deptno_20,sum(case when deptno=30 then 1 else 0 end) as deptno_30from emp;
12.2 变换结果集成多行
将行变成列,并根据指定列的值来决定每一行原来的数据要被划分到新数据的哪一列。
例如,你希望返回每个员工和他们的职位(对应EMP表的JOB列) ,先查询到的结果集如下
JOB ENAME--------- ----------ANALYST SCOTTANALYST FORDCLERK SMITHCLERK ADAMSCLERK MILLERCLERK JAMESMANAGER JONESMANAGER CLARKMANAGER BLAKEPRESIDENT KINGSALESMAN ALLENSALESMAN MARTINSALESMAN TURNERSALESMAN WARD
希望格式化上述结果集,为每一种职位生成一列新数据
CLERKS ANALYSTS MGRS PREZ SALES------ -------- ----- ---- ------MILLER FORD CLARK KING TURNERJAMES SCOTT BLAKE MARTINADAMS JONES WARDSMITH ALLEN
DB2 Oracle SQL Server
select max(case when job = 'CLERK' then ename else null end) as clerks,max(case when job = 'ANALYST' then ename else null end) as analysts,max(case when job = 'MANAGER' then ename else null end) as mgrs,max(case when job = 'PRESIDENT' then ename else null end) as prez,max(case when job = 'SALESMAN' then ename else null end) as salesfrom (select job,ename,row_number() over(partition by job order by ename) rnfrom emp) xgroup by rn ;
使用窗口函数ROW_NUMBER OVER确保每一个JOB/ENAME组合的唯一性。
mysql> select job, ename, row_number() over(partition by job order by ename) rnfrom emp;+-----------+--------+----+| job | ename | rn |+-----------+--------+----+| ANALYST | FORD | 1 || ANALYST | SCOTT | 2 || CLERK | ADAMS | 1 || CLERK | JAMES | 2 || CLERK | MILLER | 3 || CLERK | SMITH | 4 || MANAGER | BLAKE | 1 || MANAGER | CLARK | 2 || MANAGER | JONES | 3 || PRESIDENT | KING | 1 || SALESMAN | ALLEN | 1 || SALESMAN | MARTIN | 2 || SALESMAN | TURNER | 3 || SALESMAN | WARD | 4 |+-----------+--------+----+14 rows in set (0.06 sec)
对于一种给定的职位,为其中的每一个ENAME安排一个唯一的“行编号”,这样即使出现了两个员工具有相同的名字和职位情况也不会相同。这样做是为了既能基于行编号分组,又不会因为使用了MAX而遗漏了任何一个员工。如果不使用ROW_NUMBER OVER函数的话,会得到如下结果。
mysql> select max(case when job='CLERK'then ename else null end) as clerks,max(case when job='ANALYST'then ename else null end) as analysts,max(case when job='MANAGER'then ename else null end) as mgrs,max(case when job='PRESIDENT'then ename else null end) as prez,max(case when job='SALESMAN'then ename else null end) as salesfrom emp;+--------+----------+-------+------+-------+| clerks | analysts | mgrs | prez | sales |+--------+----------+-------+------+-------+| SMITH | SCOTT | JONES | KING | WARD |+--------+----------+-------+------+-------+1 row in set (0.04 sec)
上述,每一种JOB只会返回一行:ENAME值最大的那个员工会被返回。做结果集变换的时候,使用MIN和MAX是为了从结果集里面剔除掉Null值,而不是为了过滤掉一些ENAME。
mysql> select rn,case when job='CLERK'then ename else null end as clerks,case when job='ANALYST'then ename else null end as analysts,case when job='MANAGER'then ename else null end as mgrs,case when job='PRESIDENT'then ename else null end as prez,case when job='SALESMAN'then ename else null end as salesfrom (select job,ename,row_number()over(partition by job order by ename) rnfrom emp) x;+----+--------+----------+-------+------+--------+| rn | clerks | analysts | mgrs | prez | sales |+----+--------+----------+-------+------+--------+| 1 | NULL | FORD | NULL | NULL | NULL || 2 | NULL | SCOTT | NULL | NULL | NULL || 1 | ADAMS | NULL | NULL | NULL | NULL || 2 | JAMES | NULL | NULL | NULL | NULL || 3 | MILLER | NULL | NULL | NULL | NULL || 4 | SMITH | NULL | NULL | NULL | NULL || 1 | NULL | NULL | BLAKE | NULL | NULL || 2 | NULL | NULL | CLARK | NULL | NULL || 3 | NULL | NULL | JONES | NULL | NULL || 1 | NULL | NULL | NULL | KING | NULL || 1 | NULL | NULL | NULL | NULL | ALLEN || 2 | NULL | NULL | NULL | NULL | MARTIN || 3 | NULL | NULL | NULL | NULL | TURNER || 4 | NULL | NULL | NULL | NULL | WARD |+----+--------+----------+-------+------+--------+14 rows in set (0.04 sec)
12.3 反向变换结果集
将列数据编程行数据,如下
DEPTNO_10 DEPTNO_20 DEPTNO_30---------- ---------- ----------3 5 6
转换成
DEPTNO COUNTS_BY_DEPT------ --------------10 320 530 6
为了把列数据变换成行数据,需要用到笛卡尔积。事先知道多少列需要被转换成行形式,因为创建笛卡尔积是用到的表达式必须有一个计数,该基数至少要等到需要做变换的列的个数。
SELECT dept.deptno,CASE dept.deptnoWHEN 10 THEN emp_cnts.deptno_10WHEN 20 THEN emp_cnts.deptno_20WHEN 30 THEN emp_cnts.deptno_30END AS counts_by_deptFROM (SELECT SUM(CASE WHEN deptno=10 THEN 1 ELSE 0 END) AS deptno_10,SUM(CASE WHEN deptno=20 THEN 1 ELSE 0 END) AS deptno_20,SUM(CASE WHEN deptno=30 THEN 1 ELSE 0 END) AS deptno_30FROM emp) emp_cnts,(SELECT deptno FROM dept WHERE deptno <= 30) dept ;
上述,内嵌试图EMP_CNTS就是上述的非规范化视图。即“宽”的结果集,也就是变换前的列形式的数据。
SELECT SUM(CASE WHEN deptno=10 THEN 1 ELSE 0 END) AS deptno_10,SUM(CASE WHEN deptno=20 THEN 1 ELSE 0 END) AS deptno_20,SUM(CASE WHEN deptno=30 THEN 1 ELSE 0 END) AS deptno_30FROM emp ;-----------------------------deptno_10 deptno_20 deptno_30--------- --------- -----------3 5 6
因为上述数据分别为3列存储,所以我们需要生成3行新数据。首先基于内嵌试图EMP_CNT和至少有3行数据的表结构构造一个笛卡尔积。下面的diamante借助DEPT表构造了一个笛卡尔积
SELECT dept.deptno,
emp_cnts.deptno_10,
emp_cnts.deptno_20,
emp_cnts.deptno_30
FROM (
SELECT SUM(CASE WHEN deptno=10 THEN 1 ELSE 0 END) AS deptno_10,
SUM(CASE WHEN deptno=20 THEN 1 ELSE 0 END) AS deptno_20,
SUM(CASE WHEN deptno=30 THEN 1 ELSE 0 END) AS deptno_30
FROM emp
) emp_cnts,
(SELECT deptno FROM dept WHERE deptno <= 30) dept;
----------------------------------------------
deptno deptno_10 deptno_20 deptno_30
------ --------- --------- -----------
10 3 5 6
20 3 5 6
30 3 5 6
笛卡尔积使得我们能够为内嵌试图EMP_CNTS的每一列返回一行数据,由于最终的结果集要DEPTNO和DEPTNO对应的员工人数,因此要用CASE表达式把每行3列变成每行1列。
12.4 反向变换结果集成一列
将一个查询结果合并为一列。例如,返回DEMPTNO等于10的全体员工的ENAME JOB和SAL。并且将3列的值合并为1列。每一个员工返回3行数据。员工之间用空行分隔开。如下
EMPS
----------
CLARK
MANAGER
2450
KING
PRESIDENT
5000
MILLER
CLERK
1300
解决方案
关键在于使用笛卡尔积为每个员工返回4行数据。我们需要把每一行变成一列,并且在两个员工之间多留一个空白行。
DB2 Oracle SQL Server
使用窗口函数ROW_NUMBER OVER基于EMPNO为每一列数据排名(1-4 行)。然后使用case 表达式把3列数据变成1列。
SELECT CASE rn
WHEN 1 THEN ename
WHEN 2 THEN job
WHEN 3 THEN CAST(sal AS CHAR(4))
END emps
FROM (
SELECT e.ename,e.job,e.sal,
row_number()over(PARTITION BY e.empno
ORDER BY e.empno) rn
FROM emp e,
(SELECT *
FROM emp WHERE job='CLERK') four_rows
WHERE e.deptno=10
) X ;
讨论
使用窗口函数ROW_NUMBER为DEPTNO等于10的每一个员工生成一个序号
select e.ename,
e.job,
e.sal,
row_number() over(partition by e.empno order by e.empno) rn
from emp e
where e.deptno = 10 ;
---------------
ename job sal rn
------ --------- ------ --------
CLARK MANAGER 2450 1
KING PRESIDENT 5000 1
MILLER CLERK 1300 1
此时,上面的序号其实每一意义。我们按照EMPNO分区,因而DEPTNO等于10的所有行的序号都是1,如果引入笛卡尔积,序号的作用就显现出来了。如下所示。
SELECT e.ename,
e.job,
e.sal,
row_number() over(PARTITION BY e.empno ORDER BY e.empno) rn
FROM emp e, (SELECT * FROM emp WHERE job = 'CLERK') four_rows
WHERE e.deptno = 10
------------------------------------------
ename job sal rn
------ --------- ------ --------
CLARK MANAGER 2450 1
CLARK MANAGER 2450 2
CLARK MANAGER 2450 3
CLARK MANAGER 2450 4
KING PRESIDENT 5000 1
KING PRESIDENT 5000 2
KING PRESIDENT 5000 3
KING PRESIDENT 5000 4
MILLER CLERK 1300 1
MILLER CLERK 1300 2
MILLER CLERK 1300 3
MILLER CLERK 1300 4
上述需要注意2个关键点
- 每个员工的RN值不再是1,现在它变成了从1到4的循环出现的序列值,原因在于窗口函数会在FROM和WHERE子句之后才评估执行。因此,EMPNO分区就导致当遇到一个新员工是,RN值才会被重置为1.
- 内嵌试图FOUR_ROWS的存在只是为了返回一个包含4行数据的结果集,它的作用仅限于次。我们希望为每一列(ENAME、JOB SAL)返回每一行,然后再加上一个空行。
最困难的工作已经完成了,剩下的就是使用CASE表达式把每个员工的ENAME JOB 和SAL并入一列(为了保证CASE成功执行,我们还需要把SAL转换成字符串)。
SELECT CASE rn
WHEN 1 THEN
ename
WHEN 2 THEN
job
WHEN 3 THEN
CAST(sal AS CHAR(4))
END emps
FROM (SELECT e.ename,
e.job,
e.sal,
row_number() over(PARTITION BY e.empno ORDER BY e.empno) rn
FROM emp e, (SELECT * FROM emp WHERE job = 'CLERK') four_rows
WHERE e.deptno = 10) X
---------------------------------------------
emps
-----------
CLARK
MANAGER
2450
(NULL)
KING
PRESIDENT
5000
(NULL)
MILLER
CLERK
1300
(NULL)
12.5 删除重复数据
正在生成一个报表,当相邻两行的某列出现了相同的值时,希望那个值值显示一次,例如,从EMP表中提出出DEPTNO和ENAME,希望按照DEPTNO对所有的进行分组,并且希望每个DEPTNO只显示一次,希望返回如下的结果集。
DEPTNO ENAME
------ ---------
10 CLARK
KING
MILLER
20 SMITH
ADAMS
FORD
SCOTT
JONES
30 ALLEN
BLAKE
MARTIN
JAMES
TURNER
WARD
解决方案
这是一个简单的格式化问题,Oracle提供的窗口函数LAG OVER能很容易地解决这一问题。
select to_number(decode(lag(deptno) over(order by deptno),
deptno,
null,
deptno)) deptno,
ename
from emp
使用窗口函数 LAG OVER 为每一行返回前一行的 DEPTNO。
SELECT
lag(deptno)over(ORDER BY deptno),
deptno, ename
FROM emp;
---------------------------------------------------
lag(deptno)over(order by deptno) deptno ename
-------------------------------- ------ --------
(NULL) 10 MILLER
10 10 CLARK
10 10 KING
10 20 SMITH
20 20 ADAMS
20 20 SCOTT
20 20 FORD
20 20 JONES
20 30 MARTIN
30 30 TURNER
30 30 BLAKE
30 30 ALLEN
30 30 JAMES
30 30 WARD
上述的结果集,很容易区分出哪些行的DEPTNO和LAG_DEPTNO相等,对于这些行,我们希希望把DEPTNO显示为NULL。借助DEOCDE韩式实现。
12.6 变换结果集以实现跨行计算
将列都转换为行,这样需要的所有值都会出现在同一个行里。
例如,DEPTNO 20是工资总额最高的部门,
SELECT deptno, SUM(sal) AS sal
FROM emp
GROUP BY deptno;
-----------------------------
deptno sal
------ --------
10 8750
20 10875
30 9400
计算上述DEPTNO 20和DEPTNO 10 之间的工资总额的差值,以及上述DEPTNO 20和 DEPTNO 20之间的工资总额差值。
解决问题
使用聚合函数 SUM和CASE表达式计算工资总额,然后在SELECT列表做出差值计算。
12.7 创建固定大小的数据桶
把数据放入若干个大小固定的桶里,每个桶的元素的个数是事先定好的。桶的个数可能是不确定的,但希望确保每个桶有5个元素。例如,希望基于EMPNO值为EMP表里的员工分组,一组最多5人。结果集如下
GRP EMPNO ENAME
--- ---------- -------
1 7369 SMITH
1 7499 ALLEN
1 7521 WARD
1 7566 JONES
1 7654 MARTIN
2 7698 BLAKE
2 7782 CLARK
2 7788 SCOTT
2 7839 KING
2 7844 TURNER
3 7876 ADAMS
3 7900 JAMES
3 7902 FORD
3 7934 MILLE
解决方案
对于提供了排名函数的数据库而言。为每一行数据生成一个序号之后,创建含有5个元素的桶的问题就变成了简单的除法问题。做过除法之后,我们只需针对商值向上取整即可。
DB2 Oracle SQL Server
按照EMPNO排序后,窗口函数ROW_NUMBER OVER为每一行分配一个排名或“行号”。
SELECT CEIL(row_number() over(ORDER BY empno) / 5.0) grp, empno, ename
FROM emp;
SELECT row_number() over(ORDER BY empno) rn,
row_number() over(ORDER BY empno) / 5.0 division,
CEIL(row_number() over(ORDER BY empno) / 5.0) grp,
empno,
ename
FROM emp;
----------------------------------------
rn division grp empno ename
------ -------- ------ ------ --------
1 0.2000 1 7369 SMITH
2 0.4000 1 7499 ALLEN
3 0.6000 1 7521 WARD
4 0.8000 1 7566 JONES
5 1.0000 1 7654 MARTIN
6 1.2000 2 7698 BLAKE
7 1.4000 2 7782 CLARK
8 1.6000 2 7788 SCOTT
9 1.8000 2 7839 KING
10 2.0000 2 7844 TURNER
11 2.2000 3 7876 ADAMS
12 2.4000 3 7900 JAMES
13 2.6000 3 7902 FORD
14 2.8000 3 7934 MILLER
PostgreSQL MySQL
借助标量子查询基于EMPNO为每一行生成一个序号。
SELECT
(SELECT
COUNT(*)
FROM
emp d
WHERE e.empno < d.empno) + 1 AS rnk,
e.empno,
e.ename
FROM
emp e
ORDER BY 1 ;
12.8 创建预定数目的桶
把EMP表里的员工分别放入到4个桶中,结果集如下.(需要创建固定数目的桶)
GRP EMPNO ENAME
--- ----- ---------
1 7369 SMITH
1 7499 ALLEN
1 7521 WARD
1 7566 JONES
2 7654 MARTIN
2 7698 BLAKE
2 7782 CLARK
2 7788 SCOTT
3 7839 KING
3 7844 TURNER
3 7876 ADAMS
4 7900 JAMES
4 7902 FORD
4 7934 MILLER
解决方案
对应那些提供了专有函数帮助我们创建“桶”的数据库而言,很容易解决本问题。但是,如果数据库不提供这类函数,则只好为每一行生成一个序号,然后针对该虚高和N执行模运算以决定把某一行放入哪个桶,此处的n代表我们希望创建桶的个数。
借助窗口函数NTILE创建数据固定的桶,NTILE负责把排序号的集合分别放入到指定数目的桶里去,每一个元素必然被分配到某个桶中。
使用窗口函数ROW_NUMBER
SELECT MOD(row_number() over(ORDER BY empno), 4) + 1 grp, empno, ename
FROM emp
ORDER BY 1
使用窗口函数 NTILE创建4个桶
select ntile(4) over(order by empno) grp, empno, ename from emp
MySQL和PostgreSQL
使用自连接基于EMPNO为每一行生成一个序号,然后针对该序号和4执行模运算以创建桶。
select mod(count(*), 4) + 1 as grp, e.empno, e.ename
from emp e, emp d
where e.empno >= d.empno
group by e.empno, e.ename
order by 1
12.9 创建水平直方图
用SQL创建水平直方图,例如,希望以水平直方图的形式显示每个部门的员工人数,用一个“*”表示一个员工。希望返回如下结果集
DEPTNO CNT
------ ----------
10 ***
20 *****
30 ******
解决方案
解决本问题的关键是使用聚合函数COUNT 和GROUP BY DEPTNO计算每个DEPTNO对应的员工人数。然后,把COUNT的返回值传递给字符串函数以生成一系列的“*”字符。
Oracle PostgreSQL MySQL
SELECT deptno, LPAD('*', COUNT(*), '*') AS cnt FROM emp GROUP BY deptno
12.11 返回非分组列
执行GROUP BY查询,并希望通过SELECT列表返回一些列,但这些列却不会出现在GROUP BY子句中。这通常无法办到,因为不能保证这些列在每个分组都有唯一的值。
假如希望找出每个部门工资最高和最低的员工,同时也希望找出每个职位对应的工资最高和最低的员工。希望返回如下结果
deptno ename job sal dept_status job_status
------ ------ --------- ------ --------------- ----------------
20 FORD ANALYST 3000 TOP SAL IN DEPT TOP SAL IN JOB
20 SCOTT ANALYST 3000 TOP SAL IN DEPT TOP SAL IN JOB
20 SMITH CLERK 800 LOW SAL IN DEPT LOW SAL IN JOB
30 JAMES CLERK 950 LOW SAL IN DEPT (NULL)
10 MILLER CLERK 1300 LOW SAL IN DEPT TOP SAL IN JOB
10 CLARK MANAGER 2450 (NULL) LOW SAL IN JOB
20 JONES MANAGER 2975 (NULL) TOP SAL IN JOB
30 BLAKE MANAGER 2850 TOP SAL IN DEPT (NULL)
10 KING PRESIDENT 5000 TOP SAL IN DEPT TOP SAL IN JOB
30 WARD SALESMAN 1250 (NULL) LOW SAL IN JOB
30 MARTIN SALESMAN 1250 (NULL) LOW SAL IN JOB
30 ALLEN SALESMAN 1600 (NULL) TOP SAL IN JOB
如果将上述所有列放入SELECT子句,会破坏分组操作。
SELECT ename,MAX(sal)
FROM emp
GROUP BY ename
上述查询返回EMP表的全部14行数据。
解决方案
使用内嵌试图找出每个DEPTNO和JOB对应的最高和最低的工资。然后筛选出工资与之匹配的行。
Oracle DB2 SQL Server
使用窗口函数MAX OVER和MIN OVER找出每个DEPTNO 和JOB对应的最高和最低的工资,然后,筛选出工资与之匹配的行
SELECT deptno,
ename,
job,
sal,
CASE WHEN sal = max_by_dept THEN 'TOP SAL IN DEPT' WHEN sal = min_by_dept THEN
'LOW SAL IN DEPT' END dept_status,
CASE WHEN sal = max_by_job THEN 'TOP SAL IN JOB' WHEN sal = min_by_job THEN
'LOW SAL IN JOB' END job_status
FROM (SELECT deptno,
ename,
job,
sal,
MAX(sal) over(PARTITION BY deptno) max_by_dept,
MAX(sal) over(PARTITION BY job) max_by_job,
MIN(sal) over(PARTITION BY deptno) min_by_dept,
MIN(sal) over(PARTITION BY job) min_by_job
FROM emp) emp_sals
WHERE sal IN (max_by_dept, max_by_job, min_by_dept, min_by_job)
PostgreSQL MySQL
使用标量子查询找出每个DEPTNO和JOB对应的最高和最低的工资。然后,只保留与之匹配的员工。
select deptno,
ename,
job,
sal,
case when sal = max_by_dept then 'TOP SAL IN DEPT' when sal = min_by_dept then 'LOW SAL IN DEPT' end as dept_status,
case when sal = max_by_job then 'TOP SAL IN JOB' when sal = min_by_job then 'LOW SAL IN JOB' end as job_status
from (select e.deptno,
e.ename,
e.job,
e.sal,
(select max(sal) from emp d where d.deptno = e.deptno) as max_by_dept,
(select max(sal) from emp d where d.job = e.job) as max_by_job,
(select min(sal) from emp d where d.deptno = e.deptno) as min_by_dept,
(select min(sal) from emp d where d.job = e.job) as min_by_job
from emp e) x
where sal in (max_by_dept, max_by_job, min_by_dept, min_by_job)
12.12 计算简单的小计
例如,一个结果集既包括了EMP表各个JOB对应的工资合计值,也包括了全部工资的总计。如下
JOB SAL
--------- ----------
ANALYST 6000
CLERK 4150
MANAGER 8275
PRESIDENT 5000
SALESMAN 5600
TOTAL 29025
解决方案
针对GROUP BY子句的ROLLUP扩展完美地解决了本问题。对于不支持ROLLUP的数据库,可以借助标量子查询或UNION查询解决本问题。
DB2 Oracle
使用聚合函数SUM计算工资合计值,并使用GROUP BY的ROLLUP扩展构造出同时包含小计和总计的结果集
SELECT CASE grouping(job)
WHEN 0 THEN
job
ELSE
'TOTAL'
END job,
SUM(sal) sal
FROM emp
GROUP BY ROLLUP(job)
MariaDB
https://mariadb.com/kb/en/select-with-rollup/
SELECT coalesce(job,'TOTAL') job,
SUM(sal) sal
FROM emp
GROUP BY job WITH ROLLUP;
12.13 计算所有可能的表达式组合的小计
按照DEPTNO JOB 以及JOB/DEPTNO组合分别计算出工资合计值。同时也希望得到EMP表的工资总计,
解决方案
对应GROUP BY语法的扩展使得本问题变得容易多了。对应那些尚不支持这一类语法扩展的数据库,就必须用(通过自连接和多个标量子查询)手动计算多种层次的小计。
