SQL 为两次变换后的结果集增加列标题,你想把两个结果集叠加起来,并把它们转换成两列。除此之外,你还希望为每一列中的每一组行数据增加一个列标题。例如,你有两个表,它们分别是公司里从事两个不同领域工作的员工名单(假设是研究领域和应用领域)。
SQL 为两次变换后的结果集增加列标题 问题描述
你想把两个结果集叠加起来,并把它们转换成两列。除此之外,你还希望为每一列中的每一组行数据增加一个列标题。例如,你有两个表,它们分别是公司里从事两个不同领域工作的员工名单(假设是研究领域和应用领域)。
你希望创建一个报表,分两列列出每个表的员工。你也希望返回每个 DEPTNO
下的 ENAME
。最终你想得到如下所示的结果集。
SQL 为两次变换后的结果集增加列标题 解决方案
基本上,本解决方案只需要一个简单的 stack-n-pivot
操作(先执行 UNION
操作,然后再做行列翻转)。除此之外,还要做一个额外的操作:DEPTNO
必须要先于 ENAME
被返回。这里使用的技巧是,借助笛卡儿积为每个 DEPTNO
产生一行额外的数据,这样我们就不仅得到所有员工的数据,也得到了 DEPTNO
对应的行。本解决方案采用 Oracle 语法,但是由于 DB2 的窗口函数也支持滑动窗口(Framing
子句),适当修改一下本解决方案不难得到适用于 DB2 的代码。因为 IT_RESEARCH
表和 IT_APPS
表只为本实例而存在,下面的解决方案里也顺便列出了创建这些表的语句。
SQL 为两次变换后的结果集增加列标题 扩展知识
和其他的数据仓库和报表类型的查询一样,上述解决方案看起来相当复杂,但是如果拆解开来一一细看的话,不难发现它其实是一个 stack-n-pivot
操作,外加一个笛卡儿积(困难重重且没有任何办法)。拆解上述查询的方法是先仔细查看 UNION ALL
的每个组成部分,然后再把它们合并起来做行列变换。先从 UNION ALL
的后半部分开始。
仔细看一下上述结果集究竟是怎么拼凑出来的。把上面的查询分解成最基本的组成部分,即可得到内嵌视图 X
,该视图从 IT_APPS
表里取出每个 ENAME
和 DEPTNO
,并计算出每个 DEPTNO
对应的员工人数。结果如下所示。
下一步是基于内嵌视图 X
返回的行和由 CONNECT BY
从 DUAL
表中产生出来的两行数据创建一个笛卡儿积。该操作的结果集显示如下。
如上所示,由于笛卡儿积的存在内嵌视图 X
的每一行数据都被返回了两次。不用着急,你很快就会明白为什么此处需要一个笛卡儿积。下一步是根据 ID
(ID
的值为 1 或 2,这是由笛卡儿积生成的)为当前结果集中每个 DEPTNO
对应的员工进行编号。编号的结果显示在下面查询的输出部分。
每个员工都有了一个编号,并且,他们的重复项也都被分配了编号。上述结果集中包含了 IT_APP
表里的所有员工及其重复项,以及基于所属 DEPTNO
为每一行生成的编号。我们需要额外生成这些重复项的原因是,因为我们需要在结果集中留一个缝隙把 DEPTNO
插入到 ENAME
列。如果我们把一个只有 1 行数据的表和 IT_APPS
表连接起来做笛卡儿积,就无法得到这些额外的行。(因为一个表的记录条数乘以 1 的结果仍然会等于该表的记录条数。)
下一步是把到目前为止的结果集做行列翻转操作,这样 ENAMES
会以一列的形式返回,但会先返回他们所属的 DEPTNO
。如下所示的查询展示了这一操作过程。
先暂时忽略掉 FLAG1
和 FLAG2
,稍后再做讨论。注意观察上述 IT_DEPT
列的结果。每个 DEPTNO
返回的记录行数是 CNT*2
,但实际上只需要 CNT+1
行记录,WHERE
子句的过滤条件会限制记录行数。RN
是每个员工的编号,所有编号值小于或等于 CNT+1
的行都会被保留下来。也就是说,每个 DEPTNO
对应的所有员工再加上额外的 1 行记录会被保留下来(额外的那 1 行记录是每个 DEPTNO
对应的编号最小的员工)。这一行额外的记录就是用来插入 DEPTNO
的地方。调用 DECODE
函数(该函数的功能类似于 CASE
表达式,早期 Oracle 版本已经支持该函数)判定 RN
的值,并把 DEPTNO
插入到结果集里。RN
值等于 1 的员工不会被漏掉,但会被放在每个 DEPTNO
的最后位置(因为顺序无关紧要,放在最后也没有关系)。至此为止,我们已经详尽地讨论了 UNION ALL
的后半部分。
UNION ALL
的前半部分过程和后半部分相同,因此就没必要再重复讨论了。下面仔细观察一下两个查询结果叠加后的结果集。
此时你或许还不明白 FLAG1
的作用,但可以看出 FLAG2
被用来标识行记录来自 UNION ALL
的哪个部分(0 表示前半部门,1 表示后半部分)。
下一步是把叠加后的结果集包裹在一个内嵌视图里,并计算 FLAG1
的累计合计值(终于知道它的作用了!),该累计合计值可以看作是 UNION ALL
的两个数据子集内部各自生成的行编号。编号后的结果集(累计合计值)如下所示。
剩下的最后一步是,基于 FLAG2
把 TMP1
的返回值做行列翻转,同时也要按照 FLAG
(TMP1
中生成的累计合计值)进行分组。把 TMP1
的查询结果包裹在一个内嵌视图(最外层的内嵌视图 TMP2
)里,并做行列翻转。最终的解决方案和结果集显示如下。