0


【SQL开发实战技巧】系列(二十四):数仓报表场景☞通过执行计划详解”行转列”,”列转行”是如何实现的

系列文章目录

【SQL开发实战技巧】系列(一):关于SQL不得不说的那些事
【SQL开发实战技巧】系列(二):简单单表查询
【SQL开发实战技巧】系列(三):SQL排序的那些事
【SQL开发实战技巧】系列(四):从执行计划讨论UNION ALL与空字符串&UNION与OR的使用注意事项
【SQL开发实战技巧】系列(五):从执行计划看IN、EXISTS 和 INNER JOIN效率,我们要分场景不要死记网上结论
【SQL开发实战技巧】系列(六):从执行计划看NOT IN、NOT EXISTS 和 LEFT JOIN效率,记住内外关联条件不要乱放
【SQL开发实战技巧】系列(七):从有重复数据前提下如何比较出两个表中的差异数据及对应条数聊起
【SQL开发实战技巧】系列(八):聊聊如何插入数据时比约束更灵活的限制数据插入以及怎么一个insert语句同时插入多张表
【SQL开发实战技巧】系列(九):一个update误把其他列数据更新成空了?Merge改写update!给你五种删除重复数据的写法!
【SQL开发实战技巧】系列(十):从拆分字符串、替换字符串以及统计字符串出现次数说起
【SQL开发实战技巧】系列(十一):拿几个案例讲讲translate|regexp_replace|listagg|wmsys.wm_concat|substr|regexp_substr常用函数
【SQL开发实战技巧】系列(十二):三问(如何对字符串字母去重后按字母顺序排列字符串?如何识别哪些字符串中包含数字?如何将分隔数据转换为多值IN列表?)
【SQL开发实战技巧】系列(十三):讨论一下常用聚集函数&通过执行计划看sum()over()对员工工资进行累加
【SQL开发实战技巧】系列(十四):计算消费后的余额&计算银行流水累计和&计算各部门工资排名前三位的员工
【SQL开发实战技巧】系列(十五):查找最值所在行数据信息及快速计算总和百之max/min() keep() over()、fisrt_value、last_value、ratio_to_report
【SQL开发实战技巧】系列(十六):数据仓库中时间类型操作(初级)日、月、年、时、分、秒之差及时间间隔计算
【SQL开发实战技巧】系列(十七):数据仓库中时间类型操作(初级)确定两个日期之间的工作天数、计算—年中周内各日期出现次数、确定当前记录和下一条记录之间相差的天数
【SQL开发实战技巧】系列(十八):数据仓库中时间类型操作(进阶)INTERVAL、EXTRACT以及如何确定一年是否为闰年及周的计算
【SQL开发实战技巧】系列(十九):数据仓库中时间类型操作(进阶)如何一个SQL打印当月或一年的日历?如何确定某月内第一个和最后—个周内某天的日期?
【SQL开发实战技巧】系列(二十):数据仓库中时间类型操作(进阶)获取季度开始结束时间以及如何统计非连续性时间的数据
【SQL开发实战技巧】系列(二十一):数据仓库中时间类型操作(进阶)识别重叠的日期范围,按指定10分钟时间间隔汇总数据
【SQL开发实战技巧】系列(二十二):数仓报表场景☞ 从分析函数效率一定快吗聊一聊结果集分页和隔行抽样实现方式
【SQL开发实战技巧】系列(二十三):数仓报表场景☞ 如何对数据排列组合去重以及通过如何找到包含最大值和最小值的记录这个问题再次用执行计划给你证明分析函数性能不一定高
【SQL开发实战技巧】系列(二十四):数仓报表场景☞通过案例执行计划详解”行转列”,”列转行”是如何实现的
【SQL开发实战技巧】系列(二十五):数仓报表场景☞结果集中的重复数据只显示一次以及计算部门薪资差异高效的写法以及如何对数据进行快速分组
【SQL开发实战技巧】系列(二十六):数仓报表场景☞聊聊ROLLUP、UNION ALL是如何分别做分组合计的以及如何识别哪些行是做汇总的结果行


文章目录


前言

本篇文章讲解的主要内容是:目前Oracle支持的行列互换有两种方式:case when、pivot\unpivot,我将通过几个案例来给大家详解如何通过这两种方式实现“行转列”,“列转行”的需求,并通过执行计划看case when、pivot\unpivot二者的底层逻辑关系以及效率上的影响。
【SQL开发实战技巧】这一系列博主当作复习旧知识来进行写作,毕竟SQL开发在数据分析场景非常重要且基础,面试也会经常问SQL开发和调优经验,相信当我写完这一系列文章,也能再有所收获,未来面对SQL面试也能游刃有余~。


一、行转列

"行转列"的这个写法,在做报表或语句改写时要经常用到,是一个非常重要的语句。
在Oracle中,有

CASE WHEN END

和Oracle11g新增的pivot函数两种方式。其中:

  • CASE WHEN END编写和维护较麻烦,但适合的场景较多。
  • PIVOT编写和维护简单,但有较大限制。

下面简单介绍这两种方法。
现在我有这么一个需求:
对emp表按job分组汇总,每个部门显示为一列。首先看一下最常用的

CASE WHEN END

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

SQL>select a.job,2casewhen a.deptno=10then sal endas d10,3casewhen a.deptno=20then sal endas d20,4casewhen a.deptno=30then sal endas d30
  5from emp a
  6orderby1;

JOB              D10        D20        D30
--------- ---------- ---------- ----------
ANALYST                    3000 
CLERK                       800 
CLERK                                  950
CLERK                      1100 
MANAGER         2450            
MANAGER                    2975 
MANAGER                               2850
MGR                             
PRESIDENT       5000            
SALESMAN                              1250
SALESMAN                              1600
SALESMAN                              1250
SALESMAN                              1500
sdf             130017rows selected

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

SQL>select a.job,2sum(casewhen a.deptno=10then sal end)as d10,3sum(casewhen a.deptno=20then sal end)as d20,4sum(casewhen a.deptno=30then sal end)as d30,5sum(sal)as sm
  6from emp a
  7groupby job
  8orderby1;

JOB              D10        D20        D30         SM
--------- ---------- ---------- ---------- ----------
ANALYST                    30003000
CLERK                      19009502850
MANAGER         2450297528508275
MGR                                        
PRESIDENT       50005000
SALESMAN                              56005600
sdf             130013008rows selected

下面看一下Oracle11g新增的"行转列"函数PIVOT,对简单的PIVOT环境提供了简单的实现方法。

SQL>SQL>select*from(2--先查询出来要进行操作的数据3select job,sal,deptno from emp
  4)pivot(5sum(sal)as sm /*这里写聚集函数以及起别名,SUM、MAX等聚集函数+列别名,若不设置别名,则后面生成的列名字默认只使用后面in里设的别名,否则两个别名相加*/6for deptno in(710as d10,/*这里等价于前面的查询:sum(case when deptno =10 then sal end) as d10,列名称为d10_sm,这个sm是前面聚集函数的别名*/820as d20,/*这里等价于前面的查询:sum(case when deptno =20 then sal end) as d20*/930as d30/*这里等价于前面的查询:sum(case when deptno =30 then sal end) as d30*/10)11)12orderby1;

JOB           D10_SM     D20_SM     D30_SM
--------- ---------- ---------- ----------
ANALYST                    3000 
CLERK                      1900950
MANAGER         245029752850
MGR                             
PRESIDENT       5000            
SALESMAN                              5600
sdf             13008rows selected

大家可以看一下两种方式的对比,如果还要增加提成的返回,用PIVOT则只需要增加一个设定即可。

SQL>select*from(2--先查询出来要进行操作的数据3select job,sal,comm,deptno from emp
  4)pivot(5sum(sal)as sm,/*这里写聚集函数以及起别名,SUM、MAX等聚集函数+列别名,若不设置别名,则后面生成的列名字默认只使用后面in里设的别名,否则两个别名相加*/6sum(comm)as sc /*这里写聚集函数以及起别名,SUM、MAX等聚集函数+列别名,若不设置别名,则后面生成的列名字默认只使用后面in里设的别名,否则两个别名相加*/7for deptno in(810as d10,/*这里等价于前面的查询:sum(case when deptno =10 then sal end) as d10,sum(case when deptno =10 then comm end) as d10,列名称为d10_sc,这个sm是前面聚集函数的别名*/920as d20,/*这里等价于前面的查询:sum(case when deptno =20 then sal end) as d20*/1030as d30/*这里等价于前面的查询:sum(case when deptno =30 then sal end) as d30*/11)12)13orderby1;

JOB           D10_SM     D10_SC     D20_SM     D20_SC     D30_SM     D30_SC
--------- ---------- ---------- ---------- ---------- ---------- ----------
ANALYST                               3000                       
CLERK                                 1900950 
MANAGER         245029752850 
MGR                                                              
PRESIDENT       5000                                             
SALESMAN                                                    56002200
sdf             13008rows selected

如果用CASE WHEN要增加三行语句。

SQL>select a.job,2sum(casewhen a.deptno=10then sal end)as d10_sm,3sum(casewhen a.deptno=10then sal end)as d10_sc,4sum(casewhen a.deptno=20then sal end)as d20_sm,5sum(casewhen a.deptno=20then sal end)as d20_sc,6sum(casewhen a.deptno=30then sal end)as d30_sm,7sum(casewhen a.deptno=30then sal end)as d30_sc
  8from emp a
  9groupby job
 10orderby1;

JOB           D10_SM     D10_SC     D20_SM     D20_SC     D30_SM     D30_SC
--------- ---------- ---------- ---------- ---------- ---------- ----------
ANALYST                               30003000            
CLERK                                 19001900950950
MANAGER         245024502975297528502850
MGR                                                              
PRESIDENT       50005000                                  
SALESMAN                                                    56005600
sdf             130013008rows selected

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

SQL>select2casewhen deptno=10then ename endas d10,3casewhen deptno=20then ename endas d20,4casewhen deptno=30then ename endas d30,5casewhen job='ANALYST'then ename endas ANALYST,6casewhen job='CLERK'then ename endas CLERK,7casewhen job='MANAGER'then ename endas MANAGER,8casewhen job='MGR'then ename endas MGR,9casewhen job='PRESIDENT'then ename endas PRESIDENT,10casewhen job='SALESMAN'then ename endas SALESMAN
 11from emp;

D10        D20        D30        ANALYST    CLERK      MANAGER    MGR        PRESIDENT  SALESMAN
---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ----------
           zhaoyd                                                 zhaoyd                
           er                               er                                          
                      ALLEN                                                             ALLEN
                      WARD                                                              WARD
           JONES                                       JONES                            
                      MARTIN                                                            MARTIN
                      BLAKE                            BLAKE                            
CLARK                                                  CLARK                            
KING                                                                         KING       
                      TURNER                                                            TURNER
           ADAMS                            ADAMS                                       
                      JAMES                 JAMES                                       
           FORD                  FORD                                                   
sdf                                                                                     

17rows selected

SQL>

最后分析一下PIVOT的PLAN(用

dbms_xplan.display_cursor

看):

select/*zydtest*/*from(--先查询出来要进行操作的数据select job,sal,comm,deptno from emp
)pivot(sum(sal)as sm /*这里写聚集函数以及起别名,SUM、MAX等聚集函数+列别名,若不设置别名,则后面生成的列名字默认只使用后面in里设的别名,否则两个别名相加*/for deptno in(10as d10,/*这里等价于前面的查询:sum(case when deptno =10 then sal end) as d10,sum(case when deptno =10 then comm end) as d10,列名称为d10_sc,这个sm是前面聚集函数的别名*/20as d20,/*这里等价于前面的查询:sum(case when deptno =20 then sal end) as d20*/30as d30 /*这里等价于前面的查询:sum(case when deptno =30 then sal end) as d30*/))orderby1;select*from v$sql aa where aa.SQL_TEXT like'%zydtest%';SQL>select*fromtable(dbms_xplan.display_cursor('5ss3y129x7p0a',0,'advanced'));  

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------
SQL_ID  5ss3y129x7p0a, child number 0-------------------------------------select/*zydtest*/*from(--�������������������������� select
job,sal,comm,deptno from emp )pivot(sum(sal)as sm
/*��������������������������SUM��MAX������������������,������������,����
����������������������������in����������,����������������*/for deptno
in(10as d10,/*����������������������sum(case when deptno =10 then
sal end) as d10,sum(case when deptno =10 then comm end) as
d10,��������d10_sc������sm��������������������*/20as
d20,/*����������������������sum(case when deptno =20 then sal end) as
d20*/30as d30 /*��������
Plan hash value: 1018027214
----------------------------------------------------------------------------
| Id  | Operation           | Name | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |      |       |       |     4 (100)|          |
|   1 |  SORT GROUP BY PIVOT|      |    19 |   285 |     4  (25)| 00:00:01 |
|   2 |   TABLE ACCESS FULL | EMP  |    19 |   285 |     3   (0)| 00:00:01 |

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------
----------------------------------------------------------------------------
Query Block Name / Object Alias (identified by operation id):
-------------------------------------------------------------
   1 - SEL$117FC0EF
   2 - SEL$117FC0EF / EMP@SEL$2
Outline Data
-------------
  /*+
      BEGIN_OUTLINE_DATA
      IGNORE_OPTIM_EMBEDDED_HINTS
      OPTIMIZER_FEATURES_ENABLE('11.2.0.4')
      DB_VERSION('11.2.0.4')
      ALL_ROWS
      OUTLINE_LEAF(@"SEL$117FC0EF")
      MERGE(@"SEL$F5BB74E1")
      OUTLINE(@"SEL$3")
      OUTLINE(@"SEL$F5BB74E1")

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------
      MERGE(@"SEL$2")
      OUTLINE(@"SEL$1")
      OUTLINE(@"SEL$2")
      FULL(@"SEL$117FC0EF" "EMP"@"SEL$2")
      END_OUTLINE_DATA
  */Column Projection Information (identified by operation id):
-----------------------------------------------------------1-(#keys=2) "JOB"[VARCHAR2,9], "COMM"[NUMBER,22], SUM(CASE  WHEN("DEPTNO"=10)THEN"SAL"END)[22],SUM(CASEWHEN("DEPTNO"=20)THEN"SAL"END)[22],SUM(CASEWHEN("DEPTNO"=30)THEN"SAL"END)[22]2-"JOB"[VARCHAR2,9],"SAL"[NUMBER,22],"COMM"[NUMBER,22],"DEPTNO"[NUMBER,22]57rows selected

通过上面PLAN可以看到,PIVOT被转换成了如下语句:

SUM(CASEWHEN("DEPTNO"=10)THEN"SAL"END)[22],SUM(CASEWHEN("DEPTNO"=20)THEN"SAL"END)[22],SUM(CASEWHEN("DEPTNO"=30)THEN"SAL"END)[22]

也就是说,PIVOT只是写法简单了一些,实际上仍用的是CASE WHEN语句。

二、列转行

测试数据如下:

droptable test purge;createtable test asselect*from(select deptno, sal from emp)pivot(count(*)as ct,sum(sal)as s
   for deptno in(10as d10,20as d20,30as d30));SQL>select*from test;

    D10_CT      D10_S     D20_CT      D20_S     D30_CT      D30_S
---------- ---------- ---------- ---------- ---------- ----------387505787569400SQL>

要求把三个部门的"人次"转为一列显示。以前这种需求一直用UNION ALL来写:

SQL>SELECT'10'AS 部门编码,d10_ct AS 人次 FROM test UNIONALL2SELECT'20'AS 部门编码,d20_ct AS 人次 FROM test UNIONALL3SELECT'30'AS 部门编码,d30_ct AS 人次 FROM test
  4;

部门编码       人次
---------- ----------103205306

这时PLAN如下:

PlanHashValue  : 2176849128---------------------------------------------------------------------| Id | Operation            | Name |Rows| Bytes | Cost |Time|---------------------------------------------------------------------|0|SELECT STATEMENT     ||3|39|9|00:00:01||1|UNION-ALL|||||||2|TABLE ACCESS FULL| TEST |1|13|3|00:00:01||3|TABLE ACCESS FULL| TEST |1|13|3|00:00:01||4|TABLE ACCESS FULL| TEST |1|13|3|00:00:01|---------------------------------------------------------------------

Note
------ dynamic sampling used for this statement

需要扫描test三次,而且如果列数较多,这种查询编写与维护都比较麻烦,而用UNPIVOT就不一样了。

SQL>SQL>select*from test unpivot(2  ct for deptno in(d10_ct,d20_ct,d30_ct)3);

     D10_S      D20_S      D30_S DEPTNO         CT
---------- ---------- ---------- ------ ----------875078759400 D10_CT          3875078759400 D20_CT          5875078759400 D30_CT          6SQL>

UNPIYOT函数生成两个新列:“deptno"与"ct”。

而in()中的D10_CT、D20_CT和D30_CT三列,其列名成为行"deptno"的值,原来D10_CT等列中的值分别转为"ct"列中的三行:
那怎么恢复成原来deptno号的样子?只要将语句整理如下:

SQL>select deptno AS lm,substr(deptno,-5,2)AS deptno,ct
  2from test unpivot(3  ct for deptno in(d10_ct,d20_ct,d30_ct)4);

LM     DEPTNO           CT
------ -------- ----------
D10_CT 103
D20_CT 205
D30_CT 306

这时PLAN如下:
 PlanHashValue  : 734873962-----------------------------------------------------------------------| Id  | Operation             | Name |Rows| Bytes | Cost |Time|-----------------------------------------------------------------------|0|SELECT STATEMENT      ||3|63|9|00:00:01||*1|VIEW||3|63|9|00:00:01||2|UNPIVOT|||||||3|TABLE ACCESS FULL| TEST |1|39|3|00:00:01|-----------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------*1- filter("unpivot_view_006"."CT"ISNOTNULL)

Note
------ dynamic sampling used for this statement

可以看到,与PIVOT不一样,UNPIVOT不仅语句简略,而且只需要扫描test一次。我们可以很容易地在后面的unpivot列表里维护要转换的列。当然,UNPIVOT同样有限制。
如果现在有这么一个需求:还是用前面的表test:

SQL>select*from test;

    D10_CT      D10_S     D20_CT      D20_S     D30_CT      D30_S
---------- ---------- ---------- ---------- ---------- ----------387505787569400

我们要求将人次和sal聚合的结果恢复成按照部门统计的行列表,也就是下面这样的:

 DEPTNO           RC          SM
 -------- ----------  ----------103875020578753069400

那我们该怎么实现?
要记得如果同时有人次与工资合计要转换,unpivot就不能一次性完成,只有分别转换后再用JOIN连接。

select*from(select substr(deptno,1,3)as lm,
               substr(deptno,-5,2)AS deptno,
               rc
          from(select*from test unpivot include nulls(rc for deptno in(d10_ct, d20_ct, d30_ct)))) a
 innerjoin(select substr(deptno,1,3)as lm,
                    substr(deptno,-4,2)AS deptno,
                    sm
               from(select*from test unpivot include nulls(sm for deptno in(d10_s,
                                                          d20_s,
                                                          d30_s)))) b
    on a.lm = b.lm

LM           DEPTNO           RC LM           DEPTNO           SM
------------ -------- ---------- ------------ -------- ----------
D10          103 D10          108750
D20          205 D20          207875
D30          306 D30          309400

上面的结果只要去掉几个列名就实现了前面的要求。这里为了让两个结果集一致,使用了参数

include nulls

这样即使数据为空,也显示一行。
是否有办法只用UNPIVOT,而不用JOIN呢?看下面的示例:

SQL>select*2from test
  3unpivot include nulls(rc for deptno in(d10_ct as10, d20_ct as20, d30_ct as30))4unpivot include nulls(sm for deptno2 in(d10_s as10, d20_s as20, d30_s as30));

    DEPTNO         RC    DEPTNO2         SM
---------- ---------- ---------- ----------1031087501032078751033094002051087502052078752053094003061087503062078753063094009rows selected

可以看到,当有两个UNPJVOT时,生成的结果是一个笛卡儿积。
上面的语句实际上就是一个嵌套语句,前一个UNPIVOT结果出来后,再执行另一个

SQL>with t as(2select*3from test
  4unpivot include nulls(rc for deptno in(d10_ct as10, d20_ct as20, d30_ct as30))5)6select*from t unpivot include nulls(sm for deptno2 in(d10_s as10, d20_s as20, d30_s as30));

    DEPTNO         RC    DEPTNO2         SM
---------- ---------- ---------- ----------1031087501032078751033094002051087502052078752053094003061087503062078753063094009rows selected

回来继续说,那上面这样的数据就不能用了吗?必须用join吗?
不不不,既然他笛卡尔积了,其实针对需要的数据,在上面的查询上加一个过滤即可。

SQL>select*2from test
  3unpivot include nulls(rc for deptno in(d10_ct as10, d20_ct as20, d30_ct as30))4unpivot include nulls(sm for deptno2 in(d10_s as10, d20_s as20, d30_s as30))5where deptno=deptno2;

    DEPTNO         RC    DEPTNO2         SM
---------- ---------- ---------- ----------103108750205207875306309400SQL>

三、将结果集反向转置为一列

有时会要求数据竖向显示,如CLARK的数据显示如下(各行之间用空格隔开):

CLARK
MANAGER
2450

我们使用刚学到的UNPIVOT,再加一点小技巧就可以。

select emps from(select ename, job, to_char(sal)as sal,nullas t_col
  from emp
 where deptno =10)unpivot include nulls(emps for aa in(ename,job,sal,t_col));
EMPS
------
CLARK
MANAGER
2450

KING
PRESIDENT
5000

sdf
sdf
1300

这里要注意以下两点。

  1. 与UNION ALL一样,要合并的几列数据类型必须相同,如果sal不用to_char转换,就会报错:
SQL>SQL>select emps from(2select ename, job,  sal  as sal,nullas t_col
  3from emp
  4where deptno =10)5unpivot include nulls(emps for aa in(ename,job,sal,t_col));select emps from(select ename, job,  sal  as sal,nullas t_col
  from emp
 where deptno =10)unpivot include nulls(emps for aa in(ename,job,sal,t_col))

ORA-01790: expression must have same datatype as corresponding expression
  1. 如果不加include nu11s,将不会显示空行:
select emps from(select ename, job,  to_Char(sal)as sal,nullas t_col
  from emp
 where deptno =10)unpivot(emps for aa in(ename,job,sal,t_col));
EMPS
------
CLARK
MANAGER
2450
KING
PRESIDENT
5000
sdf
sdf
1300

总结

本篇文章讲解的主要内容是:目前Oracle支持的行列互换有两种方式:case when、pivot\unpivot,我将通过几个案例来给大家详解如何通过这两种方式实现“行转列”,“列转行”的需求,并通过执行计划看case when、pivot\unpivot二者的底层逻辑关系以及效率上的影响。

标签: sql 行转列 列转行

本文转载自: https://blog.csdn.net/qq_28356739/article/details/129398257
版权归原作者 赵延东的一亩三分地 所有, 如有侵权,请联系我们删除。

“【SQL开发实战技巧】系列(二十四):数仓报表场景☞通过执行计划详解”行转列”,”列转行”是如何实现的”的评论:

还没有评论