水环境监测数据存储结构中有一种模式叫做"竖表模式",即在监测数据表中,某个点位在某个时间点上各监测项目的浓度测定值在物理表中存储在多条记录中,呈竖状分布。监测数据表中包含测点代码、监测时间、监测项目代码、浓度测定值等,所有监测项目的浓度测定值都存储在同一个字段中,加以监测项目代码作为区分字段。
"竖表模式"的最大特点是灵活、具有较好的扩展性。针对新增监测因子,只需要在监测因子编码表进行维护,增加新的监测因子,就能满足数据存储的要求。这种设计模式非常适合自动监测站以及污染源监测数据的存储。但是"竖表模式"也有一些不足之处,比如数据可读性不好,难以表达某个点位某个时间点上的整体数据情况,不符合传统习惯的"横表"数据阅读方式。如何让"竖表"转换成"横表"是很多应用系统中都要遇到的一个问题。
现假设有一张名称为"自动站监测数据日均值表" 的数据表,详细数据如下:
测点代码 |
年 |
月 |
日 |
监测项目 |
污染物浓度 |
P001 |
2008 |
2 |
1 |
生化需氧量 |
298 |
P001 |
2008 |
2 |
1 |
总磷 |
0.526 |
P001 |
2008 |
2 |
1 |
化学需氧量 |
414 |
P001 |
2008 |
2 |
1 |
氨氮 |
1.09 |
P002 |
2008 |
2 |
3 |
生化需氧量 |
198 |
P002 |
2008 |
2 |
3 |
总磷 |
0.426 |
P002 |
2008 |
2 |
3 |
化学需氧量 |
314 |
P002 |
2008 |
2 |
3 |
总氮 |
3.71 |
为了能够将数据转换为横表形式,首先,在SQL Server 2005(或2008)数据库中建立存储过程usp_pivot。SQL创建语句如下:
CREATE PROC [dbo].[usp_pivot]
@schema_name AS sysname = N'dbo',-- 表/视图的架构
@object_name AS sysname = NULL, -- 表/视图的名称
@on_rows AS sysname = NULL, -- 分组列--可以是以,相隔的多个列
@on_cols AS sysname = NULL, -- 旋转列
@agg_func AS NVARCHAR(12) = N'MAX',-- 聚集函数
@agg_col AS sysname = NULL -- 统计列
AS
DECLARE
@object AS NVARCHAR(600),
@sql AS NVARCHAR(MAX),
@cols AS NVARCHAR(MAX),
@newline AS NVARCHAR(2),
@msg AS NVARCHAR(500);
SET @newline = NCHAR(13) + NCHAR(10);
SET @object = QUOTENAME(@schema_name) + N'.' + QUOTENAME(@object_name);
-- 检查是否缺少输入
IF @schema_name IS NULL
OR @object_name IS NULL
OR @on_rows IS NULL
OR @on_cols IS NULL
OR @agg_func IS NULL
OR @agg_col IS NULL
BEGIN
SET @msg = N'Missing input parameters: '
+ CASE WHEN @schema_name IS NULL
THEN N'@schema_name;' ELSE N'' END
+ CASE WHEN @object_name IS NULL
THEN N'@object_name;' ELSE N'' END
+ CASE WHEN @on_rows IS NULL
THEN N'@on_rows;' ELSE N'' END
+ CASE WHEN @on_cols IS NULL
THEN N'@on_cols;' ELSE N'' END
+ CASE WHEN @agg_func IS NULL
THEN N'@agg_func;' ELSE N'' END
+ CASE WHEN @agg_col IS NULL
THEN N'@agg_col;' ELSE N'' END
RAISERROR(@msg, 16, 1);
RETURN;
END
--只允许已存在的表或视图作为输入对象
IF COALESCE(OBJECT_ID(@object, N'U'),
OBJECT_ID(@object, N'V')) IS NULL
BEGIN
SET @msg = N'%s is not an existing table or view in the database.';
RAISERROR(@msg, 16, 1, @object);
RETURN;
END
-- 检查@on_rows, @on_cols, @agg_col 中的列名称是否存在
IF --COLUMNPROPERTY(OBJECT_ID(@object), @on_rows, 'ColumnId') IS NULL OR --为允许多个分组列,注释
COLUMNPROPERTY(OBJECT_ID(@object), @on_cols, 'ColumnId') IS NULL
OR COLUMNPROPERTY(OBJECT_ID(@object), @agg_col, 'ColumnId') IS NULL
BEGIN
SET @msg = N'%s, %s and %s must'
+ N' be existing column names in %s.';
RAISERROR(@msg, 16, 1, @on_rows, @on_cols, @agg_col, @object);
RETURN;
END
-- 检查@agg_func是否是已知的函数
-- 根据需要增加该清单并相应调整@agg_func的大小
IF @agg_func NOT IN
(N'AVG', N'COUNT', N'COUNT_BIG', N'SUM', N'MIN', N'MAX',
N'STDEV', N'STDEVP', N'VAR', N'VARP')
BEGIN
SET @msg = N'%s is an unsupported aggregate function.';
RAISERROR(@msg, 16, 1, @agg_func);
RETURN;
END
--构造列列表
SET @sql =
N'SET @result = ' + @newline +
N' STUFF(' + @newline +
N' (SELECT N'','' + '
+ N'QUOTENAME(pivot_col) AS [text()]' + @newline +
N' FROM (SELECT DISTINCT('
+ QUOTENAME(@on_cols) + N') AS pivot_col' + @newline +
N' FROM ' + @object + N') AS DistinctCols' + @newline +
N' ORDER BY pivot_col' + @newline +
N' FOR XML PATH('''')),' + @newline +
N' 1, 1, N'''');'
EXEC sp_executesql
@stmt = @sql,
@params = N'@result AS NVARCHAR(MAX) OUTPUT',
@result = @cols OUTPUT;
--检查@cols 是否存在SQL 注入尝试
IF UPPER(@cols) LIKE UPPER(N'%0x%')
OR UPPER(@cols) LIKE UPPER(N'%;%')
OR UPPER(@cols) LIKE UPPER(N'%''%')
OR UPPER(@cols) LIKE UPPER(N'%--%')
OR UPPER(@cols) LIKE UPPER(N'%/*%*/%')
OR UPPER(@cols) LIKE UPPER(N'%EXEC%')
OR UPPER(@cols) LIKE UPPER(N'%xp[_]%')
OR UPPER(@cols) LIKE UPPER(N'%sp[_]%')
OR UPPER(@cols) LIKE UPPER(N'%SELECT%')
OR UPPER(@cols) LIKE UPPER(N'%INSERT%')
OR UPPER(@cols) LIKE UPPER(N'%UPDATE%')
OR UPPER(@cols) LIKE UPPER(N'%DELETE%')
OR UPPER(@cols) LIKE UPPER(N'%TRUNCATE%')
OR UPPER(@cols) LIKE UPPER(N'%CREATE%')
OR UPPER(@cols) LIKE UPPER(N'%ALTER%')
OR UPPER(@cols) LIKE UPPER(N'%DROP%')
--其他一些可能用于SQL注入的字符串
BEGIN
SET @msg = N'Possible SQL injection attempt.';
RAISERROR(@msg, 16, 1);
RETURN;
END
--创建PIVOT查询
SET @sql =
N'SELECT *' + @newline +
N'FROM' + @newline +
N' ( SELECT ' + @newline +
N' ' + @on_rows + N',' + @newline +--QUOTENAME(@on_rows) + N','--为允许多个分组列,注释
N' ' + QUOTENAME(@on_cols) + N' AS pivot_col,' + @newline +
N' ' + QUOTENAME(@agg_col) + N' AS agg_col' + @newline +
N' FROM ' + @object + @newline +
N' ) AS PivotInput' + @newline +
N' PIVOT' + @newline +
N' ( ' + @agg_func + N'(agg_col)' + @newline +
N' FOR pivot_col' + @newline +
N' IN(' + @cols + N')' + @newline +
N' ) AS PivotOutput;';
EXEC sp_executesql@sql;
存储过程建立之后,在SQL Server Management Studio中使用以下语句调用查询:
EXEC dbo.usp_pivot @object_name = N'自动站监测数据日均值表',@on_rows = N'测点代码,年,月,日',@on_cols = N'监测项目',@agg_func = N'AVG',@agg_col = N'污染物浓度';
通过执行,顺利地实现了"竖表"向"横表"的转换,查询结果如下:
测点代码 |
年 |
月 |
日 |
生化需氧量 |
总磷 |
化学需氧量 |
氨氮 |
总氮 |
P001 |
2008 |
2 |
1 |
298 |
0.526 |
414 |
1.09 |
NULL |
P002 |
2008 |
2 |
3 |
198 |
0.426 |
314 |
NULL |
3.71 |
SQL Server2005新增的T-SQL语句PIVOT能够很容易地将"竖表"处理为"横表",通过建立公用的存储过程usp_pivot,参数传递表名、分组列、旋转列、聚合函数、统计列,巧妙地将数据变成"横表"形式,让数据展现符合阅读习惯。除了自动站监测数据,该存储过程还可用以转换显示类似于企业污染源监测等"竖表"数据。
备注:原始源码出自《Microsoft SQL Server 2005技术内幕:T-SQL程序设计》。