SQL经典实例——杂项

杂项

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;