lzdyxin 发表于 2011-12-22 08:54

SQL 操作:Pivot 和 Unpivot

<DIV>
<H2>使用简单的 SQL 以电子表格类型的交叉表报表显示任何关系表中的信息,并将交叉表中的所有数据存储到关系表中</H2>
<H2>Pivot</H2>
<P>如您所知,关系表是表格化的,即,它们以列-值对的形式出现。假设一个表名为 CUSTOMERS。</P><PRE>SQL&gt; desc customers
Name                                    Null?    Type
----------------------------------------- -------- ---------------------------
CUST_ID                                          NUMBER(10)
CUST_NAME                                          VARCHAR2(20)
STATE_CODE                                       VARCHAR2(2)
TIMES_PURCHASED                                    NUMBER(3)
</PRE>选定该表: <PRE>select cust_id, state_code, times_purchased
from customers
order by cust_id;
</PRE>输出结果如下: <PRE>CUST_ID STATE_CODE TIMES_PURCHASED
------- ---------- ---------------
      1 CT                     1
      2 NY                      10
      3 NJ                     2
      4 NY                     4
...
                              <EM><BR>and so on</EM> ...
                            </PRE>注意数据是如何以行值的形式显示的:针对每个客户,该记录显示了客户所在的州以及该客户在商店购物的次数。当该客户从商店购买更多物品时,列 times_purchased 会进行更新。 <BR><BR>
<P>现在,假设您希望统计一个报表,以了解各个州的购买频率,即,各个州有多少客户只购物一次、两次、三次等等。如果使用常规 SQL,您可以执行以下语句:</P><PRE>select state_code, times_purchased, count(1) cnt
from customers
group by state_code, times_purchased;
</PRE>输出如下: <PRE>ST TIMES_PURCHASED      CNT
-- --------------- ----------
CT               0         90
CT               1      165
CT               2      179
CT               3      173
CT               4      173
CT               5      152
...
                              <EM><BR>and so on</EM> ...
                            </PRE>这就是您所要的信息,但是看起来不太方便。使用交叉表报表可能可以更好地显示这些数据,这样,您可以垂直排列数据,水平排列各个州,就像电子表格一样: <PRE>Times_purchased
             CT         NY         NJ      ...
                              <EM><BR>and so on</EM> ...

1             0            1          0      ...
2            23          119         37      ...
3            17         45          1      ...
...
                              <EM><BR>and so on</EM> ...
                            </PRE>在 Oracle 数据库 11<EM>g</EM> 推出之前,您需要针对每个值通过 decode 函数进行以上操作,并将每个不同的值编写为一个单独的列。但是,该方法一点也不直观。 <BR><BR>
<P>庆幸的是,您现在可以使用一种很棒的新特性 PIVOT 通过一种新的操作符以交叉表格式显示任何查询,该操作符相应地称为 <TT><FONT face=NSimsun>pivot</FONT></TT>。下面是查询的编写方式:</P><PRE>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
/
</PRE>输出如下: <PRE>. 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 ...
</PRE>这表明了 <TT><FONT face=NSimsun>pivot</FONT></TT> 操作符的威力。state_codes 作为标题行而不是列显示。下面是传统的表格化格式的图示:
<P><IMG alt=" " src="http://www.oracle.com/ocom/groups/public/@otn/documents/digitalasset/119256.jpg"> <BR><STRONG>图 1</STRONG> 传统的表格化显示</P>在交叉表报表中,您希望将 Times Purchased 列的位置掉换到标题行,如图 2 所示。该列变为行,就好像该列逆时针旋转 90 度而变为标题行一样。该象征性的旋转需要有一个支点 (pivot point),在本例中,该支点为 count(state_code) 表达式。
<P><IMG alt=" " src="http://www.oracle.com/ocom/groups/public/@otn/documents/digitalasset/116473.jpg"> <BR><STRONG>图 2</STRONG> 执行了 Pivot 操作的显示</P>该表达式需要采用以下查询语法: <PRE>...
pivot
(
   count(state_code)
   for state_code in ('NY','CT','NJ','FL','MO')
)
...
</PRE>第二行“for state_code ...”限制查询对象仅为这些值。该行是必需的,因此不幸的是,您需要预先知道可能的值。该限制在 XML 格式的查询将有所放宽,如本文后面部分所述。
<P>注意输出中的标题行:</P><PRE>. TIMES_PURCHASED       'NY'       'CT'       'NJ'       'FL'       'MO'
--------------- ---------- ---------- ---------- ---------- ----------
</PRE>列标题是来自表本身的数据:州代码。缩写可能已经相当清楚无需更多解释,但是假设您希望显示州名而非缩写(“Connecticut”而非“CT”),那又该如何呢?如果是这样,您需要在查询的 FOR 子句中进行一些调整,如下所示: <PRE>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
...
                              <EM><BR>and so on</EM> ...
                            </PRE>FOR 子句可以提供其中的值(这些值将成为列标题)的别名。
<H2>Unpivot</H2>
<P>就像有物质就有反物质一样,有 pivot 就应该有“unpivot”,对吧?</P>
<P>好了,不开玩笑,但 pivot 的反向操作确实需要。假设您有一个显示交叉表报表的电子表格,如下所示:</P><BR>
<TABLE border=1 cellPadding=2 width=545>
<TBODY>
<TR>
<TD width=99>Purchase Frequency</TD>
<TD width=84>New York</TD>
<TD width=82>Connecticut</TD>
<TD width=88>New Jersey</TD>
<TD width=64>Florida</TD>
<TD width=76>Missouri</TD></TR>
<TR>
<TD>0</TD>
<TD>12</TD>
<TD>11</TD>
<TD>1</TD>
<TD>0</TD>
<TD>0</TD></TR>
<TR>
<TD>1</TD>
<TD>900</TD>
<TD>14</TD>
<TD>22</TD>
<TD>98</TD>
<TD>78</TD></TR>
<TR>
<TD>2</TD>
<TD>866</TD>
<TD>78</TD>
<TD>13</TD>
<TD>3</TD>
<TD>9</TD></TR>
<TR>
<TD>...</TD>
<TD>.</TD>
<TD>&nbsp;</TD>
<TD>&nbsp;</TD>
<TD>&nbsp;</TD>
<TD>&nbsp;</TD></TR></TBODY></TABLE><BR>现在,您希望将这些数据加载到一个名为 CUSTOMERS 的关系表中: <BR><PRE>SQL&gt; desc customers
Name                                    Null?    Type
----------------------------------------- -------- ---------------------------
CUST_ID                                          NUMBER(10)
CUST_NAME                                          VARCHAR2(20)
STATE_CODE                                       VARCHAR2(2)
TIMES_PURCHASED                                    NUMBER(3)
</PRE>必须将电子表格数据去规范化为关系格式,然后再进行存储。当然,您可以使用 DECODE 编写一个复杂的 SQL*:Loader 或 SQL 脚本,以将数据加载到 CUSTOMERS 表中。或者,您可以使用 <TT><FONT face=NSimsun>pivot</FONT></TT> 的反向操作 UNPIVOT,将列打乱变为行,这在 Oracle 数据库 11<EM>g</EM> 中可以实现。
<P>通过一个示例对此进行演示可能更简单。让我们首先使用 <TT><FONT face=NSimsun>pivot</FONT></TT> 操作创建一个交叉表:</P><PRE>1create table cust_matrix
2as
3select * from (
4   select times_purchased as "Puchase Frequency", state_code
5   from customers t
6)
7pivot
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
</PRE>您可以查看数据在表中的存储方式: <PRE>SQL&gt; 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 ...
</PRE>这是数据在电子表格中的存储方式:每个州是表中的一个列(“New York”、“Conn”等等)。 <PRE>SQL&gt; desc cust_matrix
Name                                    Null?    Type
----------------------------------------- -------- ---------------------------
Puchase Frequency                                  NUMBER(3)
New York                                           NUMBER
Conn                                             NUMBER
New Jersey                                       NUMBER
Florida                                          NUMBER
Missouri                                           NUMBER
</PRE>您需要将该表打乱,使行仅显示州代码和该州的购物人数。通过 <TT><FONT face=NSimsun>unpivot</FONT></TT> 操作可以达到此目的,如下所示: <PRE>select *
from cust_matrix
unpivot
(
state_counts
    for state_code in ("New York","Conn","New Jersey","Florida","Missouri")
)
order by "Puchase Frequency", state_code
/
</PRE>输出如下: <PRE>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
...
                              <EM><BR>and so on</EM> ...
                            </PRE>注意每个列名如何变为 STATE_CODE 列中的一个值。Oracle 如何知道 state_code 是一个列名?它是通过查询中的子句知道的,如下所示: <PRE>for state_code in ("New York","Conn","New Jersey","Florida","Missouri")
</PRE>这里,您指定“New York”、“Conn”等值是您要对其执行 unpivot 操作的 state_code 新列的值。我们来看看部分原始数据: <PRE>Puchase Frequency   New York       Conn New Jersey    Florida   Missouri
----------------- ---------- ---------- ---------- ---------- ----------
                1      33048      165          0          0          0
</PRE>当列“纽约”突然变为一个行中的值时,您会怎样显示值 33048 呢?该值应该显示在哪一列下呢?上述查询中 <TT><FONT face=NSimsun>unpivot</FONT></TT> 操作符内的 for 子句上面的子句对此进行了解答。您指定了 state_counts,它就是在生成的输出中创建的新列的名称。 <BR><BR>
<P><TT><FONT face=NSimsun>Unpivot</FONT></TT> 可以是 <TT><FONT face=NSimsun>pivot</FONT></TT> 的反向操作,但不要以为前者可以对后者所进行的任何操作进行反向操作。例如,在上述示例中,您对 CUSTOMERS 表使用 <TT><FONT face=NSimsun>pivot</FONT></TT> 操作创建了一个新表 CUST_MATRIX。然后,您对 CUST_MATRIX 表使用了 <TT><FONT face=NSimsun>unpivot</FONT></TT>,但这并没有取回原始表 CUSTOMERS 的详细信息。相反,交叉表报表以便于您将数据加载到关系表中的不同方式显示。因此 <TT><FONT face=NSimsun>unpivot</FONT></TT> 并不是为了取消 <TT><FONT face=NSimsun>pivot</FONT></TT> 所进行的操作。在使用 pivot 创建一个表然后删除原始表之前,您应该慎重考虑。</P>
<P><TT><FONT face=NSimsun>unpivot</FONT></TT> 的某些很有趣的用法超出了通常的强大数据操作功能范围(如上面的示例)。Amis Technologies 的 Oracle ACE 总监 Lucas Jellema 介绍了如何<A href="http://technology.amis.nl/blog/?p=2419"><FONT color=#000000>生成若干行特定数据用于测试</FONT></A>。在此,我将对他的原始代码稍加修改,以显示英语字母表中的元音:</P><PRE>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)
    )
)
/
</PRE>输出如下: <PRE>V
-
a
e
i
o
u
</PRE>该模型可以扩展为包含任何类型的行生成器。感谢 Lucas 为我们提供了这一巧妙招术。
<H2>XML 类型</H2>
<P>在上述示例中,注意您指定有效的 state_codes 的方式:</P><PRE>for state_code in ('NY','CT','NJ','FL','MO')
</PRE>该要求假设您知道 state_code 列中显示的值。如果您不知道都有哪些值,您怎么构建查询呢?
<P><TT><FONT face=NSimsun>pivot</FONT></TT> 操作中的另一个子句 XML 可用于解决此问题。该子句允许您以 XML 格式创建执行了 pivot 操作的输出,在此输出中,您可以指定一个特殊的子句 ANY 而非文字值。示例如下:</P><PRE>select * from (
   select times_purchased as "Purchase Frequency", state_code
   from customers t
)
                               <STRONG><BR>pivot xml</STRONG>
(
   count(state_code)
   
                              <STRONG><BR>for state_code in (any)</STRONG>
)
order by 1
/
                            </PRE>输出恢复为 CLOB 以确保 LONGSIZE 在查询运行之前设置为大值。
<P>&nbsp;</P><PRE>SQL&gt; set long 99999
</PRE>较之原始的 <TT><FONT face=NSimsun>pivot</FONT></TT> 操作,该查询有两处明显不同(用粗体显示)。首先,您指定了一个子句 pivot xml 而不只是 <TT><FONT face=NSimsun>pivot</FONT></TT>。该子句生成 XML 格式的输出。其次,for 子句显示 for state_code in (any) 而非长列表的 state_code 值。该 XML 表示法允许您使用 ANY 关键字,您不必输入 state_code 值。输出如下: <BR><BR><PRE>Purchase Frequency STATE_CODE_XML
------------------ --------------------------------------------------
               1 &lt;PivotSet&gt;&lt;item&gt;&lt;column name = "STATE_CODE"&gt;CT&lt;/co
                   lumn&gt;&lt;column name = "COUNT(STATE_CODE)"&gt;165&lt;/colum
                   n&gt;&lt;/item&gt;&lt;item&gt;&lt;column name = "STATE_CODE"&gt;NY&lt;/col
                   umn&gt;&lt;column name = "COUNT(STATE_CODE)"&gt;33048&lt;/colu
                   mn&gt;&lt;/item&gt;&lt;/PivotSet&gt;

               2 &lt;PivotSet&gt;&lt;item&gt;&lt;column name = "STATE_CODE"&gt;CT&lt;/co
                   lumn&gt;&lt;column name = "COUNT(STATE_CODE)"&gt;179&lt;/colum
                   n&gt;&lt;/item&gt;&lt;item&gt;&lt;column name = "STATE_CODE"&gt;NY&lt;/col
                   umn&gt;&lt;column name = "COUNT(STATE_CODE)"&gt;33151&lt;/colu
                   mn&gt;&lt;/item&gt;&lt;/PivotSet&gt;

... and so on ...
</PRE>如您所见,列 STATE_CODE_XML 是 XMLTYPE,其中根元素是 &lt;PivotSet&gt;。每个值以名称-值元素对的形式表示。您可以使用任何 XML 分析器中的输出生成更有用的输出。
<P>除了 ANY 子句外,您还可以编写一个子查询。假设您有一个优先州列表并希望仅选择这些州的行。您将优先州放在一个名为 preferred_states 的新表中:</P><PRE>SQL&gt; create table preferred_states
2(
3   state_code varchar2(2)
4)
5/

Table created.

SQL&gt; insert into preferred_states values ('FL')
2&gt; /

1 row created.

SQL&gt; commit;

Commit complete.
</PRE>现在 <TT><FONT face=NSimsun>pivot</FONT></TT> 操作如下所示: <PRE>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
/
</PRE>for 子句中的子查询可以是您需要的任何内容。例如,如果希望选择所有记录而不限于任何优先州,您可以使用以下内容作为 for 子句: <PRE>for state_code in (select distinct state_code from customers)
</PRE>子查询必须返回不同的值,否则查询将失败。这就是我们指定上述 DISTINCT 子句的原因。
<H2>结论</H2>
<P><TT><FONT face=NSimsun>Pivot</FONT></TT> 为 SQL 语言增添了一个非常重要且实用的功能。您可以使用 pivot 函数针对任何关系表创建一个交叉表报表,而不必编写包含大量 decode 函数的令人费解的、不直观的代码。同样,您可以使用 <TT><FONT face=NSimsun>unpivot</FONT></TT> 操作转换任何交叉表报表,以常规关系表的形式对其进行存储。 <TT><FONT face=NSimsun>Pivot</FONT></TT> 可以生成常规文本或 XML 格式的输出。如果是 XML 格式的输出,您不必指定 pivot 操作需要搜索的值域</P></DIV>
页: [1]
查看完整版本: SQL 操作:Pivot 和 Unpivot