杂项
-
- [1、使用SQL Server运算符PIVOT创建交叉报表](#1、使用SQL Server运算符PIVOT创建交叉报表)
- [2、使用SQL Server运算符UNPIVOT逆转置交叉报表](#2、使用SQL Server运算符UNPIVOT逆转置交叉报表)
- 3、使用Oracle子句MODEL转置结果集
- 4、从不固定的位置提取子串
- 5、确定特定年份有多少天(另一种Oracle解决方案)
- 6、找出同时包含字母和数字的字符串
- 7、在Oracle中将整数转换为其二进制表示
- 8、对经过排名的结果集进行转置
- 9、给经过两次转置的结果集添加列标题
- 10、在Oracle中将标量子查询转换为复合子查询
- 11、将序列化数据转换为行
- 12、计算占总计的百分比
- 13、确定编组是否包含指定的值
1、使用SQL Server运算符PIVOT创建交叉报表
问题:你想创建一张交叉报表,将结果集中的行转换为列。你熟悉传统的转置方法,但想尝试点儿新鲜的东西。具体地说,你想在不使用 CASE 表达式和连接的情况下,返回如下结果集。
sql
DEPT_10 DEPT_20 DEPT_30 DEPT_40
------- ----------- ----------- ----------
3 5 6 0
解决方案:使用 PIVOT 运算符生成所需的结果集,而不使用 CASE表达式或额外的连接操作。
sql
select [10] as dept_10,
[20] as dept_20,
[30] as dept_30,
[40] as dept_40
from (select deptno, empno from emp) driver
pivot (
count(driver.empno)
for driver.deptno in ( [10],[20],[30],[40] )
) as empPivot;
2、使用SQL Server运算符UNPIVOT逆转置交叉报表
问题:你有一个转置得到的结果集(或事实表),你想对其进行逆转置。例如,当前结果集为 1 行 4 列,而你想将其转换为 4 行 2 列。对于上一节实例生成的结果集:
sql
ACCOUNTING RESEARCH SALES OPERATIONS
---------- ---------- ---------- ----------
3 5 6 0
你想将其转换成下面这样。
sql
DNAME CNT
-------------- ----------
ACCOUNTING 3
RESEARCH 5
SALES 6
OPERATIONS 0
解决方案:SQL Server 不仅提供了 PIVOT,也提供了 UNPIVOT。要逆转置结果集,只需将它作为驱动表(driver),并让UNPIVOT 运算符来完成所有工作。你需要做的全部工作就是指定列名。
sql
select DNAME, CNT
from (
select [ACCOUNTING] as ACCOUNTING,
[SALES] as SALES,
[RESEARCH] as RESEARCH,
[OPERATIONS] as OPERATIONS
from (
select d.dname, e.empno
from emp e,dept d
where e.deptno=d.deptno
) driver
pivot (
count(driver.empno)
for driver.dname in ([ACCOUNTING],[SALES],[RESEARCH],[OPERATIONS])
) as empPivot
) new_driver
unpivot (cnt for dname in (ACCOUNTING,SALES,RESEARCH,OPERATIONS)
) as un_pivot;
3、使用Oracle子句MODEL转置结果集
问题:与 1 节的实例一样,你想找到一种替代方案,用于替代本书前面介绍过的传统转置方法。你想尝试使用Oracle 子句 MODEL。不同于 SQL Server 运算符PIVOT,Oracle 子句 MODEL 并非是为转置结果集而生的。事实上,将 MODEL 子句用于转置属于滥用,这也不是 MODEL 子句的初衷。尽管如此,MODEL 子句还是提供了一种解决常见问题的有趣方法。在这里,你想对下面的结果集进行转换。
sql
select deptno, count(*) cnt
from emp
group by deptno
DEPTNO CNT
------ ----------
10 3
20 5
30 6
结果如下。
sql
D10 D20 D30
---------- ---------- ----------
3 5 6
解决方案:就像传统转置方法那样,在 MODEL 子句中使用聚合函数和 CASE 表达式。主要区别在于,在 MODEL 子句中,我们会使用数组来存储聚合结果,并将结果集中的数组返回。
sql
select max(d10) d10,
max(d20) d20,
max(d30) d30
from (
select d10,d20,d30
from ( select deptno, count(*) cnt from emp group by deptno )
model
dimension by(deptno d)
measures(deptno, cnt d10, cnt d20, cnt d30)
rules(
d10[any] = case when deptno[cv()]=10 then d10[cv()] else 0 end,
d20[any] = case when deptno[cv()]=20 then d20[cv()] else 0 end,
d30[any] = case when deptno[cv()]=30 then d30[cv()] else 0 end
)
)
4、从不固定的位置提取子串
问题:你有一个字符串字段,其中包含序列化的日志数据,你想对字符串进行分析,从中提取意义重大的信息。可惜这些信息在字符串中的位置并不固定。因此,要提取这些信息,必须利用这样一个事实:这些信息前后是一些独特的字符。例如,请看下面的字符串。
sql
xxxxxabc[867]xxx[-]xxxx[5309]xxxxx
xxxxxtime:[11271978]favnum:[4]id:[Joe]xxxxx
call:[F_GET_ROWS()]b1:[ROSEWOOD...SIR]b2:[44400002]77.90xxxxx
film:[non_marked]qq:[unit]tailpipe:[withabanana?]80sxxxxx
你想提取包含在方括号内的值,进而返回如下结果集。
sql
FIRST_VAL SECOND_VAL LAST_VAL
--------------- ------------------- ---------------
867 - 5309
11271978 4 Joe
F_GET_ROWS() ROSEWOOD...SIR 44400002
non_marked unit withabanana?
解决方案:虽然不知道你感兴趣的值在字符串中的准确位置,但你知道它们位于方括号()内,且这样的值有 3 个。使用Oracle 内置函数 INSTR 确定方括号的位置,然后使用内置函数 SUBSTR 从字符串中提取所需的值。视图 V 包含要分析的字符串,其定义如下所示(这里使用它只是为了提高可读性)。
sql
create view V
as
select 'xxxxxabc[867]xxx[-]xxxx[5309]xxxxx' msg
from dual
union all
select 'xxxxxtime:[11271978]favnum:[4]id:[Joe]xxxxx' msg
from dual
union all
select 'call:[F_GET_ROWS()]b1:[ROSEWOOD...SIR]b2:[44400002]77.90xxxxx' msg
from dual
union all
select 'film:[non_marked]qq:[unit]tailpipe:[withabanana?]80sxxxxx' msg
from dual
1 select substr(msg,
2 instr(msg,'[',1,1)+1,
3 instr(msg,']',1,1)-instr(msg,'[',1,1)-1) first_val,
4 substr(msg,
5 instr(msg,'[',1,2)+1,
6 instr(msg,']',1,2)-instr(msg,'[',1,2)-1) second_val,
7 substr(msg,
8 instr(msg,'[',-1,1)+1,
9 instr(msg,']',-1,1)-instr(msg,'[',-1,1)-1) last_val
10 from V
5、确定特定年份有多少天(另一种Oracle解决方案)
问题:你想确定特定年份有多少天。
解决方案:获取给定日期所属年份的最后一天,再使用函数TO_CHAR 将该日期转换为一个 3 位数,这个数字指出了该日期是当前年份的第几天。
sql
select 'Days in 2021: '||
to_char(add_months(trunc(sysdate,'y'),12)-1,'DDD')
as report
from dual
union all
select 'Days in 2020: '||
to_char(add_months(trunc(
to_date('01-SEP-2020'),'y'),12)-1,'DDD')
from dual;
REPORT
---------------
Days in 2021: 365
Days in 2020: 366
6、找出同时包含字母和数字的字符串
问题:你有一个包含字母和数字数据的列,而你想返回这样的行,即其也包含字母和数字。换言之,如果该列值只包含字母或只包含数字,则不返回相应的行。返回的值应该同时包含字母和数字。请看下面的数据。
sql
STRINGS
------------
1010 switch
333
3453430278
ClassSummary
findRow 55
threes
最终的结果集应该只包含那些同时有字母和数字的行。
sql
STRINGS
------------
1010 switch
findRow 55
解决方案:使用内置函数 TRANSLATE 将每个字母都转换为某种字符,并将每个数字都转换为另一种字符。然后,只保留同时包含前述两种字符的字符串。本解决方案使用的是Oracle 语法,但 DB2 和 PostgreSQL 也支持TRANSLATE,因此只需做简单修改,本解决方案就将适用于这两种 RDBMS。
sql
with v as (
select 'ClassSummary' strings from dual union
select '3453430278' from dual union
select 'findRow 55' from dual union
select '1010 switch' from dual union
select '333' from dual union
select 'threes' from dual
)
select strings
from (
select strings,
translate(
strings,
'abcdefghijklmnopqrstuvwxyz0123456789',
rpad('#',26,'#')||rpad('*',10,'*')) translated
from v
) x
whereinstr(translated,'#') > 0
and instr(translated,'*') > 0
7、在Oracle中将整数转换为其二进制表示
问题:在 Oracle 系统中,你想将整数转换为其二进制表示。例如,你想返回 EMP 表中所有薪水的二进制表示,如下面的结果集所示。
sql
ENAME SAL SAL_BINARY
---------- ----- --------------------
SMITH 800 1100100000
ALLEN 1600 11001000000
WARD 1250 10011100010
JONES 2975 101110011111
MARTIN 1250 10011100010
BLAKE 2850 101100100010
CLARK 2450 100110010010
SCOTT 3000 101110111000
KING 5000 1001110001000
TURNER 1500 10111011100
ADAMS 1100 10001001100
JAMES 950 1110110110
FORD 3000 101110111000
MILLER 1300 10100010100
解决方案:MODEL 子句提供了迭代以及以数组方式访问行值的功能,因此使用它来解决这个问题是一种自然而然的想法。(这里假设必须使用 SQL 来解决这个问题,如若不然,使用存储函数则更合适。)与本书的其他解决方案一样,即便找不到本解决方案的实际用途,其中介绍的技巧也很有用。MODEL 子句可以执行过程性任务,同时具备SQL 基于集合的特征和强大威力,明白这一点很有用。因此,即便你认为自己绝不可能在 SQL 中这样做,也没有关系。这里无意于建议你应该怎么做,不该怎么做,而只想让你专注于其中的技巧,以便在合适的情况下付诸应用。
下面的解决方案会返回 EMP 表中所有的 ENAME 和SAL,同时在标量子查询中调用 MODEL。(从某种意义上说,可以将其作为 EMP 表中一个独立的函数,它像其他函数一样接受输入,启动处理过程,并返回一个值。)
sql
select ename,
sal,
(
select bin
from dual
model
dimension by ( 0 attr )
measures ( sal num,
cast(null as varchar2(30)) bin,
'0123456789ABCDEF' hex
)
rules iterate (10000) until (num[0] <= 0) (
bin[0] = substr(hex[cv()],mod(num[cv()],2)+1,1)||bin[cv()],
num[0] = trunc(num[cv()]/2)
)
) sal_binary
from emp;
8、对经过排名的结果集进行转置
问题:你想对表中的值进行排名,然后将结果集转置为 3 列。这样做旨在分别显示前 3 名、接下来的 3 名以及其余各行记录。例如,你想根据 SAL 对 EMP 表中的员工进行排名,然后将结果转置为 3 列,以得到如下结果集。
sql
TOP_3 NEXT_3 REST
--------------- --------------- --------------
KING (5000) BLAKE (2850) TURNER (1500)
FORD (3000) CLARK (2450) MILLER (1300)
SCOTT (3000) ALLEN (1600) MARTIN (1250)
JONES (2975) WARD (1250)
ADAMS (1100)
JAMES (950)
SMITH (800)
解决方案:本解决方案的关键在于,先使用窗口函数 DENSE_RANKOVER 根据 SAL 对员工进行排名,并允许排名相同。使用 ENSE_RANK OVER,可以轻松地获悉最高的 3 个薪水值、接下来的 3 个薪水值以及其他薪水值。
然后,使用窗口函数 ROW_NUMBER OVER 分别对各个分组(薪水前 3 组、薪水第 4~6 组和其他薪水组)中的员工进行排名。接下来,只需执行传统的转置并利用 RDBMS 提供的内置字符串函数,就可以获得漂亮的结果。下面的解决方案使用的是 Oracle 语法,但现在所有的 RDBMS 都支持窗口函数,因此很容易对该解决方案进行修改,以用于其他 RDBMS。
sql
select max(case grp when 1 then rpad(ename,6) ||
' ('|| sal ||')' end) top_3,
max(case grp when 2 then rpad(ename,6) ||
' ('|| sal ||')' end) next_3,
max(case grp when 3 then rpad(ename,6) ||
' ('|| sal ||')' end) rest
from (
select ename,
sal,
rnk,
case when rnk <= 3 then 1
when rnk <= 6 then 2
else 3
end grp,
row_number()over (
partition by case when rnk <= 3 then 1
when rnk <= 6 then 2
else 3
end
order by sal desc, ename
) grp_rnk
from (
select ename,
sal,
dense_rank()over(order by sal desc) rnk
from emp
) x
) y
group by grp_rnk;
9、给经过两次转置的结果集添加列标题
问题:你想合并两个结果集,并将它们转置为两列。另外,你还想给各组添加列"标题"。例如,你有两张表,分别包含公司中从事不同领域(比如研究领域和应用领域)开发工作的员工的信息。
sql
select * from it_research
DEPTNO ENAME
------ --------------------
100 HOPKINS
100 JONES
100 TONEY
200 MORALES
200 P.WHITAKER
200 MARCIANO
200 ROBINSON
300 LACY
300 WRIGHT
300 J.TAYLOR
select * from it_apps
DEPTNO ENAME
------ -----------------
400 CORRALES
400 MAYWEATHER
400 CASTILLO
400 MARQUEZ
400 MOSLEY
500 GATTI
500 CALZAGHE
600 LAMOTTA
600 HAGLER
600 HEARNS
600 FRAZIER
700 GUINN
700 JUDAH
700 MARGARITO
你想制作一张报表,在两列中分别列出这两张表中的员工。你还想显示返回每个部门的编号(DEPTNO),并在部门编号后面显示相应部门所有员工的名字(ENAME)。换言之,你想返回如下结果集。
sql
RESEARCH APPS
-------------------- ---------------
100 400
JONES MAYWEATHER
TONEY CASTILLO
HOPKINS MARQUEZ
200 MOSLEY
P.WHITAKER CORRALES
MARCIANO 500
ROBINSON CALZAGHE
MORALES GATTI
300 600
WRIGHT HAGLER
J.TAYLOR HEARNS
LACY FRAZIER
LAMOTTA
700
JUDAH
MARGARITO
GUINN
解决方案:从很大程度上说,本解决方案只需执行合并和转置,然后再做一些细微的调整:在每个部门的员工 ENMAE 前面加上相应的 DEPTNO。这里使用笛卡儿积来为每个 DEPTNO多生成一行数据,以便显示部门的所有员工以及 DEPTNO对应的行。本解决方案使用的是 Oracle 语法,但由于DB2 支持计算移动窗口的窗口函数(框架子句),因此很容易对本解决方案进行转换,使其适用于 DB2。由于只在本实例中使用了 IT_RESEARCH 表和 IT_APPS表,因此下面的解决方案中也会包含创建这些表的语句。
sql
create table IT_research (deptno number, ename varchar2(20))
insert into IT_research values (100,'HOPKINS')
insert into IT_research values (100,'JONES')
insert into IT_research values (100,'TONEY')
insert into IT_research values (200,'MORALES')
insert into IT_research values (200,'P.WHITAKER')
insert into IT_research values (200,'MARCIANO')
insert into IT_research values (200,'ROBINSON')
insert into IT_research values (300,'LACY')
insert into IT_research values (300,'WRIGHT')
insert into IT_research values (300,'J.TAYLOR')
create table IT_apps (deptno number, ename varchar2(20))
insert into IT_apps values (400,'CORRALES')
insert into IT_apps values (400,'MAYWEATHER')
insert into IT_apps values (400,'CASTILLO')
insert into IT_apps values (400,'MARQUEZ')
insert into IT_apps values (400,'MOSLEY')
insert into IT_apps values (500,'GATTI')
insert into IT_apps values (500,'CALZAGHE')
insert into IT_apps values (600,'LAMOTTA')
insert into IT_apps values (600,'HAGLER')
insert into IT_apps values (600,'HEARNS')
insert into IT_apps values (600,'FRAZIER')
insert into IT_apps values (700,'GUINN')
insert into IT_apps values (700,'JUDAH')
insert into IT_apps values (700,'MARGARITO')
select max(decode(flag2,0,it_dept)) research,
max(decode(flag2,1,it_dept)) apps
from (
select sum(flag1)over(partition by flag2
order by flag1,rownum) flag,
it_dept, flag2
from (
select 1 flag1, 0 flag2,
decode(rn,1,to_char(deptno),' '||ename) it_dept
from (
select x.*, y.id,
row_number()over(partition by x.deptno order by y.id) rn
from (
select deptno,
ename,
count(*)over(partition by deptno) cnt
from it_research
) x,
(select level id from dual connect by level <= 2) y
)
where rn <= cnt+1
union all
select 1 flag1, 1 flag2,
decode(rn,1,to_char(deptno),' '||ename) it_dept
from (
select x.*, y.id,
row_number()over(partition by x.deptno order by y.id) rn
from (
select deptno,
ename,
count(*)over(partition by deptno) cnt
from it_apps
) x,
(select level id from dual connect by level <= 2) y
)
where rn <= cnt+1
) tmp1
) tmp2
group by flag;
解决方案:从很大程度上说,本解决方案只需执行合并和转置,然后再做一些细微的调整:在每个部门的员工 ENMAE 前面加上相应的 DEPTNO。这里使用笛卡儿积来为每个 DEPTNO多生成一行数据,以便显示部门的所有员工以及 DEPTNO对应的行。本解决方案使用的是 Oracle 语法,但由于DB2 支持计算移动窗口的窗口函数(框架子句),因此很容易对本解决方案进行转换,使其适用于 DB2。由于只在本实例中使用了 IT_RESEARCH 表和 IT_APPS表,因此下面的解决方案中也会包含创建这些表的语句。
sql
create table IT_research (deptno number, ename varchar2(20))
insert into IT_research values (100,'HOPKINS')
insert into IT_research values (100,'JONES')
insert into IT_research values (100,'TONEY')
insert into IT_research values (200,'MORALES')
insert into IT_research values (200,'P.WHITAKER')
insert into IT_research values (200,'MARCIANO')
insert into IT_research values (200,'ROBINSON')
insert into IT_research values (300,'LACY')
insert into IT_research values (300,'WRIGHT')
insert into IT_research values (300,'J.TAYLOR')
create table IT_apps (deptno number, ename varchar2(20))
insert into IT_apps values (400,'CORRALES')
insert into IT_apps values (400,'MAYWEATHER')
insert into IT_apps values (400,'CASTILLO')
insert into IT_apps values (400,'MARQUEZ')
insert into IT_apps values (400,'MOSLEY')
insert into IT_apps values (500,'GATTI')
insert into IT_apps values (500,'CALZAGHE')
insert into IT_apps values (600,'LAMOTTA')
insert into IT_apps values (600,'HAGLER')
insert into IT_apps values (600,'HEARNS')
insert into IT_apps values (600,'FRAZIER')
insert into IT_apps values (700,'GUINN')
insert into IT_apps values (700,'JUDAH')
insert into IT_apps values (700,'MARGARITO')
select max(decode(flag2,0,it_dept)) research,
max(decode(flag2,1,it_dept)) apps
from (
select sum(flag1)over(partition by flag2
order by flag1,rownum) flag,
it_dept, flag2
from (
select 1 flag1, 0 flag2,
decode(rn,1,to_char(deptno),' '||ename) it_dept
from (
select x.*, y.id,
row_number()over(partition by x.deptno order by y.id) rn
from (
select deptno,
ename,
count(*)over(partition by deptno) cnt
from it_research
) x,
(select level id from dual connect by level <= 2) y
)
where rn <= cnt+1
union all
select 1 flag1, 1 flag2,
decode(rn,1,to_char(deptno),' '||ename) it_dept
from (
select x.*, y.id,
row_number()over(partition by x.deptno order by y.id) rn
from (
select deptno,
ename,
count(*)over(partition by deptno) cnt
from it_apps
) x,
(select level id from dual connect by level <= 2) y
)
where rn <= cnt+1
) tmp1
) tmp2
group by flag;
10、在Oracle中将标量子查询转换为复合子查询
问题:标量子查询只能返回一个值,而你想绕过这种限制。例如,你试图执行如下查询:
sql
select e.deptno,
e.ename,
e.sal,
(select d.dname,d.loc,sysdate today
from dept d
where e.deptno=d.deptno)
from emp e
但以出错告终,因为 SELECT 列表中的子查询只能返回一个值。
解决方案:必须承认,在实际工作中,不太可能遇到这种问题,因为只要将 EMP 表和 DEPT 表连接起来,就可以从 DEPT 表中返回任意数量的值。然而,这里的重点是掌握技巧以及如何在合适的场景中使用它。嵌套在 SELECT 子句中的SELECT 子句被称为标量子查询,它们只能返回一个值,要绕过这种限制,关键是使用 Oracle 对象类型:定义包含多个属性的对象,再将对象作为单个实体并分别引用其中的元素。实际上,这样做根本没有绕过前述限制,因为确实只返回了一个值,即一个包含众多属性的对象。
本解决方案将使用如下对象类型。
sql
create type generic_obj
as object (
val1 varchar2(10),
val2 varchar2(10),
val3 date
);
定义这个对象类型后,便可执行如下查询。
sql
select x.deptno,
x.ename,
x.multival.val1 dname,
x.multival.val2 loc,
x.multival.val3 today
from (
select e.deptno,
e.ename,
e.sal,
(select generic_obj(d.dname,d.loc,sysdate+1)
from dept d
where e.deptno=d.deptno) multival
from emp e
) x
DEPTNO ENAME DNAME LOC TODAY
------ ---------- ---------- ---------- -----------
20 SMITH RESEARCH DALLAS 12-SEP-2020
30 ALLEN SALES CHICAGO 12-SEP-2020
30 WARD SALES CHICAGO 12-SEP-2020
20 JONES RESEARCH DALLAS 12-SEP-2020
30 MARTIN SALES CHICAGO 12-SEP-2020
30 BLAKE SALES CHICAGO 12-SEP-2020
10 CLARK ACCOUNTING NEW YORK 12-SEP-2020
20 SCOTT RESEARCH DALLAS 12-SEP-2020
10 KING ACCOUNTING NEW YORK 12-SEP-2020
30 TURNER SALES CHICAGO 12-SEP-2020
20 ADAMS RESEARCH DALLAS 12-SEP-2020
30 JAMES SALES CHICAGO 12-SEP-2020
20 FORD RESEARCH DALLAS 12-SEP-2020
10 MILLER ACCOUNTING NEW YORK 12-SEP-2020
11、将序列化数据转换为行
问题:你想对序列化数据(存储在字符串中的数据)进行分析,并将其作为行返回。例如,你存储了如下数据。
sql
STRINGS
-----------------------------------
entry:stewiegriffin:lois:brian:
entry:moe::sizlack:
entry:petergriffin:meg:chris:
entry:willie:
entry:quagmire:mayorwest:cleveland:
entry:::flanders:
entry:robo:tchi:ken:
对于这些序列化字符串,你想将其转换为如下结果集。
sql
VAL1 VAL2 VAL3
--------------- --------------- ---------------
moe sizlack
petergriffin meg chris
quagmire mayorwest cleveland
robo tchi ken
stewiegriffin lois brian
willie
flanders
解决方案:本例中每个序列化字符串最多可以存储 3 个值,值之间用冒号分隔。不一定每个字符串都包含 3 个值,也可能更少。如果一个字符串包含的值不到 3 个,你必须小心处理,将各个值放在结果集的正确的列中。例如,请看下面的字符串。
sql
entry:::flanders:
这个字符串缺失了前两个值,只有第三个值。因此,如果查看本节"问题"部分的结果集,你将发现在 FLANDERS所在的行中,VAL1 列和 VAL2 列的值都为 NULL。
本解决方案的关键是先对字符串进行分析,然后再执行简单的转置。本解决方案使用了视图 V 返回的行,该视图的定义如下所示。
sql
create view V
as
select 'entry:stewiegriffin:lois:brian:' strings
from dual
union all
select 'entry:moe::sizlack:'
from dual
union all
select 'entry:petergriffin:meg:chris:'
from dual
union all
select 'entry:willie:'
from dual
union all
select 'entry:quagmire:mayorwest:cleveland:'
from dual
union all
select 'entry:::flanders:'
from dual
union all
select 'entry:robo:tchi:ken:'
from dual
本解决方案对视图 V 提供的示例数据进行了分析,如以下代码所示。这里使用的是 Oracle 语法,但由于只涉及字符串分析函数,因此很容易对该解决方案进行转换,使其适用于其他 RDBMS。
sql
with cartesian as (
select level id
from dual
connect by level <= 100
)
select max(decode(id,1,substr(strings,p1+1,p2-1))) val1,
max(decode(id,2,substr(strings,p1+1,p2-1))) val2,
max(decode(id,3,substr(strings,p1+1,p2-1))) val3
from (
select v.strings,
c.id,
instr(v.strings,':',1,c.id) p1,
instr(v.strings,':',1,c.id+1)-instr(v.strings,':',1,c.id) p2
from v, cartesian c
where c.id <= (length(v.strings)-length(replace(v.strings,':')))-1
)
group by strings
order by 1;
12、计算占总计的百分比
问题:你想制作一张报表,其中包含一系列数字值,并呈现每个值占总计的百分比。假设你使用的是 Oracle,你想返回一个结果集,呈现薪水在不同职位之间的分布情况,以确定哪种职位给公司带来的开销最高。为避免误导,你还想在结果集中呈现各种职位的员工数量。换言之,你想制作如下报表。
sql
JOB NUM_EMPS PCT_OF_ALL_SALARIES
--------- ---------- -------------------
CLERK 4 14
ANALYST 2 20
MANAGER 3 28
SALESMAN 4 19
PRESIDENT 1 17
如你所见,如果没有在报表中呈现各种职位的员工数量,那么总裁的薪水在薪水总计中的占比将看起来很低。在加入员工数量后,我们获悉总裁一个人的薪水占了总薪水的17%。
解决方案:仅当你使用的是 Oracle 时,才能使用内置函数RATIO_TO_REPORT 来妥善地解决上述问题。在其他RDBMS 中,要计算占总计的百分比,可以使用除法运算,这在 11 节中介绍过。
sql
select job,num_emps,sum(round(pct)) pct_of_all_salaries
from (
select job,
count(*)over(partition by job) num_emps,
ratio_to_report(sal)over()*100 pct
from emp
)
group by job,num_emps;
13、确定编组是否包含指定的值
问题:对于给定的行,你想创建一个布尔标志,指出在该行所属的编组中,是否至少有 1 行包含特定的值。来看一个例子:某位学生在给定时段(3 个月)参加特定次数(3次)考试。只要该学生通过了其中一次考试,便满足了要求,应返回一个指出这一点的标志。在 3 个月期间的 3次考试中,如果该学生一次都没通过,就返回另一个标志,以指出这一点。请看下面的示例(这里生成示例行时使用的是 Oracle 语法,如果你使用的是其他 RDBMS,则必须做细微的修改)。
sql
create view V
as
select 1 student_id,
1 test_id,
2 grade_id,
1 period_id,
to_date('02/01/2020','MM/DD/YYYY') test_date,
0 pass_fail
from dual union all
select 1, 2, 2, 1, to_date('03/01/2020','MM/DD/YYYY'), 1 from dual union all
select 1, 3, 2, 1, to_date('04/01/2020','MM/DD/YYYY'), 0 from dual union all
select 1, 4, 2, 2, to_date('05/01/2020','MM/DD/YYYY'), 0 from dual union all
select 1, 5, 2, 2, to_date('06/01/2020','MM/DD/YYYY'), 0 from dual union all
select 1, 6, 2, 2, to_date('07/01/2020','MM/DD/YYYY'), 0 from dual
select *
from V
STUDENT_ID TEST_ID GRADE_ID PERIOD_ID TEST_DATE PASS_FAIL
---------- ------- -------- --------- ----------- ---------
1 1 2 1 01-FEB-2020 0
1 2 2 1 01-MAR-2020 1
1 3 2 1 01-APR-2020 0
1 4 2 2 01-MAY-2020 0
1 5 2 2 01-JUN-2020 0
1 6 2 2 01-JUL-2020 0
从上面的结果集可知,该学生在两个学期(每个学期 3个月)内参加了 6 次考试。该学生通过了其中一次考试(1 表示通过,0 表示未通过),因此满足了第一学期的要求。由于该学生没有通过第二学期(接下来的 3 个月)的任何一次考试,因此对于第二学期的全部 3 次考试,PASS_FAIL 值都为 0。你想返回一个结果集,指出某位学生是否通过了给定学期的考试。
sql
STUDENT_ID TEST_ID GRADE_ID PERIOD_ID TEST_DATE METREQ IN_PROGRESS
---------- ------- -------- --------- ----------- ------ -----------
1 1 2 1 01-FEB-2020 + 0
1 2 2 1 01-MAR-2020 + 0
1 3 2 1 01-APR-2020 + 0
1 4 2 2 01-MAY-2020 - 0
1 5 2 2 01-JUN-2020 - 0
1 6 2 2 01-JUL-2020 - 1
在这个结果集中,METREQ 的值为 + 或--,表示某位学生是否满足了如下要求:在跨度为 3 个月的学期内,是否至少通过了一次考试。对于给定的学期,如果该学生至少通过了其中的一次考试,那么 IN_PROGRESS 应为 0。否则,在表示该学期最后一次考试的行中,IN_PROGRESS 的值应为 1。
解决方案:上述问题看起来很棘手,因为必须将编组中的行作为一个整体进行处理,而不能分别处理。请看本节"问题"部分中PASS_FAIL 列的值,如果逐行进行评估,好像除TEST_ID 值为 2 的行外,其他各行的 METREQ 值都应为--,但情况并非如此。你必须将编组中的所有行作为一个整体进行评估。使用窗口函数 MAX OVER,可以轻松地确定某位学生在特定学期内是否至少通过了一次考试。确定这一点后,只需使用 CASE 表达式就可以生成正确的布尔值。
sql
select student_id,
test_id,
grade_id,
period_id,
test_date,
decode( grp_p_f,1,lpad('+',6),lpad('-',6) ) metreq,
decode( grp_p_f,1,0,
decode( test_date,last_test,1,0 ) ) in_progress
from (
select V.*,
max(pass_fail)over(partition by
student_id,grade_id,period_id) grp_p_f,
max(test_date)over(partition by
student_id,grade_id,period_id) last_test
from V
) x;