冠军

导航

数据库表的转置

问题

在工作中会遇到这样的问题。

一个学生有多科成绩,每科成绩记录在表中为一条记录,那么查询出来之后,要把一个学生所有科目的成绩打印成一行,即把表中的多条记录合成一条记录。

在销售管理中,每个月的销售数字在表中表示为一条记录,在统计的时候,需要按照产品,在一行中输出所有月份的销售统计。

当然对于一些比较死板的人来说可以说那我就做多个字段来记录就是了,但是这样的扩展性就很差了。如果科目变了呢,你不得改表结构吗?

准备数据

下面我们以销售为例,首先创建如下的表。

create table Orders
(
	ProductID  int,
	OrderMonth int,
	SubTotal    money
)

表中的每一行表示一个产品每月的销售情况。

然后,插入若干数据。

insert into Orders ( ProductID, OrderMonth, SubTotal )
select 1, 5,  100.00  union all
select 1, 6,  100.00  union all
select 2, 5,  200.00  union all
select 2, 6,  200.00  union all
select 2, 7,  300.00  union all
select 3, 5,  400.00  union all
select 3, 5,  400.00  

此时,表中的数据如下所示。

现在,我们需要统计每种产品在每个月的销售情况。

SQL Server2005 中的方式

使用在 SQL Server2005 中提供的 Pivot 进行转置。

SELECT ProductID, [5] AS 五月, [6] AS 六月, [7] AS 七月
FROM 
Orders PIVOT
(
SUM (Orders.SubTotal)
FOR Orders.OrderMonth IN
( [5], [6], [7] )
) AS pvt

ORDER BY ProductID;

Orders PIVOT 表示对表 Orders 进行转置操作,这个表称为输入表。

Orders.OrderMonth 称为透视列(pivot_column),FOR Orders.OrderMonth IN ( [5], [6], [7] ) 表示针对表中 OrderMonth 为 5,6,7  的月进行分组之后转置。

SUM (Orders.SubTotal) 表示针对每组的 SubTotal 进行分组求和。SubTotal 列称为值列。

SELECT ProductID, [5] AS 五月, [6] AS 六月, [7] AS 七月 语句中的 ProductId 与 OrderMonth 组合在一起完成分组,后面的 [5] AS 五月, [6] AS 六月, [7] AS 七月 分别对应 FOR Orders.OrderMonth IN ( [5], [6], [7] ) 中的  [5], [6], [7] 。

Pivot 实际上按照以下的步骤完成操作:

1. 分组求和,先按照  ProductID,OrderMonth 进行分组求和

SELECT ProductID, OrderMonth, SUM (Orders.SubTotal) AS SumSubTotal
FROM Orders
GROUP BY ProductID,OrderMonth;

得到的结果如下:

2. PIVOT根据FOR OrderMonth IN指定的值5、6、7,首先在结果集中建立名为5、6、7的列,然后从上面的结果中取出 OrderMonth 列中取出相符合的值,分别放置到5、6、7的列中。此时得到的结果集的别名为pvt(见语句中AS pvt的指定)。结果集的内容如下所示。

3.  最后根据SELECT ProductID, [5] AS 五月, [6] AS 六月, [7] AS 七月 FROM的指定,从别名pvt结果集中检索数据,并分别将名为5、6、7的列在最终结果集中重新命名为五月、六月、七月。这里需要注意的是FROM的含义,其表示从经PIVOT关系运算符得到的pvt结果集中检索数据,而不是从Sales.Orders中检索数据。

Pivot 的语法如下:

SELECT <非透视的列>,
    [第一个透视的列] AS <列名称>,
    [第二个透视的列] AS <列名称>,
    ...
    [最后一个透视的列] AS <列名称>,
FROM
    (<生成数据的 SELECT 查询>)
    AS <源查询的别名>
PIVOT
    ( <聚合函数>(<要聚合的列>)
FOR

[<包含要成为列标题的值的列>]
    IN ( [第一个透视的列], [第二个透视的列],  ... [最后一个透视的列])
)
 AS <透视表的别名>
<可选的 ORDER BY 子句>;

需要注意的是:如果聚合函数与 PIVOT 一起使用,则计算聚合时将不考虑出现在值列中的任何空值。

SQL Server 2000 中的方式

在 SQL Server 2005 之前,通常需要通过 case 子句来处理。

select ProductID, 
	 sum( case when OrderMonth = 5 then SubTotal end ) as 五月,
	 sum( case when OrderMonth = 6 then SubTotal end ) as 六月,
	 sum( case when OrderMonth = 7 then SubTotal end ) as 七月
from Orders
group by ProductID

补充说明:

今天在使用 pivot 的时候,出现一个奇怪的事情,转置居然失败了!

表的结构如下:

create table  tbl_marks
(
	markId	int  identity(1,1) ,  -- 成绩的关键字
	sid	int   ,                    -- 学生的标识
	cid	int ,                      -- 课程的标识

	mark	int,	         -- 成绩
)

表中的数据为:

insert into tbl_marks ( sid, cid, mark ) values ( 1, 1, 100 );
insert into tbl_marks ( sid, cid, mark ) values ( 1, 2, 90 );
insert into tbl_marks ( sid, cid, mark ) values ( 1, 3, 95 );
insert into tbl_marks ( sid, cid, mark ) values ( 2, 1, 60 );
insert into tbl_marks ( sid, cid, mark ) values ( 2, 2, 61 );
insert into tbl_marks ( sid, cid, mark ) values ( 2, 3, 99 );

执行的转置语句如下:

select  sid, [1] as [database], [2] as [CSharp], [3] as [Xml]
from   tbl_marks pivot
(
	sum( mark )
	for cid in ( [1], [2], [3] )
) as pvt

注意,最后的 as pvt 提供的别名必须要有,虽然没有实际的用途,但是 SQLServer 2005 要求必须提供。

结果如下:

1	100	NULL	NULL
1	NULL	90	NULL
1	NULL	NULL	95
2	60	NULL	NULL
2	NULL	61	NULL
2	NULL	NULL	99

应该只有两行的结果,居然是六行,显然没有进行分组!

经过分析,发现在 pivot 中,分组的依据是隐含的,pivot 将对表中除了组函数计算列和转置的列之外所有的字段进行分组,在上边的情况下,组函数对 mark 进行求值,对课程列 cid 进行转置,而表中实际有 4 列,剩下了 markit 和 sid 两列,而 markit 是一个自增长的标识列,所有的行都不相同,这样,实际上的分组并不是在 sid 一个字段上进行的,导致了错误的转置结果。

处理的方法是将表中的列限制在 3 列,排除掉 markit 这个标识列,通过子查询可以轻松处理这个问题。

select  sid, [1] as [database], [2] as [CSharp], [3] as [Xml]
from  ( select sid, cid, mark from  tbl_marks) t pivot
(
	sum( mark )
	for cid in ( [1], [2], [3] )
) as pvt

注意,在 SQLServer 中子查询需要提供一个别名,虽然还是没有什么用途。  

如果使用 case 的话,上边的转置还可以这样写。

select [sid],  
	max( case  when  cid = 1    then   mark end ) as [database],
	min( case  when  cid = 2 then mark end ) as cSharp ,	-- 仅仅对课程编号是 2 的课程成绩进行求和,其实课程编号为 2 的成绩仅仅出现了一次
												-- 所以,实际上返回的就是课程 2 的成绩
        avg( case  when cid  = 3 then mark end ) as xml,
        sum( mark ) as [总分]
from tbl_marks

group by [sid]	

在这个例子中,组函数不仅可以使用 sum, 其实使用 max, min, avg 都可以,你知道为什么吗?  

  

 

参考文献

详细的 case 使用说明可以参考 钱途无梁 的 sql 中 case when 语法

http://www.cnblogs.com/qiantuwuliang/archive/2009/06/03/1495770.html

本文主要参考一下文章:

张洪举的文章:在SQL Server 2005中实现表的行列转换

http://blog.csdn.net/zhanghongju/archive/2006/06/02/769445.aspx

MSDN: 使用 PIVOT 和 UNPIVOT

http://technet.microsoft.com/zh-cn/library/ms177410.aspx

Sman Sky :表中数据转置(Pivot)在Sql Server 2000 和Sql Server 2005 的实现

http://www.cnblogs.com/huangbaixun/archive/2008/07/26/1252002.html

posted on 2011-04-09 13:15  冠军  阅读(6051)  评论(2编辑  收藏  举报