在项目中遇到一个需求,简化后的描述是这样的:一个表中存在一个表示种别的列和该种别的值的列
同一时间每种种别最多存在一条数据记录,同一时间可以存在多种种别的数据记录。现在想要查询指定时间段内的多种种别的数据,每种种别作为一列,按照时间排序打印到表格中。
最初采用join的方式,将每种种别的数据检索出来连接在一起。数据种别少的时候还能忍受,当种别增多
的时候,SQL文就相当长了,并且还是大段相似的SQL文,既不易维护也容易出错。因此考虑舍弃这种写法,调查寻找更好的写法。
一种比较简洁的方法是采用case或者decode,可以减少很多SQL文,另一种更简便的方式则是使用行列变换函数pivot和unpivot。为了说明不同的写法,下面会创建两个简化的表结构进行说明。
(注:pivot和unpivot在oracle11之后的版本支持。)
表 1:
create table test1
(
addday varchar2(10),
addtime varchar2(8),
itemname varchar2(10),
itemvalue number(4)
)
其中itemname的值是各种各样的种别,itemvalue是该种别对应的值。
表格中的值如下(值1):
addday addtime itemname itemvalue
1 2016/07/18 09:00 ITEM4 60
2 2016/08/08 01:00 ITEM1 0
3 2016/08/08 01:00 ITEM2 1
4 2016/08/08 01:00 ITEM3 2
5 2016/08/08 01:00 ITEM4 3
6 2016/08/08 02:00 ITEM1 2
7 2016/08/08 02:00 ITEM2 2
8 2016/08/08 02:00 ITEM3 2
9 2016/08/08 02:00 ITEM4 2
10 2016/08/08 03:00 ITEM1 5
11 2016/08/08 03:00 ITEM2 4
12 2016/08/08 03:00 ITEM3 2
13 2016/08/08 03:00 ITEM4 3
14 2016/08/08 04:00 ITEM1 12
15 2016/08/08 04:00 ITEM2 20
16 2016/08/08 04:00 ITEM3 44
17 2016/08/08 04:00 ITEM4 22
想要达到的查询效果是:
addday addtime ITEM1 ITEM2 ITEM3 ITEM4
方法一:
SELECT ADDDAY, ADDTIME,
MAX(CASE ITEMNAME WHEN 'ITEM1' THEN ITEMVALUE ELSE NULL END) ITEM1,
MAX(CASE ITEMNAME WHEN 'ITEM2' THEN ITEMVALUE ELSE NULL END) ITEM2,
MAX(CASE ITEMNAME WHEN 'ITEM3' THEN ITEMVALUE ELSE NULL END) ITEM3,
MAX(CASE ITEMNAME WHEN 'ITEM4' THEN ITEMVALUE ELSE NULL END) ITEM4
FROM TEST1
GROUP BY ADDDAY, ADDTIME
ORDER BY ADDDAY, ADDTIME
检索结果如下(值2):
addday addtime ITEM1 ITEM2 ITEM3 ITEM4
1 2016/07/18 09:00 60
2 2016/08/08 01:00 0 1 2 3
3 2016/08/08 02:00 2 2 2 2
4 2016/08/08 03:00 5 4 2 3
5 2016/08/08 04:00 12 20 44 22
方法二:
SELECT * FROM TEST1
PIVOT(
MAX(ITEMVALUE)
FOR ITEMNAME
IN('ITEM1' AS ITEM1,
'ITEM2' AS ITEM2,
'ITEM3' AS ITEM3,
'ITEM4' AS ITEM4)
)
ORDER BY ADDDAY, ADDTIME
检索结果同上(值2)。
注:这里由于符合条件的只有一个值,因此MAX(ITEMVALUE) 中使用MAX,SUM等没有区别,
仅因为语法要求有聚合函数,更复杂或者其他需求的场合,需要采用合适的聚合函数。
上面是行变列的转换,下面再进行列变行的转换。
表2:
create table test2
(
addday varchar2(10),
addtime varchar2(8),
item1 number(4),
item2 number(4),
item3 number(4),
item4 number(4)
)
insert into test2 (addday, addtime, item1, item2, item3, item4) values ('2016/07/18', '09:00', null, null, null, 60);
insert into test2 (addday, addtime, item1, item2, item3, item4) values ('2016/08/08', '01:00', 0, 1, 2, 3);
insert into test2 (addday, addtime, item1, item2, item3, item4) values ('2016/08/08', '02:00', 2, 2, 2, 2);
insert into test2 (addday, addtime, item1, item2, item3, item4) values ('2016/08/08', '03:00', 5, 4, 2, 3);
insert into test2 (addday, addtime, item1, item2, item3, item4) values ('2016/08/08', '04:00', 12, 20, 44, 22);
数据采用上述检索转换后的结果,看是否可以再转换成最初的样子。
通常的方法是采用union all的方法,下面仅举例说明函数unpivot的使用:
SELECT * FROM TEST2
UNPIVOT(
ITEMVALUE
FOR ITEMNAME
IN(ITEM1,ITEM2,ITEM3,ITEM4)
)
检索结果同上(值1)。
备注:
PIVOT用于将列值旋转为列名(即行转列),语法如下:
table_source
PIVOT(
聚合函数(value_column)
FOR pivot_column
IN(<column_list>)
)
带有批注的描述:
SELECT <非转换的列>,
[第一个转换的列] AS <列名称>,
[第二个转换的列] AS <列名称>,
...
[最后一个转换的列] AS <列名称>,
FROM
(<生成数据的 SELECT 查询>)
AS <源查询的别名>
PIVOT
(
<聚合函数>(<要聚合的列>)
FOR
[<包含要成为列标题的值的列>]
IN ( [第一个转换的列], [第二个转换的列],
... [最后一个转换的列])
) AS <转换表的别名>
<可选的 ORDER BY 子句>;UNPIVOT用于将列明转为列值(即列转行),UNPIVOT并不完全是PIVOT的逆操作。
PIVOT 会执行一次聚合,从而将多个可能的行合并为输出中的单个行。而UNPIVOT
不会重现原始表值表达式的结果,因为行已经被合并了。
语法如下:
table_source
UNPIVOT(
value_column
FOR pivot_column
IN(<column_list>)
)
带有批注的描述:
SELECT <非转换的列>,
[要生成的值的列] AS <列名称>,
[包含列标题要转换为的列] AS <列名称>
FROM
(<生成数据的 SELECT 查询>)
AS <源查询的别名>
UNPIVOT
(
(<要生成的值的列>)
FOR
[<包含列标题要转换为的列>]
IN ( [第一个转换的列标题], [第二个转换的列标题],
... [最后一个转换的列标题])
) AS <转换表的别名>
<可选的 ORDER BY 子句>;