SQL行列转换6种方法

时间:2024-01-04 11:07:14

在进行报表开发时,很多时候会遇到行列转换操作,很对开发人员针对于SQL级别行列转换操作一直不甚理解,今天正好抽空对其进行了一些简单的总结。这里主要列举3种可以实现SQL行列转换的方法,包括通用SQL解法以及Oracle支持解法。

一、测试数据

测试表依旧采用Oracle经典的scott模式下的dept表和emp表,结构如下:

DEPT:

create table DEPT

(

DEPTNO NUMBER(2) not null,

DNAME VARCHAR2(14),

LOC VARCHAR2(13)

)

-- Create/Recreate primary, unique and foreign key constraints

alter table DEPT

add constraint PK_DEPT primary key (DEPTNO)

using index;

SQL> select * from dept;

DEPTNO DNAME LOC

------ -------------- -------------

10 ACCOUNTING NEW YORK

20 RESEARCH DALLAS

30 SALES CHICAGO

40 OPERATIONS BOSTON

EMP:

create table EMP

(

EMPNO NUMBER(4) not null,

ENAME VARCHAR2(10),

JOB VARCHAR2(9),

MGR NUMBER(4),

HIREDATE DATE,

SAL NUMBER(7,2),

COMM NUMBER(7,2),

DEPTNO NUMBER(2),

SEX VARCHAR2(2) default '男'

)

-- Create/Recreate primary, unique and foreign key constraints

alter table EMP

add constraint PK_EMP primary key (EMPNO)

using index;

alter table EMP

add constraint FK_DEPTNO foreign key (DEPTNO)

references DEPT (DEPTNO);

SQL> select * from emp;

EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO SEX

----- ---------- --------- ----- ----------- --------- --------- ------ ---

7369 SMITH CLERK 7902 1980/12/17 800.00 20 男

7499 ALLEN SALESMAN 7698 1981/2/20 1600.00 300.00 30 女

7521 WARD SALESMAN 7698 1981/2/22 1250.00 500.00 30 女

7566 JONES MANAGER 7839 1981/4/2 2975.00 20 女

7654 MARTIN SALESMAN 7698 1981/9/28 1250.00 1400.00 30 女

7698 BLAKE MANAGER 7839 1981/5/1 2850.00 30 女

7782 CLARK MANAGER 7839 1981/6/9 2450.00 10 女

7788 SCOTT ANALYST 7566 1987/4/19 3000.00 20 男

7839 KING PRESIDENT 1981/11/17 5000.00 10 女

7844 TURNER SALESMAN 7698 1981/9/8 1500.00 0.00 30 女

7876 ADAMS CLERK 7788 1987/5/23 1100.00 20 男

7900 JAMES CLERK 7698 1981/12/3 950.00 30 女

7902 FORD ANALYST 7566 1981/12/3 3000.00 20 女

7934 MILLER CLERK 7782 1982/1/23 1300.00 10 男

14 rows selected

二、行列转换方法

需求:查询每个部门中不同职位总工资。

1、最传统的SQL解法如下:

SQL> select d.dname, e.job, sum(sal) as sum_sal

2 from emp e, dept d

3 where e.deptno = d.deptno

4 group by d.dname, e.job;

DNAME JOB SUM_SAL

-------------- --------- ----------

SALES MANAGER 2850

SALES CLERK 950

ACCOUNTING MANAGER 2450

ACCOUNTING PRESIDENT 5000

ACCOUNTING CLERK 1300

RESEARCH MANAGER 2975

SALES SALESMAN 5600

RESEARCH ANALYST 6000

RESEARCH CLERK 1900

9 rows selected

以上方法确实得到了不同部门不同职位的工资总和,可是我们看到每一个部门出现了多行信息,这并不是我们想要的结果,我们更希望每个部门显示为1行。

2、使用子查询实现行列转换:

SQL> select e.deptno,

2 (select sum(sal) from emp where emp.deptno = e.deptno and job='ANALYST') as analyst_job,

3 (select sum(sal) from emp where emp.deptno=e.deptno and job='CLERK') as clerk_job,

4 (select sum(sal) from emp where emp.deptno=e.deptno and job='MANAGER') as manager_job,

5 (select sum(sal) from emp where emp.deptno=e.deptno and job='PRESIDENT') as persident_job,

6 (select sum(sal) from emp where emp.deptno=e.deptno and job='SALESMAN') as salesman_job

7 from emp e

8 group by e.deptno;

DEPTNO ANALYST_JOB CLERK_JOB MANAGER_JOB PERSIDENT_JOB SALESMAN_JOB

------ ----------- ---------- ----------- ------------- ------------

30 950 2850 5600

20 6000 1900 2975

10 1300 2450 5000

3、尝试更改SQL如以下形式达到目的:

SQL> select DNAME,

2 SUM(MANAGER_JOB) AS SUM_MANAGER_SAL,

3 SUM(PRESIDENT_JOB) AS SUM_PRESIDENT_SAL,

4 SUM(CLERK_JOB) AS SUM_CLERK_SAL,

5 SUM(ANALYST_JOB) AS SUM_ANALYST_SAL,

6 SUM(SALESMAN_JOB) AS SUM_SALESMAN_SAL

7 from (select d.dname,

8 (select sum(sal)

9 from emp

10 where emp.empno = e.empno

11 and emp.job = 'MANAGER') as MANAGER_JOB,

12 (select sum(sal)

13 from emp

14 where emp.empno = e.empno

15 and emp.job = 'PRESIDENT') as PRESIDENT_JOB,

16 (select sum(sal)

17 from emp

18 where emp.empno = e.empno

19 and emp.job = 'CLERK') as CLERK_JOB,

20 (select sum(sal)

21 from emp

22 where emp.empno = e.empno

23 and emp.job = 'ANALYST') as ANALYST_JOB,

24 (select sum(sal)

25 from emp

26 where emp.empno = e.empno

27 and emp.job = 'SALESMAN') as SALESMAN_JOB

28 from emp e, dept d

29 where e.deptno = d.deptno) temp

30 group by temp.dname

31 order by temp.dname;

DNAME SUM_MANAGER_SAL SUM_PRESIDENT_SAL SUM_CLERK_SAL SUM_ANALYST_SAL SUM_SALESMAN_SAL

-------------- --------------- ----------------- ------------- --------------- ----------------

ACCOUNTING 2450 5000 1300

RESEARCH 2975 1900 6000

SALES 2850 950 5600

通过查询结果可以看到,这里我们已经实现了行列转换,同时这个结果也符合我们日常阅读习惯。但是代码过于复杂,执行计划回表次数明显很会很多。这里的代码基本符合任何一种支持SQL的数据库基本语法,属于通用模式,那么有没有简单一点的方法呢?答案是肯定的!

4、基于Oracle decode()函数的行列转换:

SQL> select d.dname,

2 sum(decode(job, 'MANAGER', sal, 0)) as SUM_MANAGER_SAL,

3 sum(decode(job, 'PRESIDENT', sal, 0)) as SUM_PRESIDENT_SAL,

4 sum(decode(job, 'CLERK', sal, 0)) as SUM_CLERK_SAL,

5 sum(decode(job, 'ANALYST', sal, 0)) as SUM_ANALYST_SAL,

6 sum(decode(job, 'SALESMAN', sal, 0)) as SUM_SALESMAN_SAL

7 from emp e, dept d

8 where e.deptno = d.deptno

9 group by d.dname;

DNAME SUM_MANAGER_SAL SUM_PRESIDENT_SAL SUM_CLERK_SAL SUM_ANALYST_SAL SUM_SALESMAN_SAL

-------------- --------------- ----------------- ------------- --------------- ----------------

ACCOUNTING 2450 5000 1300 0 0

RESEARCH 2975 0 1900 6000 0

SALES 2850 0 950 0 5600

根据上面执行结果,我们可以看到通过Oracle数据库的decode()函数达到了进行行列装换的目的,而且明显针对于前一种行列转换操作效率提升不少。

5、通用行列转换可以通过case when ... then ...else ... end实现:

SQL> select d.dname,

2 sum(case

3 when e.job = 'ANALYST' then

4 sal

5 else

6 0

7 end) as ANALYST_JOB,

8 sum(case

9 when e.job = 'CLERK' then

10 sal

11 else

12 0

13 end) as CLERK_JOB,

14 sum(case

15 when e.job = 'MANAGER' then

16 sal

17 else

18 0

19 end) as MANAGER_JOB,

20 sum(case

21 when e.job = 'PRESIDENT' then

22 sal

23 else

24 0

25 end) as PRESIDENT_JOB,

26 sum(case

27 when e.job = 'SALESMAN' then

28 sal

29 else

30 0

31 end) as SALESMAN_JOB

32 from emp e, dept d

33 where e.deptno = d.deptno

34 group by d.dname;

DNAME ANALYST_JOB CLERK_JOB MANAGER_JOB PRESIDENT_JOB SALESMAN_JOB

-------------- ----------- ---------- ----------- ------------- ------------

ACCOUNTING 0 1300 2450 5000 0

RESEARCH 6000 1900 2975 0 0

SALES 0 950 2850 0 5600

6、在Oracle 11g中引入了pivot和unpivot函数功能,专门用来进行行列转换,pivot函数语法如下:

select *|列 [别名],...

from 子查询

pivot(

统计函数(列)s for 转换列名称 in(

内容1 [[as] 别名],

内容2 [[as] 别名],

... ...

内容n [[as] 别名]

)

)

[where 条件(s)]

[group by 分组字段1,分组字段2,... ...]

[having 过滤条件(s)]

[order by 排序字段 asc|desc]

使用pivot函数实现行列转换:

SQL> select *

2 from (select d.dname, e.job, e.sal

3 from emp e, dept d

4 where e.deptno = d.deptno) pivot(sum(sal) for job in('ANALYST' AS

5 ANALYST_JOB,

6 'CLERK' AS

7 CLERK_JOB,

8 'MANAGER' AS

9 MANAGER_JOB,

10 'PRESIDENT' AS

11 PRESIDENT_JOB,

12 'SALESMAN' AS

13 SALESMAN_JOB))

14 order by dname;

DNAME ANALYST_JOB CLERK_JOB MANAGER_JOB PRESIDENT_JOB SALESMAN_JOB

-------------- ----------- ---------- ----------- ------------- ------------

ACCOUNTING 1300 2450 5000

RESEARCH 6000 1900 2975

SALES 950 2850 5600

以上SQL使用pivot函数实现了行列转换操作。至此,对于行列转换操作我们使用3种方法实现。

以下是pivot函数实现更复杂一些SQL的例子:

6.1、查询每个部门的总工资,每个部门不同职位的总工资和每个部门的最高工资和最低工资:

SQL> select *

2 from (select deptno,

3 job,

4 sal,

5 sum(sal) over(partition by deptno) as sum_sal,

6 max(sal) over(partition by deptno) as max_sal,

7 min(sal) over(partition by deptno) as min_sal

8 from emp) pivot(sum(sal) for job in('ANALYST' AS ANALYST_JOB,

9 'CLERK' AS CLERK_JOB,

10 'MANAGER' AS MANAGER_JOB,

11 'PRESIDENT' AS PRESIDENT_JOB,

12 'SALESMAN' AS SALESMAN_JOB))

13 order by deptno;

DEPTNO SUM_SAL MAX_SAL MIN_SAL ANALYST_JOB CLERK_JOB MANAGER_JOB PRESIDENT_JOB SALESMAN_JOB

------ ---------- ---------- ---------- ----------- ---------- ----------- ------------- ------------

10 8750 5000 1300 1300 2450 5000

20 10875 3000 800 6000 1900 2975

30 9400 2850 950 950 2850 5600

6.2、查询每个部门不同职位总工资以及每个部门不同职位最高工资:

SQL> select *

2 from (select deptno, job, sal from emp)

3 pivot(sum(sal) as sum_sal, max(sal) as sum_max

4 for job in('ANALYST' AS ANA_JOB,

5 'CLERK' AS CLE_JOB,

6 'MANAGER' AS MAN_JOB/*,

7 'PRESIDENT' AS PRE_JOB,

8 'SALESMAN' AS SAL_JOB*/)

9 )

10 order by deptno;

DEPTNO ANA_JOB_SUM_SAL ANA_JOB_SUM_MAX CLE_JOB_SUM_SAL CLE_JOB_SUM_MAX MAN_JOB_SUM_SAL MAN_JOB_SUM_MAX

------ --------------- --------------- --------------- --------------- --------------- ---------------

10 1300 1300 2450 2450

20 6000 3000 1900 1100 2975 2975

30 950 950 2850 2850

6.3、统计每个部门不同职位总工资和不同性别的最高工资:

SQL> select *

2 from (select deptno, job, sex, sal from emp)

3 pivot(sum(sal) as sum_sal, max(sal) as sum_max

4 for(job, sex) in(('MANAGER', '男') AS MANAGER_JOB_MAN,

5 ('MANAGER', '女') AS MANAGER_JOB_WOMAN/*,

6 ('CLERK', '男') AS CLERK_JOB_MAN,

7 ('CLERK', '女') AS CLERK_JOB_WOMAN,

8 ('ANALYST', '男') AS ANALYST_JOB_MAN,

9 ('ANALYST', '女') AS ANALYST_JOB_WOMAN,

10 ('PRESIDENT', '男') AS PRESIDENT_JOB_MAN,

11 ('PRESIDENT', '女') AS PRESIDENT_JOB_WOMAN,

12 ('SALESMAN', '男') AS SALESMAN_JOB_MAN,

13 ('SALESMAN', '女') AS SALESMAN_JOB_WOMAN*/))

14 order by deptno;

DEPTNO MANAGER_JOB_MAN_SUM_SAL MANAGER_JOB_MAN_SUM_MAX MANAGER_JOB_WOMAN_SUM_SAL MANAGER_JOB_WOMAN_SUM_MAX

------ ----------------------- ----------------------- ------------------------- -------------------------

10 2450 2450

20 2975 2975

30 2850 2850

6.4、如果在pivot中增加xml显示,可以利用any设置所要操作的所有数据,目前any仅能使用在xml显示中:

SQL> select *

2 from (select deptno, job, sal from emp) pivot xml(sum(sal) for job in(any))

3 order by deptno;

6.5、pivot功能是将行转换为列;同样unpivot功能进行列转行操作,操作语法如下:

select *|列 [别名],...

from 子查询

unpivot [include nulls|exclude nulls(默认)](

统计函数(列)s for 转换列名称 in(

内容1 [[as] 别名],

内容2 [[as] 别名],

... ...

内容n [[as] 别名]

)

)

[where 条件(s)]

[group by 分组字段1,分组字段2,... ...]

[having 过滤条件(s)]

[order by 排序字段 asc|desc]

如把步骤2中行转列的SQL结果再转回行:

SQL> with temp as

2 (select *

3 from (select d.dname, e.job, e.sal

4 from emp e, dept d

5 where e.deptno = d.deptno) pivot(sum(sal) for job in('ANALYST' AS

6 ANALYST_JOB,

7 'CLERK' AS

8 CLERK_JOB,

9 'MANAGER' AS

10 MANAGER_JOB,

11 'PRESIDENT' AS

12 PRESIDENT_JOB,

13 'SALESMAN' AS

14 SALESMAN_JOB))

15 order by dname)

16 select *

17 from temp unpivot(sum_sal for job in(ANALYST_JOB as 'ANALYST',

18 CLERK_JOB as 'CLERK',

19 MANAGER_JOB as 'MANAGER',

20 PRESIDENT_JOB as 'PRESIDENT',

21 SALESMAN_JOB as 'SALESMAN'));

DNAME JOB SUM_SAL

-------------- --------- ----------

ACCOUNTING CLERK 1300

ACCOUNTING MANAGER 2450

ACCOUNTING PRESIDENT 5000

RESEARCH ANALYST 6000

RESEARCH CLERK 1900

RESEARCH MANAGER 2975

SALES CLERK 950

SALES MANAGER 2850

SALES SALESMAN 5600

9 rows selected

从上述结果可以看到,这里的结果已经将列转换为行了,但是不包含nulls值得行,如果想包含nulls值的行业显示出来需要显示的生命include nulls关键字:

SQL> with temp as

2 (select *

3 from (select d.dname, e.job, e.sal

4 from emp e, dept d

5 where e.deptno = d.deptno) pivot(sum(sal) for job in('ANALYST' AS

6 ANALYST_JOB,

7 'CLERK' AS

8 CLERK_JOB,

9 'MANAGER' AS

10 MANAGER_JOB,

11 'PRESIDENT' AS

12 PRESIDENT_JOB,

13 'SALESMAN' AS

14 SALESMAN_JOB))

15 order by dname)

16 select *

17 from temp unpivot include nulls(sum_sal for job in(ANALYST_JOB as

18 'ANALYST',

19 CLERK_JOB as 'CLERK',

20 MANAGER_JOB as

21 'MANAGER',

22 PRESIDENT_JOB as

23 'PRESIDENT',

24 SALESMAN_JOB as

25 'SALESMAN'));

DNAME JOB SUM_SAL

-------------- --------- ----------

ACCOUNTING ANALYST

ACCOUNTING CLERK 1300

ACCOUNTING MANAGER 2450

ACCOUNTING PRESIDENT 5000

ACCOUNTING SALESMAN

RESEARCH ANALYST 6000

RESEARCH CLERK 1900

RESEARCH MANAGER 2975

RESEARCH PRESIDENT

RESEARCH SALESMAN

SALES ANALYST

SALES CLERK 950

SALES MANAGER 2850

SALES PRESIDENT

SALES SALESMAN 5600

15 rows selected