SQL 和 SPL 的复杂静态转置对比
【摘要】
转置功能常用报表等前端展现,将查询出来的数据转置成指定的显示格式。比如行转列,列转行,以及比较复杂的动态转置等等。SQL 和 SPL 是大家比较熟悉的程序语言,本文将探讨对于转置问题,这两种语言的解决方案和基本原理。如何简便快捷的处理转置运算,这里为你全程解析,并提供 SQL 和 SPL 示例代码。SQL 和 SPL 的复杂静态转置对比
在前文中,我们为大家介绍了静态转置。接下来我们一起看一下,对于复杂的静态转置,SQL 和 SPL 是如何处理的。
1. 多行转多行
【例 1】 根据打卡记录,整理员工每日的外出数据。在日常考勤信息表中,每卡每天有 7 条数据:
PER_CODE |
IN_OUT |
DATE |
TIME |
TYPE |
1110263 |
1 |
2013-10-11 |
09:17:14 |
In |
1110263 |
6 |
2013-10-11 |
11:37:00 |
Break |
1110263 |
5 |
2013-10-11 |
11:38:21 |
Return |
1110263 |
0 |
2013-10-11 |
11:43:21 |
NULL |
1110263 |
6 |
2013-10-11 |
13:21:30 |
Break |
1110263 |
5 |
2013-10-11 |
14:25:58 |
Return |
1110263 |
2 |
2013-10-11 |
18:28:55 |
Out |
… |
… |
… |
… |
… |
期望得到如下格式结果:
PER_CODE |
DATE |
IN |
OUT |
BREAK |
RETURN |
1110263 |
2013-10-11 |
9:17:14 |
18:28:55 |
11:37:00 |
11:38:21 |
1110263 |
2013-10-11 |
9:17:14 |
18:28:55 |
13:21:30 |
14:25:58 |
… |
… |
… |
… |
… |
… |
SQL的解决方案:
本例中需要多条记录合并计算,根据目标结构返回多条记录。我们把数据按员工号和日期时间排序,每 7 行记录作为一组。顺序取出每组 [1,7,2,3] 行的时间,分别作为 IN,OUT,BREAK,RETURN 的值,再取出每组 [1,7,5,6] 行的时间作为第二组值。对于不支持 PIVOT\UNPIVOT 的数据库,SQL 语句如下:
WITH CTE1 AS (
SELECT
PER_CODE,IN_OUT,"DATE","TIME",TYPE,
MOD(ROWNUM-1,7)+1 GROUP_ORDER
FROM DAILY_TIME
ORDER BY PER_CODE,"DATE","TIME"
)
SELECT * FROM (
SELECT
T_IN.PER_CODE,T_IN."DATE",T_IN."IN",T_OUT."OUT",
T_BREAK.BREAK,T_RETURN."RETURN"
FROM (
SELECT PER_CODE,"DATE","TIME" "IN"
FROM CTE1
WHERE GROUP_ORDER=1
) T_IN
LEFT JOIN (
SELECT PER_CODE,"DATE","TIME" "OUT"
FROM CTE1
WHERE GROUP_ORDER=7
) T_OUT
ON T_IN.PER_CODE=T_OUT.PER_CODE
AND T_IN."DATE"=T_OUT."DATE"
LEFT JOIN (
SELECT PER_CODE,"DATE","TIME" BREAK
FROM CTE1
WHERE GROUP_ORDER=2
) T_BREAK
ON T_IN.PER_CODE=T_BREAK.PER_CODE
AND T_IN."DATE"=T_BREAK."DATE"
LEFT JOIN (
SELECT PER_CODE,"DATE","TIME" "RETURN"
FROM CTE1
WHERE GROUP_ORDER=3
) T_RETURN
ON T_IN.PER_CODE=T_RETURN.PER_CODE
AND T_IN."DATE"=T_RETURN."DATE"
UNION ALL (
SELECT
T_IN.PER_CODE,T_IN."DATE",T_IN."IN",T_OUT."OUT",
T_BREAK.BREAK,T_RETURN."RETURN"
FROM (
SELECT PER_CODE,"DATE","TIME" "IN"
FROM CTE1
WHERE GROUP_ORDER=1
) T_IN
LEFT JOIN (
SELECT PER_CODE,"DATE","TIME" "OUT"
FROM CTE1
WHERE GROUP_ORDER=7
) T_OUT
ON T_IN.PER_CODE=T_OUT.PER_CODE
AND T_IN."DATE"=T_OUT."DATE"
LEFT JOIN (
SELECT PER_CODE,"DATE","TIME" BREAK
FROM CTE1
WHERE GROUP_ORDER=5
) T_BREAK
ON T_IN.PER_CODE=T_BREAK.PER_CODE
AND T_IN."DATE"=T_BREAK."DATE"
LEFT JOIN (
SELECT PER_CODE,"DATE","TIME" "RETURN"
FROM CTE1
WHERE GROUP_ORDER=6
) T_RETURN
ON T_IN.PER_CODE=T_RETURN.PER_CODE
AND T_IN."DATE"=T_RETURN."DATE"
)
)
ORDER BY PER_CODE,"DATE",BREAK
这个 SQL 语句已经很难看懂了。以 ORACLE 11g 为例,我们可以使用 PIVOT 简化 LEFT JOIN 部分,SQL 语句如下:
WITH CTE1 AS (
SELECT
PER_CODE,IN_OUT,"DATE","TIME",TYPE,
MOD(ROWNUM-1,7)+1 GROUP_ORDER
FROM DAILY_TIME
ORDER BY PER_CODE,"DATE","TIME"
)
SELECT *
FROM (
SELECT *
FROM (
SELECT
PER_CODE,"DATE","TIME",GROUP_ORDER
FROM CTE1
WHERE GROUP_ORDER IN (1,7,2,3)
)
PIVOT(
MIN("TIME") FOR GROUP_ORDER
IN (1 AS "IN",7 AS "OUT",2 AS BREAK,3 AS "RETURN")
)
UNION ALL
(
SELECT *
FROM (
SELECT
PER_CODE,"DATE","TIME",GROUP_ORDER
FROM CTE1
WHERE GROUP_ORDER IN (1,7,5,6)
)
PIVOT(
MIN("TIME") FOR GROUP_ORDER
IN (1 AS "IN",7 AS "OUT",5 AS BREAK,6 AS "RETURN")
)
)
)
ORDER BY PER_CODE,"DATE",BREAK
SPL的解决方案:
虽然转置后的表结构是可以确定的,但是用函数 A.pivot() 实现起来会很复杂。这时可以先创建目标数据结构,再填充数据。
A |
|
1 |
=create(PER_CODE,DATE,IN,OUT,BREAK,RETURN) |
2 |
=T("DailyTime.txt").sort(PER_CODE,DATE,TIME) |
3 |
=A2.group((#-1)\7).(~([1,7,2,3,1,7,5,6])) |
4 |
>A1.record(A3.conj([~.PER_CODE,~.DATE]|~.(TIME).m([1,2,3,4])|[~.PER_CODE,~.DATE]|~.(TIME).m([5,6,7,8]))) |
A1:根据目标结构创建空表。
A2:导入日常打卡记录,并按员工号和日期时间排序。
A3:每 7 条记录为一组,每组按指定顺序([1,7,2,3,1,7,5,6])返回记录。
A4:将所有的记录数据按目标顺序合并,再添加到 A3 创建的表中。
我们先分析一下 SPL 的解决方案:首先我们按照期望的结构创建一个空表。然后将数据排序,每 7 条记录分为一组,每组的成员取 [1,7,2,3,1,7,5,6],即要生成的 2 条记录的时间值。最后再按目标结构顺序填入数据即可。而在 SQL 语句中,尽管使用了 PIVOT 函数,SQL 语句仍然非常复杂。这个题目并不适合用 PIVOT 来实现,如果 SQL 中也按照 SPL 的思路来做就会遇到一些问题。首先 SQL 中的分组并不会保留分组子集,这导致我们不能像 SPL 中一样,在分组后再进行复杂计算。另外 SQL 中的集合是无序的,我们可以利用行号来计算每组中的序号,但是不能像 SPL 中一样很方便的使用多个序号有序的访问成员。
2. 行转列同时行间计算
【例 2】 根据用户支付明细表,统计各个用户 2014 年每月应付金额的汇总表。部分数据如下:
ID |
CUSTOMERID |
NAME |
UNPAID |
ORDER_DATE |
112101 |
C013 |
CA |
12800 |
2014/02/21 |
112102 |
C013 |
CA |
3500 |
2014/06/15 |
112103 |
C013 |
CA |
2600 |
2015/03/21 |
… |
… |
… |
… |
… |
期望得到如下格式结果:
NAME |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
CA |
12800 |
12800 |
12800 |
12800 |
3500 |
3500 |
3500 |
3500 |
3500 |
3500 |
3500 |
|
… |
SQL的解决方案:
先构造目标结构,再填充数据的解决方案,在 SQL 中无法实现。所以每个问题都需要思考不同的解决方案,我们知道最后一步可以用 PIVOT 函数将月份行转列,之前需要把数据准备成如下格式:
NAME |
ORDER_MONTH |
UNPAID |
CA |
1 |
|
CA |
2 |
12800 |
CA |
3 |
12800 |
CA |
4 |
12800 |
CA |
5 |
12800 |
CA |
6 |
3500 |
CA |
7 |
3500 |
… |
… |
… |
在数据表中,并不是每个月份都有记录,而我们最终需要的是 2014 年每个客户每个月的应付金额表格。所以我们使用 2014 年有应付记录的客户唯一值列表,与 1 到 12 月份叉乘,得到的就是 2014 年每个客户每个月的表格。接下来我们使用这个表格与用户支付明细表进行左连接,得到每个客户每个月的应付金额。此时的表格大概是这样的:
NAME |
ORDER_MONTH |
UNPAID |
CA |
1 |
|
CA |
2 |
12800 |
CA |
3 |
|
CA |
4 |
|
CA |
5 |
|
CA |
6 |
3500 |
CA |
7 |
|
… |
… |
… |
但是到这一步还没有完,我们期望将表中 3 到 5 月的应付金额填充为 2 月的 12800,将 CA 客户 7 月以后的应付金额填充为 6 月的 3500。这时我们可以使用子查询,当月没有应付金额时,选出该客户前面的月份中有应付金额的记录,并从这些记录中选择月份最靠后的一条。最后我们看一下 SQL 语句:
WITH CTE1 AS(
SELECT T1.NAME,T1.ORDER_MONTH,T2.UNPAID
FROM (
SELECT *
FROM (
SELECT DISTINCT CUSTOMERID, NAME
FROM PAYMENT
WHERE EXTRACT (YEAR FROM ORDER_DATE)=2014
)
CROSS JOIN (
SELECT 1 ORDER_MONTH FROM DUAL
UNION ALL SELECT 2 ORDER_MONTH FROM DUAL
UNION ALL SELECT 3 ORDER_MONTH FROM DUAL
UNION ALL SELECT 4 ORDER_MONTH FROM DUAL
UNION ALL SELECT 5 ORDER_MONTH FROM DUAL
UNION ALL SELECT 6 ORDER_MONTH FROM DUAL
UNION ALL SELECT 7 ORDER_MONTH FROM DUAL
UNION ALL SELECT 8 ORDER_MONTH FROM DUAL
UNION ALL SELECT 9 ORDER_MONTH FROM DUAL
UNION ALL SELECT 10 ORDER_MONTH FROM DUAL
UNION ALL SELECT 11 ORDER_MONTH FROM DUAL
UNION ALL SELECT 12 ORDER_MONTH FROM DUAL
)
) T1
LEFT JOIN (
SELECT
CUSTOMERID, NAME,
EXTRACT (MONTH FROM ORDER_DATE) ORDER_MONTH, UNPAID
FROM PAYMENT
WHERE EXTRACT (YEAR FROM ORDER_DATE)=2014
) T2
ON T1.NAME=T2.NAME AND
T1.ORDER_MONTH=T2.ORDER_MONTH
ORDER BY NAME,ORDER_MONTH
),
CTE2 AS (
SELECT
T1.NAME,T1.ORDER_MONTH,
NVL(T1.UNPAID,
(
SELECT
MIN(UNPAID) KEEP (DENSE_RANK FIRST ORDER BY ORDER_MONTH DESC)
FROM CTE1 T2
WHERE T1.NAME=T2.NAME AND
T2.UNPAID>0 AND
T2.ORDER_MONTH<T1.ORDER_MONTH
)
) UNPAID
FROM CTE1 T1
ORDER BY T1.NAME,T1.ORDER_MONTH
)
SELECT *
FROM CTE2
PIVOT(
MIN(UNPAID) FOR ORDER_MONTH
IN (
1 AS "1",2 AS "2",2 AS "3",
4 AS "4",5 AS "5",6 AS "6",
7 AS "7",8 AS "8",9 AS "9",
10 AS "10",11 AS "11",12 AS "12"
)
)
尽管步骤讲的很详细,但是这个 SQL 语句还是太复杂了,我们再来看一下 SPL 的解决方案。
SPL的解决方案:
这个问题用函数 A.pivot() 实现起来会很复杂。可以按照上一题的思路,先创建目标数据结构,再填充数据。
A |
|
1 |
=create(NAME,${12.concat@c()}) |
2 |
=T("PaymentDetail.txt").select(year(ORDER_DATE)==2014).group(CUSTOMERID) |
3 |
>A2.((m12=12.(null),~.(m12(month(ORDER_DATE))=UNPAID), m12.(~=ifn(~,~[-1])),A1.record(NAME|m12))) |
A1:根据目标结构创建空表。
A2:导入 2014 年用户支付明细表,并按客户 ID 分组。
A3:循环每组数据,对成员再进行循环,计算每个月的应付金额,最后和客户名称一起添加到 A1 创建的表中。
与 SQL 语句相比,SPL 不仅脚本很简洁,更重要的是思路清晰。对于复杂的静态转置,我们可以先创建目标结构,再填充数据。而且 SPL 中的集合都是有序集合,我们可以很方便的进行行间计算。
3. 数据分栏
数据分栏常用于数据展现,将有相似属性的数据分栏列出来,可以互相比较查看。我们通过下面例子具体看一下,SQL 和 SPL 是如何实现数据分栏功能的。
【例 3】 分栏列出销售部和研发部工资超 1 万的员工名称及工资(每栏按从多到少排序)。员工表部分数据如下:
ID |
NAME |
SURNAME |
STATE |
DEPT |
SALARY |
1 |
Rebecca |
Moore |
California |
R&D |
7000 |
2 |
Ashley |
Wilson |
New York |
Finance |
11000 |
3 |
Rachel |
Johnson |
New Mexico |
Sales |
9000 |
4 |
Emily |
Smith |
Texas |
HR |
7000 |
5 |
Ashley |
Smith |
Texas |
R&D |
16000 |
… |
… |
… |
… |
… |
… |
期望结果如下:
SALESNAME |
SALARY |
RDNAME |
SALARY |
Madeline |
15000 |
Ashley |
16000 |
Jacob |
12000 |
Jacob |
16000 |
Andrew |
12000 |
Ryan |
13000 |
… |
… |
… |
… |
SQL的解决方案:
首先根据部门名称等于销售或者研发,并且收入大于 1 万进行条件过滤。因为需要分栏显示,需要查询出每组的行号,再通过行号相等进行全连接。SQL 语句如下:
SELECT
T1.NAME SALESNAME, T1.SALARY, T2.NAME RDNAME, T2.SALARY
FROM (
SELECT
NAME,SALARY, ROW_NUMBER()OVER (ORDER BY SALARY DESC) NO
FROM EMPLOYEE
WHERE DEPT='Sales' AND SALARY >10000
) T1
FULL JOIN (
SELECT
NAME,SALARY, ROW_NUMBER()OVER (ORDER BY SALARY DESC) NO
FROM EMPLOYEE
WHERE DEPT='R&D' AND SALARY >10000
) T2
ON T1.NO=T2.NO
SPL的解决方案:
SPL处理数据分栏,与之前的动态转置方法类似。还是先构造目标数据结构,再填入数据。SPL 脚本如下:
A |
|
1 |
=T("Employee.csv").select(SALARY >10000).sort@z(SALARY) |
2 |
=A1.select(DEPT:"Sales") |
3 |
=A1.select(DEPT:"R&D") |
4 |
=create('SALESNAME',SALARY,'RDNAME', SALARY) |
5 |
=A4.paste(A2.(NAME),A2.(SALARY),A3.(NAME),A3.(SALARY)) |
A1:导入员工表,选出工资超过 1 万的记录,并按工资降序排列。
A2:取出销售部数据。
A3:取出研发部数据。
A4:按目标结构创建一个空表。
A5:使用函数 A.paste() 将值粘贴到对应列。
经过上面的讨论,我们可以看到,SQL 提供的静态转置功能 PIVOT 和 UNPIVOT 适用范围很受限,而且还只有部分数据库支持。要用 SQL 实现一些比较复杂的静态转置功能,常常会遇到语句过于复杂的问题,而且也缺少一个标准的解决思路。
而 SPL 所提供的转置功能要更加灵活,适应性也更加广泛,可以满足各种复杂的转置需求。更重要的是,SPL 处理复杂转置问题的思路非常清晰,首先按照需求创建目标数据结构,再将计算出的数据依次填入表中即可。
在后续的文章中,我们将为大家介绍 SQL 和 SPL 是如何解决动态转置问题的。
SQL 与 SPL 对比系列:
SQL 和 SPL 的集合运算对比
SQL 和 SPL 的选出运算对比
SQL 和 SPL 的有序运算对比
SQL 和 SPL 的等值分组对比
SQL 和 SPL 的非等值分组对比
SQL 和 SPL 的有序分组对比
SQL 和 SPL 的一对一和一对多连接对比
SQL 和 SPL 的多对一连接对比
SQL 和 SPL 的多对多连接对比
SQL 和 SPL 的基本静态转置对比
SQL 和 SPL 的复杂静态转置对比
SQL 和 SPL 的动态转置对比
SQL 和 SPL 的递归对比
英文版