虽然每个 SQL 实现都提供了某种形式的值参数化,但不存在这样的工具来参数化对象标识符(例如表名、列名等)——这意味着您必须求助于 Dynamic-SQL,这会带来自身的风险(即SQL 注入)。
对于您的具体问题,我们可以先尝试在不使用 Dynamic-SQL 的情况下解决它,假设要查询一组已知且固定的表,然后我们可以将其转换为 Dynamic-SQL,希望以安全的方式:
SELECT
'Table1' AS TableName
[Year],
COUNT(*) AS YearRowCount
FROM
Table1
GROUP BY
[Year]
UNION ALL
SELECT
'Table2' AS TableName
[Year],
COUNT(*) AS YearRowCount
FROM
Table2
GROUP BY
[Year]
UNION ALL
...
希望您在这里看到一种模式。
到目前为止,此查询将为我们提供以下形式的结果:
TableName Year YearRowCount
'Table1' 2017 1234
'Table1' 2016 2345
'Table1' 2015 3456
'Table1' 2014 1234
'Table1' 2013 1234
'Table1' 2011 1234
'Table2' 2017 1234
'Table2' 2016 2345
'Table2' 2015 3456
'Table2' 2013 1234
'Table2' 2012 1234
'Table2' 2011 1234
...
然后我们可以使用PIVOT 将行转置为列。不幸的是,PIVOT(和UNPIVOT)确实要求您明确命名要转置的每一列 - 但如果他们有PIVOT ALL 功能或其他东西会很好)。
SELECT
tableName,
YearRowCount,
[2011], [2012], [2013], [2014], [2015], [2016], [2017]
FROM
(
-- our UNION query goes here --
)
PIVOT
(
SUM( YearRowCount )
FOR [Year] IN ( 2011, 2012, 2013, 2014, 2015, 2016, 2017 )
)
所以现在我们知道了内部查询的模式和围绕它的 PIVOT 语句,我们可以使其动态化。
有 3 种方法可以在“每行...”的基础上生成动态 SQL。第一种是使用CURSOR,第二种是使用某种 T-SQL 循环(WHILE 等)——这两种方法都采用 迭代 方法——但还有第三种方法版本更实用,语法更简单。我将演示这种功能性方法。
此外,我们可以通过使用(滥用)作为sprintf 实现的FORMATMESSAGE 函数来避免手动字符串连接的丑陋部分。要使用FORMATMESSAGE 格式化字符串需要SQL Server 2016 或更高版本(据我所知,兼容级别不需要是130)。如果您运行的是早期版本,则需要使用 CONCAT 或 'foo' + @var + 'bar' 样式的连接。
我也在使用这个答案中描述的COALESCE( [aggregate] + [separator], '' ) + [value] 技巧:https://stackoverflow.com/a/194887/159145 - 这是一种连接(聚合)行值的方法,虽然感觉有点难看。请记住,SQL 主要关注的是无序元组数据集(即表)的关系代数,这通常不涉及视图级别的问题,例如排序或聚合排序数据——这就是串联。
DECLARE @unionTemplate varchar(1024) = '
SELECT
''%s.%s'' AS TableName
[Year],
COUNT(*) AS YearRowCount
FROM
[%s].[%s]
GROUP BY
[Year]
'
DECLARE @unionSeparator varchar(20) = '
UNION ALL
'
DECLARE @unionQuery varchar(max)
SELECT
@unionQuery = COALESCE( @unionQuery + @unionSeparator, '' ) + FORMATMESSAGE( @unionTemplate, SCHEMA_NAME, TABLE_NAME, SCHEMA_NAME, TABLE_NAME )
FROM
INFORMATION_SCHEMA.TABLES
ORDER BY
SCHEMA_NAME,
TABLE_NAME
不管怎样,这个查询会生成存储在@unionQuery中的查询,所以现在我们只需要组合它...
DECLARE @pivotQuery varchar(max) = '
SELECT
tableName,
YearRowCount,
[2011], [2012], [2013], [2014], [2015], [2016], [2017]
FROM
(
%s
)
PIVOT
(
SUM( YearRowCount )
FOR [Year] IN ( 2011, 2012, 2013, 2014, 2015, 2016, 2017 )
)'
SET @pivotQuery = FORMATMESSAGE( @pivotQuery, @unionQuery )
...并执行它(EXEC sp_executesql 比古老的 EXEC() 更受欢迎) - 另请注意 EXEC() 与 EXEC 不同!
EXEC sp_executesql @pivotQuery
哒哒!
较旧的 SQL Server 版本(2014、2012、2008 R2、2008):
这些未经测试,但如果您需要在早于 2016 (v13.0) 的 SQL Server 版本上运行,请尝试FORMATMESSAGE 的这些替代方案:
DECLARE @unionQuery nvarchar(max)
SELECT
@unionQuery =
COALESCE( @unionQuery + ' UNION ALL ', '' ) +
CONCAT(
'SELECT ''',
SCHEMA_NAME, '.', TABLE_NAME, '[Year],
COUNT(*) AS YearRowCount
FROM
[', SCHEMA_NAME, '].[', TABLE_NAME, ']
GROUP BY
[Year]
'
)
FROM
INFORMATION_SCHEMA.TABLES
ORDER BY
SCHEMA_NAME,
TABLE_NAME
由于@pivotQuery 只插入一次,因此可以使用REPLACE 插入内部@unionQuery,但在处理用户提供的值时千万不要这样做,因为这样会使自己面临类似SQL 注入的攻击:
SET @pivotQuery = REPLACE( @pivotQuery, '%s', @unionQuery )