Pivot 和 Unpivot
使用简单的 SQL 以电子表格类型的交叉表报表显示任何关系表中的信息,并将交叉表中的所有数据存储到关系表中。
Pivot
如您所知,关系表是表格化的,即,它们以列-值对的形式出现。假设一个表名为 CUSTOMERS。
SQL> desc customers Name Null? Type ----------------------------------------- -------- --------------------------- CUST_ID NUMBER(10) CUST_NAME VARCHAR2(20) STATE_CODE VARCHAR2(2) TIMES_PURCHASED NUMBER(3)
选定该表:
select cust_id, state_code, times_purchased from customers order by cust_id;
输出结果如下:
CUST_ID STATE_CODE TIMES_PURCHASED ------- ---------- --------------- 1 CT 1 2 NY 10 3 NJ 2 4 NY 4 ...and so on...
注意数据是如何以行值的形式显示的:针对每个客户,该记录显示了客户所在的州以及该客户在商店购物的次数。当该客户从商店购买更多物品时,列 times_purchased 会进行更新。
现在,假设您希望统计一个报表,以了解各个州的购买频率,即,各个州有多少客户只购物一次、两次、三次等等。如果使用常规 SQL,您可以执行以下语句:
select state_code, times_purchased, count(1) cnt from customers group by state_code, times_purchased;
输出如下:
ST TIMES_PURCHASED CNT -- --------------- ---------- CT 0 90 CT 1 165 CT 2 179 CT 3 173 CT 4 173 CT 5 152 ...and so on...
这就是您所要的信息,但是看起来不太方便。使用交叉表报表可能可以更好地显示这些数据,这样,您可以垂直排列数据,水平排列各个州,就像电子表格一样:
Times_purchased CT NY NJ ...and so on... 1 0 1 0 ... 2 23 119 37 ... 3 17 45 1 ... ...and so on...
在 Oracle 数据库 11g推出之前,您需要针对每个值通过 decode 函数进行以上操作,并将每个不同的值编写为一个单独的列。但是,该方法一点也不直观。
庆幸的是,您现在可以使用一种很棒的新特性 PIVOT 通过一种新的操作符以交叉表格式显示任何查询,该操作符相应地称为pivot。下面是查询的编写方式:
select * from ( select times_purchased, state_code from customers t ) pivot ( count(state_code) for state_code in ('NY','CT','NJ','FL','MO') ) order by times_purchased /
输出如下:
. TIMES_PURCHASED 'NY' 'CT' 'NJ' 'FL' 'MO' --------------- ---------- ---------- ---------- ---------- ---------- 0 16601 90 0 0 0 1 33048 165 0 0 0 2 33151 179 0 0 0 3 32978 173 0 0 0 4 33109 173 0 1 0 ... and so on ...
这表明了pivot操作符的威力。state_codes 作为标题行而不是列显示。下面是传统的表格化格式的图示:
图 1传统的表格化显示
在交叉表报表中,您希望将 Times Purchased 列的位置掉换到标题行,如图 2 所示。该列变为行,就好像该列逆时针旋转 90 度而变为标题行一样。该象征性的旋转需要有一个支点 (pivot point),在本例中,该支点为 count(state_code) 表达式。
图 2执行了 Pivot 操作的显示
该表达式需要采用以下查询语法:
... pivot ( count(state_code) for state_code in ('NY','CT','NJ','FL','MO') ) ...
第二行“for state_code ...”限制查询对象仅为这些值。该行是必需的,因此不幸的是,您需要预先知道可能的值。该限制在 XML 格式的查询将有所放宽,如本文后面部分所述。
注意输出中的标题行:
. TIMES_PURCHASED 'NY' 'CT' 'NJ' 'FL' 'MO' --------------- ---------- ---------- ---------- ---------- ----------
列标题是来自表本身的数据:州代码。缩写可能已经相当清楚无需更多解释,但是假设您希望显示州名而非缩写(“Connecticut”而非“CT”),那又该如何呢?如果是这样,您需要在查询的 FOR 子句中进行一些调整,如下所示:
select * from ( select times_purchased as "Puchase Frequency", state_code from customers t ) pivot ( count(state_code) for state_code in ('NY' as "New York",'CT' "Connecticut",'NJ' "New Jersey",'FL' "Florida",'MO' as "Missouri") ) order by 1 / Puchase Frequency New York Connecticut New Jersey Florida Missouri ----------------- ---------- ----------- ---------- ---------- ---------- 0 16601 90 0 0 0 1 33048 165 0 0 0 2 33151 179 0 0 0 3 32978 173 0 0 0 4 33109 173 0 1 0 ...and so on...
FOR 子句可以提供其中的值(这些值将成为列标题)的别名。
My Example:
--------- Example 1 ---------
SELECT CLASS_LEVEL, min(NCHI) NUM_CHI, min(NENG) NUM_ENG, min(NMATH) NUM_MATH, min(PCHI) PCT_CHI, min(PENG) PCT_ENG, min(PMATH) PCT_MATH FROM (
select B.CLASS_LEVEL as CLASS_LEVEL,
B.SUBJECT_CODE as SUBJECT_01,
B.SUBJECT_CODE AS SUBJECT_02,
B.NUMBER_OF_PASSED AS NUM_OF_PASSED,
ROUND(B.NUMBER_OF_PASSED/B.NUMBER_OF_STUDENT*100, 0) AS PCT_OF_PASSED
FROM TSA_RPT_SCH B
where B.SCHOOL_ID = 0
AND B.TSA_YEAR = '2008'
AND B.CLASS_LEVEL = 'P3'
)
PIVOT(
MIN(NUM_OF_PASSED) FOR SUBJECT_01 IN ('CHI' AS NCHI, 'ENG' AS NENG, 'MATH' AS NMATH)
)
PIVOT(
MIN(PCT_OF_PASSED) FOR SUBJECT_02 IN ('CHI' AS PCHI, 'ENG' AS PENG, 'MATH' AS PMATH)
)
group by CLASS_LEVEL;
-------- Example 2 ---------
select CLASS_LEVEL, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, F13, F14, F15 from (
select CLASS_LEVEL, SUBJECT_CODE||'|'||dimension||'|'||SUB_PAPER_CODE SDS, SCH_AVG_SCORE from (
select CLASS_LEVEL, SUBJECT_CODE, dimension, SUB_PAPER_CODE, SCH_AVG_SCORE
from TSA_RPT_SCH_SP B
where B.SCHOOL_ID = 0
and B.TSA_YEAR = '2008'
and CLASS_LEVEL = 'P3'
and ((SUBJECT_CODE in ('CHI', 'ENG') and not dimension like 'S%') or
SUBJECT_CODE = 'MATH')
-- order by SUBJECT_CODE,
-- DECODE(B.dimension,'R',1,'L',2,'W',3,'V',4,'S',5,'SI',6,'SY',7,'SG',8,'N',-2,'M',-1,'D',9,'A',10,'Z',11),
-- SUB_PAPER_CODE
)
)
PIVOT
(
min(SCH_AVG_SCORE) for SDS in ('CHI|L|3CL1' as F1,'CHI|L|3CL2' as F2,'CHI|V|3CAV2' as F3, 'CHI|W|3CW1' as F4, 'ENG|L|3EL1' as F5, 'ENG|L|3EL2' as F6,
'ENG|R|3ERW1' as F7, 'ENG|R|3ERW2' as F8, 'MATH|D|3M1' as F9, 'MATH|D|3M2' as F10, 'MATH|D|3M3' as F11, 'MATH|D|3M4' as F12,
'CHI|SI' as F13, 'CHI|SG' as F14, 'CHI|3ES' as F15)
) ;
Unpivot
就像有物质就有反物质一样,有 pivot 就应该有“unpivot”,对吧?
好了,不开玩笑,但 pivot 的反向操作确实需要。假设您有一个显示交叉表报表的电子表格,如下所示:
Purchase Frequency | New York | Connecticut | New Jersey | Florida | Missouri |
0 | 12 | 11 | 1 | 0 | 0 |
1 | 900 | 14 | 22 | 98 | 78 |
2 | 866 | 78 | 13 | 3 | 9 |
... | . |
现在,您希望将这些数据加载到一个名为 CUSTOMERS 的关系表中:
SQL> desc customers Name Null? Type ----------------------------------------- -------- --------------------------- CUST_ID NUMBER(10) CUST_NAME VARCHAR2(20) STATE_CODE VARCHAR2(2) TIMES_PURCHASED NUMBER(3)
必须将电子表格数据去规范化为关系格式,然后再进行存储。当然,您可以使用 DECODE 编写一个复杂的 SQL*:Loader 或 SQL 脚本,以将数据加载到 CUSTOMERS 表中。或者,您可以使用pivot的反向操作 UNPIVOT,将列打乱变为行,这在 Oracle 数据库 11g中可以实现。
通过一个示例对此进行演示可能更简单。让我们首先使用pivot操作创建一个交叉表:
1 create table cust_matrix 2 as 3 select * from ( 4 select times_purchased as "Puchase Frequency", state_code 5 from customers t 6 ) 7 pivot 8 ( 9 count(state_code) 10 for state_code in ('NY' as "New York",'CT' "Conn",'NJ' "New Jersey",'FL' "Florida", 'MO' as "Missouri") 11 ) 12* order by 1
您可以查看数据在表中的存储方式:
SQL> select * from cust_matrix 2 / Puchase Frequency New York Conn New Jersey Florida Missouri ----------------- ---------- ---------- ---------- ---------- ---------- 1 33048 165 0 0 0 2 33151 179 0 0 0 3 32978 173 0 0 0 4 33109 173 0 1 0 ... and so on ...
这是数据在电子表格中的存储方式:每个州是表中的一个列(“New York”、“Conn”等等)。
SQL> desc cust_matrix Name Null? Type ----------------------------------------- -------- --------------------------- Puchase Frequency NUMBER(3) New York NUMBER Conn NUMBER New Jersey NUMBER Florida NUMBER Missouri NUMBER
您需要将该表打乱,使行仅显示州代码和该州的购物人数。通过unpivot操作可以达到此目的,如下所示:
select * from cust_matrix unpivot ( state_counts for state_code in ("New York","Conn","New Jersey","Florida","Missouri") ) order by "Puchase Frequency", state_code /
输出如下:
Puchase Frequency STATE_CODE STATE_COUNTS ----------------- ---------- ------------ 1 Conn 165 1 Florida 0 1 Missouri 0 1 New Jersey 0 1 New York 33048 2 Conn 179 2 Florida 0 2 Missouri 0 ...and so on...
注意每个列名如何变为 STATE_CODE 列中的一个值。Oracle 如何知道 state_code 是一个列名?它是通过查询中的子句知道的,如下所示:
for state_code in ("New York","Conn","New Jersey","Florida","Missouri")
这里,您指定“New York”、“Conn”等值是您要对其执行 unpivot 操作的 state_code 新列的值。我们来看看部分原始数据:
Puchase Frequency New York Conn New Jersey Florida Missouri ----------------- ---------- ---------- ---------- ---------- ---------- 1 33048 165 0 0 0
当列“纽约”突然变为一个行中的值时,您会怎样显示值 33048 呢?该值应该显示在哪一列下呢?上述查询中unpivot操作符内的 for 子句上面的子句对此进行了解答。您指定了 state_counts,它就是在生成的输出中创建的新列的名称。
Unpivot可以是pivot的反向操作,但不要以为前者可以对后者所进行的任何操作进行反向操作。例如,在上述示例中,您对 CUSTOMERS 表使用pivot操作创建了一个新表 CUST_MATRIX。然后,您对 CUST_MATRIX 表使用了unpivot,但这并没有取回原始表 CUSTOMERS 的详细信息。相反,交叉表报表以便于您将数据加载到关系表中的不同方式显示。因此unpivot并不是为了取消pivot所进行的操作。在使用 pivot 创建一个表然后删除原始表之前,您应该慎重考虑。
unpivot的某些很有趣的用法超出了通常的强大数据操作功能范围(如上面的示例)。Amis Technologies 的 Oracle ACE 总监 Lucas Jellema 介绍了如何生成若干行特定数据用于测试。在此,我将对他的原始代码稍加修改,以显示英语字母表中的元音:
select value from ( ( select 'a' v1, 'e' v2, 'i' v3, 'o' v4, 'u' v5 from dual ) unpivot ( value for value_type in (v1,v2,v3,v4,v5) ) ) /
输出如下:
V - a e i o u
该模型可以扩展为包含任何类型的行生成器。感谢 Lucas 为我们提供了这一巧妙招术。
XML 类型
在上述示例中,注意您指定有效的 state_codes 的方式:
for state_code in ('NY','CT','NJ','FL','MO')
该要求假设您知道 state_code 列中显示的值。如果您不知道都有哪些值,您怎么构建查询呢?
pivot操作中的另一个子句 XML 可用于解决此问题。该子句允许您以 XML 格式创建执行了 pivot 操作的输出,在此输出中,您可以指定一个特殊的子句 ANY 而非文字值。示例如下:
select * from ( select times_purchased as "Purchase Frequency", state_code from customers t )pivot xml( count(state_code)for state_code in (any)) order by 1 /
输出恢复为 CLOB 以确保 LONGSIZE 在查询运行之前设置为大值。
SQL> set long 99999
较之原始的pivot操作,该查询有两处明显不同(用粗体显示)。首先,您指定了一个子句 pivot xml 而不只是pivot。该子句生成 XML 格式的输出。其次,for 子句显示 for state_code in (any) 而非长列表的 state_code 值。该 XML 表示法允许您使用 ANY 关键字,您不必输入 state_code 值。输出如下:
Purchase Frequency STATE_CODE_XML ------------------ -------------------------------------------------- 1 <PivotSet><item><column name = "STATE_CODE">CT</co lumn><column name = "COUNT(STATE_CODE)">165</colum n></item><item><column name = "STATE_CODE">NY</col umn><column name = "COUNT(STATE_CODE)">33048</colu mn></item></PivotSet> 2 <PivotSet><item><column name = "STATE_CODE">CT</co lumn><column name = "COUNT(STATE_CODE)">179</colum n></item><item><column name = "STATE_CODE">NY</col umn><column name = "COUNT(STATE_CODE)">33151</colu mn></item></PivotSet> ... and so on ...
如您所见,列 STATE_CODE_XML 是 XMLTYPE,其中根元素是 <PivotSet>。每个值以名称-值元素对的形式表示。您可以使用任何 XML 分析器中的输出生成更有用的输出。
除了 ANY 子句外,您还可以编写一个子查询。假设您有一个优先州列表并希望仅选择这些州的行。您将优先州放在一个名为 preferred_states 的新表中:
SQL> create table preferred_states 2 ( 3 state_code varchar2(2) 4 ) 5 / Table created. SQL> insert into preferred_states values ('FL') 2> / 1 row created. SQL> commit; Commit complete.
现在pivot操作如下所示:
select * from ( select times_purchased as "Puchase Frequency", state_code from customers t ) pivot xml ( count(state_code) for state_code in (select state_code from preferred_states) ) order by 1 /
for 子句中的子查询可以是您需要的任何内容。例如,如果希望选择所有记录而不限于任何优先州,您可以使用以下内容作为 for 子句:
for state_code in (select distinct state_code from customers)
子查询必须返回不同的值,否则查询将失败。这就是我们指定上述 DISTINCT 子句的原因。
结论
Pivot为 SQL 语言增添了一个非常重要且实用的功能。您可以使用 pivot 函数针对任何关系表创建一个交叉表报表,而不必编写包含大量 decode 函数的令人费解的、不直观的代码。同样,您可以使用unpivot操作转换任何交叉表报表,以常规关系表的形式对其进行存储。Pivot可以生成常规文本或 XML 格式的输出。如果是 XML 格式的输出,您不必指定 pivot 操作需要搜索的值域。
有关 pivot 和 unpivot 操作的详细信息,请参考Oracle 数据库 11g SQL 语言参考。