SQL> desc customers Name Null? Type ----------------------------------------- -------- --------------------------- CUST_ID NUMBER(10) CUST_NAME VARCHAR2(20) STATE_CODE VARCHAR2(2) TIMES_PURCHASED NUMBER(3)

当选择此表时:

and so on ...

注意数据是如何以行值的形式显示的:对于每个客户,该记录显示客户所在州以及客户从商店购买商品的次数。随着客户从商店购买更多商品,times_purchased 列会更新。

现在考虑一种情况,您希望了解每个州的购买频率 - 也就是说,每个州有多少客户只购物一次、两次、三次等。在常规 SQL 中,您可执行以下语句:

select state_code, times_purchased, count(1) cnt from customers group by state_code, times_purchased;

输出如下:

and so on ...

在 Oracle Database 11 g 之前,您可通过对每个值使用某种解码函数来进行上述操作,并将每个不同的值编写为单独的列。但是,这种方法非常不直观。

幸运的是,您现在有了一个强大的新功能,即“透视”,可通过新运算符(相应地命名为 pivot )以交叉表格式显示任何查询。下面说明了查询的编写方式:

. 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 度成为标题行一样。这种形象的旋转动作需要有一个支点,在此例中,支点恰好是 count(state_code) 表达式。

图 2 透视后的图示

该表达式需要使用查询的语法:

第二行“for state_code ...,”将查询限制为仅这些值。这行代码是必需的,因此,您需要事先知道可能的值。在 XML 格式的查询中放宽了此限制,本文稍后将对此进行介绍。

请注意输出中的标题行:

. TIMES_PURCHASED 'NY' 'CT' 'NJ' 'FL' 'MO' --------------- ---------- ---------- ---------- ---------- ----------

列标题是表本身中的数据:州代码。这些缩写可能是不言而喻的,但假设您要显示州名称而不是缩写(例如要显示“Connecticut”而不是“CT”)。在这种情况下,您必须在查询中的 FOR 子句中进行一点调整,如下所示:

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 子句可在语句中为这些值使用别名,这些别名将成为列标题。

有物质,就有反物质;同样对于透视,应该有“逆透视”,对吧?

开个玩笑,不过确实需要透视操作的反向操作。假设您有一个电子表格,其中显示了如下所示的交叉表报表:

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 的反向操作“逆透视”将列变为行,这在 Oracle Database 11 g 中是可行的。

举个例子可能更容易证明这一点。首先,使用 pivot 操作创建一个交叉表:

3 select * from ( 4 select times_purchased as "Puchase Frequency", state_code 5 from customers t 7 pivot 9 count(state_code) 10 for state_code in ('NY' as "New York",'CT' "Conn",'NJ' "New Jersey",'FL' "Florida",'MO' as "Missouri") 12* order by 1

您可检查数据在表中的存储方式:

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 操作来完成,如下所示:

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”等是要逆透视的新列(称为 state_code)的值。看一下下面的部分原始数据:

Purchase Frequency New York Conn New Jersey Florida Missouri ----------------- ---------- ---------- ---------- ---------- - --------- 1 33048 165 0 0 0

当列“New York”突然变成行中的值时,该如何显示值 33048,放在哪一列下方?该问题的答案就是上述查询中 unpivot 运算符内紧邻 for 子句上方的子句。指定 state_counts 时,它就是在结果输出中创建的新列的名称。

Unpivot 可能是 pivot 的相反操作,但不应假定前者可逆转后者的操作。例如,在上例中,对表 CUSTOMERS 使用 pivot 操作创建了一个新表 CUST_MATRIX。后来,对表 CUST_MATRIX 使用了 unpivot ,但该操作没有取回原始表 CUSTOMERS 的详细信息。相反,交叉表报表以不同的方式显示,可供加载到关系表中。因此, unpivot 并不是要撤消 pivot 所执行的操作,在创建数据透视表然后删除原始表之前,应认真考虑这一事实。

unpivot 的一些非常有趣的用法超出了通常功能强大的数据操纵,例如前面显示的示例。Amis Technologies 的 Oracle ACE 总监 Lucas Jellema 演示了如何 生成多行特定数据用于测试 。在此,我将略微修改他的原始代码来生成英语字母的元音:

for state_code in ('NY','CT','NJ','FL','MO')

此要求假定您知道 state_code 列中存在哪些值。如果不知道哪些值可用,将如何构造查询?

pivot 操作中有另一个子句 XML,它可将透视输出创建为 XML,在其中您可指定特殊子句 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 CT 165 NY 33048 2 CT 179 NY 33151 ... and so on ...

如您所见,STATE_CODE_XML 列为 XMLTYPE,其根元素为 。每个值都表示为名称/值元素对。您可使用任何 XML 解析器中的输出生成更有用的输出。

除了 ANY 子句之外,您还可编写一个子查询。假设您有一个首选州列表,并且只想选择这些州所对应的行。将首选州放置在名为 preferred_states 的新表中:

for state_code in (select distinct state_code from customers)

子查询必须返回不同的值;否则,查询将会失败。这就是为什么我们在上文指定了 DISTINCT 子句。

Pivot 在 SQL 语言中添加了非常重要的实用功能。您可使用透视函数在任何关系表上创建交叉表报表,而不用编写包含许多解码函数、复杂且不直观的代码。同样,您可使用 unpivot 操作转换任何交叉表报表,并将其存储为常规关系表。 Pivot 可生成采用常规文本或 XML 形式的输出。在后一种情况下,您无需指定透视操作需要搜索的值的域。

有关透视和逆透视操作的更多信息,请参考 Oracle Database 11g SQL 语言参考

返回系列目录