Oracle 报表与数据仓库中的运算

11.1 行转列

“行转列” 在做报表或语句改写时要经常用到,是个非常重要的语句。在Oracle 中CASE WHEN END 以及新增的PIVOT函数。其中CASE WHEN END 编写及维护较麻烦,但适合的场景较多。PIVOT编写及维护简单但有较大的限制。以下简单介绍一下这两种行转列的方法:

首先看下最常用的CASE WHEN END的方法:

根据不同的条件来取值就可以把数据分为几列。

SELECT job as 职位,
       case deptno
         when 10 then
          sal
       end as 部门10工资,
       case deptno
         when 20 then
          sal
       end as 部门20工资,
       case deptno
         when 30 then
          sal
       end as 部门30工资,
       sal as 合计工资
  FROM emp
 order by 1;

 

上图中看上去杂乱无章,需要再按job 分组汇总,所以一般“行转列”语句里都会有聚集函数,就是为了把同类数据转换为一行显示。另外注意最后一列,我们增加了合计工资的显示,这在PIVOT函数里是做不到的,PIVOT函数只能按同一个规则分类各数据,各列之间的数据不能交叉重复。

SELECT job as 职位,
       SUM(CASE DEPTNO
             WHEN 10 THEN
              SAL
           END) AS 部门10工资,
       SUM(CASE DEPTNO
             WHEN 20 THEN
              SAL
           END) AS 部门20工资,
       SUM(CASE DEPTNO
             WHEN 30 THEN
              SAL
           END) AS 部门30工资,
       SUM(SAL) AS 合计工资
  FROM EMP
 GROUP BY job
 ORDER BY 1;

接下来讲  Oracle 11g 新增的“行转列”函数PIVOT,对于简单的环境PIVOT 提供了简单的实现方法。

SELECT *
  FROM (SELECT job,/*该列未在PIVOT里面,所以被当作分组条件*/
               sal, 
               deptno 
               from emp)
pivot(sum(sal) as s/*SUM,MAX等聚合函数+列别名,若不设置则默认只使用后面IN 里设的别名,否则两个别名相加*/
   for deptno in(10 as d10,/*相当于case when deptno = 10 then sal end) as d10,别名与前面的别名合并后为D10_S*/
                 20 as d20,/*相当于case when deptno = 20 then sal end) as d20,别名与前面的别名合并后为D20_S*/
                 30 as d30))/*相当于case when deptno = 30 then sal end) as d30,别名与前面的别名合并后为D30_S*/
 order by 1;

两种方式对比,倘若需要增添提成的返回,用PIVOT则只需要增加一个设定即可。

SELECT *
FROM (SELECT job,sal,comm,deptno
        from emp)
pivot(sum(sal)as s,sum(comm) as c
      for deptno
        in (10 as d10,20 as d20,30 as d30))
order by 1;

而用CASE WHEN 要增加三行语句。

SELECT job,
       sum(case when deptno = 10 then sal end) as d10_s,
       sum(case when deptno = 20 then sal end) as d20_s,
       sum(case when deptno = 30 then sal end) as d30_s,
       sum(case when deptno = 10 then comm end) as d10_c,
       sum(case when deptno = 20 then comm end) as d20_c,
       sum(case when deptno = 30 then comm end) as d30_c
   from emp
   GROUP BY job
   order by 1;

PIVOT 一次只能按一个条件来“行转列",如果同时把职位与部门都转为列并汇总为一行时,PIVOT就无能为力了,这时就只能用CASE WHEN。

SELECT sum(case when deptno = 10 then sal end) AS d10_s,
       sum(case when deptno = 20 then sal end) AS d20_s,
       sum(case when deptno = 30 then sal end) AS d30_s,
       sum(case when job = 'ANALYST' then sal end) AS ANALYST,
       sum(case when job = 'CLERK' then sal end) AS CLERK,
       sum(case when job = 'MANAGER' then sal end) AS MANAGER,
       sum(case when job = 'PRESIDENT' then sal end) AS PRESIDENT,
       sum(case when job = 'SALESMAN' then sal end) AS SALESMAN
  FROM emp;

11.2 列转行

CREATE OR REPLACE VIEW v AS 
SELECT *
  FROM (SELECT deptno, sal FROM emp)
pivot(COUNT(*) AS ct, SUM(sal) AS s
   FOR deptno IN(10 AS deptno_10, 20 AS deptno_20, 30 AS deptno_30));

  SELECT * FROM v;

要求把三个部门的人次转为一列上显示,以前这种需求我们一直用 UNION ALL来写

SELECT '10' AS 部门编码,DEPTNO_10_CT AS 人次 FROM V
UNION ALL
SELECT '20' AS 部门编码,DEPTNO_20_CT AS 人次 FROM V
UNION ALL
SELECT '30' AS 部门编码, DEPTNO_30_CT AS 人次 FROM V;

如果列数多了,这种查询编写与维护都比较麻烦,而用UNPIVOT就不一样了。

SELECT DEPTNO AS 列名,substr(deptno, -5, 2) AS 部门编码,人次
  FROM V UNPIVOT(人次 FOR deptno in(deptno_10_ct, deptno_20_ct, deptno_30_ct));

我们可以很容易的在后面的in列表里维护要转换的列,当然UNPIVOT同样有限制,如果同时有人次与工资合计要转换时,就不能一次性的写了。 只能分别转换后再JOIN

SELECT a.列名, a.部门编码, a.人次, b.工资
  FROM (SELECT substr(deptno, 1, 9) AS 列名,
               substr(deptno, -5, 2) AS 部门编码,
               人次
          FROM v unpivot include NULLS(人次 FOR deptno IN(deptno_10_ct,
                                                        deptno_20_ct,
                                                        deptno_30_ct))) a
 INNER JOIN (SELECT substr(deptno, 1, 9) AS 列名, 工资
               FROM v unpivot include NULLS(工资 FOR deptno IN(deptno_10_s,
                                                             deptno_20_s,
                                                             deptno_30_s))) b
    ON (b.列名 = a.列名);

这儿我们为了两个结果集一致,使用了参数INCLUDE NULLS,这样即使数据为空也显示一行。

11.3 将结果集反向转置为一列。

有时会要求数据竖向显示,如SMITH的数据如下显示,各行之间用空格隔开。 SMITH CLERK 800 我们使用刚学到的UNPIVOT再加一点小技巧就可以了。

SELECT EMPS
 FROM (SELECT ENAME,JOB,TO_CHAR(SAL) AS SAL,NULL AS T_COL /*增加这一列来显示空行*/FROM EMP)
 UNPIVOT INCLUDE NULLS/*增加这个参数把空值也转换为一行*/(EMPS FOR COL IN(ENAME,JOB,SAL,T_COL));

11.4 抑制结果集中的重复值

我们返回的数据中经常会有重值,如EMP.DEPTNO,这些数据经常要求合并显示。这种一般都在前台处理,偶尔也有特殊情况,需要查询返回时 就只显示第一行数据,那如何处理呢?我们用LAG进行判断即可

SELECT CASE
        /*当部门分类按姓名排序当与上一条内容相同时不显示*/
         WHEN LAG(DEPTNO) OVER(ORDER BY DEPTNO, ENAME) = DEPTNO THEN
          NULL
         ELSE
          DEPTNO
       END AS DEPTNO,
       ENAME
  FROM EMP
 ORDER BY EMP.DEPTNO, ENAME;

11.5 利用”行转列“进行计算。

“行转列”除了用于报表显示外,还可以利用转换后各值在同一行上的特点进行计算。如下面计算部门 20与10及部门20与30之间的总工资 差额

SELECT d10_sal,
       d20_sal,
       d30_sal,
       d20_sal - d10_sal as d20_10_diff,
       d20_sal - d30_sal as d20_30_diff
  from (SELECT SUM(CASE
                     WHEN deptno = 10 then
                      sal
                   end) as d10_sal,
               SUM(CASE
                     WHEN deptno = 20 then
                      sal
                   end) as d20_sal,
               SUM(CASE
                     WHEN deptno = 30 then
                      sal
                   end) as d30_sal
          FROM emp) totals_by_dept;

11.6 数据分组

公司要开展活动,需要把人员分组,每5个一组,用查询语句怎么实现?

/*然后编号除5,并用ceil返回不小于当前值的下一个整数就可以了*/

SELECT ceil(rn / 5) as 组,empno as 编码,ename as 姓名
  FROM (
        /*首先我们要给每位员工生成一个编号*/
        SELECT row_number() over(order by empno) as rn, empno, ename from emp);

11.7 创建预定数目的桶

活动需求变了,我们不管有多少人,只需要分成4组就可以了,那需要先计算总人数么?自从有了分析函数ntile以后就不用了。

SELECT ntile(4) over(ORDER BY empno) AS 组, empno as 编码, ename as 姓名
  from emp;

11.8 创建横向直方图

相对于报表数据,领导更喜欢看各式各样的图表。如对各部门的员工人数用“*”横向表示,每个“*”表示一位员工。 这个查询我们只需要把人数转换为对应个数的“*”就可以了

select deptno as 部门编号,lpad('*', count(*), '*') as 员工分布
  from emp
 group by deptno;

11.9 创建纵向直方图

上节讲了部门人数横向分布显示,那如果要纵向显示呢。我们需要先获得一个分组依据,不然出来的数据就是乱的。按人员名称生成一个序号, 再按这个序号分组就是一个不错的主意

11.10 标识最大最小值

·报表查询时有时会要求标识最大最小值,以前我们常用子查询关联的方式,这样就要对表多扫描一次。那有了分析函数就容易多了,我们可以用 分析函数提出相应的值,然后在返回的数据内做比较即可

select deptno,
       ename,
       job,
       sal,
       case
         when sal = max_by_dept THEN
          '部门内最高工资'
         when sal = min_by_dept THEN
          '部门内最低工资'
       END dept_status,
       CASE
         WHEN SAL = MAX_BY_JOB THEN
          '职内最高工资'
         WHEN SAL = MIN_BY_JOB THEN
          '职内最低工资'
       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);

11.11 计算简单的小计

常常生成报表数据时还要加一个总合计,必须要用UNION ALL来做? No,我们用rollup就可以达到这个目的。

SELECT CASE GROUPING(job)
         WHEN 0 THEN
          job
         else
          '总合计'
       END AS 职位,
       SUM(sal) AS 工资小计
  FROM emp
 GROUP BY ROLLUP(job);

ROLLUP,是GROUP BY子句的一种扩展,可以为每一个分组返回小计记录以及为所有分组返回总计记录。为了便于理解我们作个与UNION ALL 实例

 SELECT DEPTNO AS 部门编码,
        JOB AS 职位,
        to_char(hiredate, 'yyyy') as 年份,
        SUM(SAL)
   FROM emp
  group by rollup(deptno, job, to_char(hiredate, 'yyyy'));

 SELECT DEPTNO AS 部门编码,Job as 职位,
        TO_CHAR(hiredate, 'yyyy') as 年份,SUM(SAL)
   FROM EMP
  GROUP BY deptno, job, to_char(hiredate, 'yyyy')
 UNION ALL
 SELECT DEPTNO AS 部门编码,
        job as 职位,
        NULL /*职位小讲*/ as 年份,
        sum(sal)
   from emp
  group by deptno, job
 union all
 SELECT DEPTNO AS 部门编码,NULL /*部门小计*/ as 职位,
        NULL as 年份,
        sum(sal)
   from emp
  group by deptno
 union all
 select null /*总合计*/, '' as 职位, '' as y, sum(sal)
   from emp;

发布了39 篇原创文章 · 获赞 4 · 访问量 3487

猜你喜欢

转载自blog.csdn.net/u011868279/article/details/100186133
今日推荐