从 SQL 函数返回动态透视表的困难 - “必须声明表变量 xxx”

Posted

技术标签:

【中文标题】从 SQL 函数返回动态透视表的困难 - “必须声明表变量 xxx”【英文标题】:Difficulties Returning a Dynamic Pivoted Table From a SQL Function - "Must Declare the table variable xxx" 【发布时间】:2021-07-19 18:07:41 【问题描述】:

总结

我的存储过程在运行时返回“必须声明表变量“@InitialData”。

详情

我表中的数据如下所示:

Code Date Value
001 1-1-2020 $1,500
001 2-1-2020 $3,500
002 1-1-2020 $500
002 2-1-2020 $7000

我的目标是像这样旋转数据,并将其返回给应用程序。

Code 1-1-2020 2-1-2020
001 $1,500 $3,500
002 $500 $7000

为了进一步过滤返回的数据,我使用了一个函数返回一些被查询的数据,叫做ReturnEVMTotals,它接受三个参数:

项目 ID:仅检索所需项目的数据。 状态日期:仅检索到给定日期的数据。 Scale:返回按年、月或周分组的数据。正在进行中。

这些信息都与手头的任务没有直接关系,但我认为它应该有助于理解这个存储过程:

sp_ReturnPivotTotals 
    @intUProj int, 
    @dateStatus date,
    @strScale varchar(10)

DECLARE @InitialData TABLE 
        (
        intProjectID int, 
        strCC varchar(50), 
        decAmount decimal(18,2), 
        DateType varchar(50)
        )

    INSERT INTO @InitialData 
        SELECT intProjectID, strCC, decAmount, DateType FROM [dbo].[ReturnEVMTotals] (@intUProj,@dateStatus,@strScale)
    
    DECLARE @Columns as VARCHAR(MAX)
        SELECT @Columns =
        COALESCE(@Columns + ', ','') + QUOTENAME(DateType)
        FROM
           (SELECT DISTINCT DateType
            FROM   @InitialData
           ) AS B
        ORDER BY DateType

DECLARE @ReturnTable VARCHAR(MAX)

SET @ReturnTable = 'SELECT * FROM @InitialData  AS SourceTable
    PIVOT (
        MAX(decAmount)
        FOR DateType IN ('+@Columns+')
    ) AS PivotTable'

EXEC(@ReturnTable)

END

当我尝试运行该过程时,我收到以下错误:

消息 1087,第 15 级,状态 2,第 3 行 必须声明表变量“@InitialData”。

我怀疑问题在于 EXEC(@ReturnTable) 命令是针对 数据库 执行的,而不是过程中的表变量。但是: 我在找出检索动态数据透视表的最佳方法时遇到了很大的麻烦,该动态数据透视表从带有参数 (ReturnEVMTotals) 的函数中检索其初始数据。

我已经成功地完成了很长一段时间的工作,没有提出任何问题,只是在谷歌上搜索我不知道的信息。这让我完全被难住了。

请帮忙,谢谢。

【问题讨论】:

@InitialData 在动态 SQL 的上下文中不存在。您需要创建一个表TYPE,然后将其作为参数传递给您的动态SQL。但是,EXEC (Variable); 无法做到这一点,您需要使用sys.sp_executesql 正确执行动态 SQL。或者,您可以使用临时表(尽管我仍然建议使用 sys.sp_executesql)。 另外,不要在您的过程名称前加上sp_。 Microsoft 为 S 特殊的 P 程序保留了前缀。使用前缀会带来性能成本,并且可能意味着您的程序将停止工作更新更新/升级。 我完全理解我的前缀错误。我觉得我知道一次,但我忘记了。解决了这个问题。表类型似乎假设我知道将返回的列。我不知道可以返回的列数。它的范围可能在“2020 年 1 月 1 日”到“2022 年 1 月 1 日”之间以及其间的每个月,也可能从 90 年代开始。我对表类型的使用有误解吗? "Table TYPES 似乎假设我知道将要返回的列" 你把INSERT 的数据放到@InitialData 之外的动态语句不在它里面,在上面。所以你确实知道定义。 您的表变量超出了动态查询的范围。请改用# 临时表。顺便注意一下,SELECT @Columns = COALESCE(@Columns + ', ','')... 是未定义的语法,可能会返回不正确的结果,尤其是使用ORDER BY 子句时。请改用STRING_AGGFOR XML。此外,您应该始终将动态 SQL 变量声明为 nvarchar(max) 【参考方案1】:

首先让我们澄清你的错误的原因。这是因为在动态语句中您没有声明或定义变量@InitialData。它只定义在动态语句之外,但变量只在你定义它们的范围内可用,所以你不能使用它。

如果您必须使用表变量,则需要先定义表类型,然后使用该参数。

首先,创建类型:

CREATE TYPE dbo.InitialData AS table (ProjectID int,        --Don't prefix your column names with the data type,
                                      CC varchar(50),       --it isn't needed. It also doesn't make a lot of sense
                                      Amount decimal(18,2), --when you name the last column in this table as DateType
                                      DateType varchar(50));--but it's not a date, it's a varchar. That's confusing.

然后你的 proc 看起来像这样:

CREATE PROC dbo.ReturnPivotTotals  --Removed sp_ prefix
    @intUProj int, 
    @dateStatus date,
    @strScale varchar(10)
AS --This was missing
BEGIN --This was also missing as you had a END
    DECLARE @InitialData dbo.InitialData

    INSERT INTO @InitialData 
    SELECT intProjectID, strCC, decAmount, DateType FROM [dbo].[ReturnEVMTotals] (@intUProj,@dateStatus,@strScale);
    
    DECLARE @Columns as NVARCHAR(MAX)
        SELECT @Columns =
        COALESCE(@Columns + ', ','') + QUOTENAME(DateType)
        FROM
           (SELECT DISTINCT DateType
            FROM   @InitialData
           ) AS B
        ORDER BY DateType

DECLARE @ReturnTable NVARCHAR(MAX)

    SET @ReturnTable = 'SELECT * FROM @InitialData  AS SourceTable
    PIVOT (
        MAX(decAmount)
        FOR DateType IN ('+@Columns+')
    ) AS PivotTable'

    EXEC sys.sp_executesql @ReturnTable, N'@InitialData dbo.InitialData READONLY', @InitialData;

END
GO

您也可以使用临时表,该表将在内部范围内公开:

CREATE PROC dbo.ReturnPivotTotals  --Removed sp_ prefix
    @intUProj int, 
    @dateStatus date,
    @strScale varchar(10)
AS --This was missing
BEGIN --This was also missing as you had a END
    CREATE TABLE #InitialData (ProjectID int,        --Again, don't prefix your column names with the data type.
                               CC varchar(50),       
                               Amount decimal(18,2), 
                               DateType varchar(50));
    INSERT INTO @InitialData 
    SELECT intProjectID, strCC, decAmount, DateType FROM [dbo].[ReturnEVMTotals] (@intUProj,@dateStatus,@strScale);
    
    DECLARE @Columns as NVARCHAR(MAX)
        SELECT @Columns =
        COALESCE(@Columns + ', ','') + QUOTENAME(DateType)
        FROM
           (SELECT DISTINCT DateType
            FROM  #InitialData
           ) AS B
        ORDER BY DateType

DECLARE @ReturnTable NVARCHAR(MAX)

    SET @ReturnTable = 'SELECT * FROM #InitialData  AS SourceTable
    PIVOT (
        MAX(decAmount)
        FOR DateType IN ('+@Columns+')
    ) AS PivotTable'

    EXEC sys.sp_executesql @ReturnTable;

END
GO

说实话,你需要表变量/临时表吗?假设您的函数是一个编写良好的内联表值函数,我假设它非常高效并且可以快速返回数据。另外,Charlieface mentions 不要使用无证行为;不能保证语法会起作用。请改用FOR XML PATHSTRING_AGG。我将在这里使用FOR XML PATH,因为我们不知道您的 SQL Server 版本:

CREATE PROC dbo.ReturnPivotTotals  --Removed sp_ prefix
    @UProj int, --Don't prefix your variable names with the data type either
    @Status date,
    @Scale varchar(10)
AS --This was missing
BEGIN --This was also missing as you had a END
    
    DECLARE @Columns NVARCHAR(MAX);
    SET @Columns = STUFF((SELECT N',' + QUOTENAME(DateType)
                          FROM [dbo].[ReturnEVMTotals] (@UProj,@Status,@Scale)
                          GROUP BY DateType
                          ORDER BY DateType
                          FOR XML PATH(N''),TYPE).value('(./text())[1]','nvarchar(MAX)'),1,1,'');

    DECLARE @ReturnTable NVARCHAR(MAX)

    SET @ReturnTable = 'SELECT *
    FROM [dbo].[ReturnEVMTotals] (@UProj,@Status,@Scale) AS SourceTable
    PIVOT (
        MAX(decAmount)
        FOR DateType IN ('+@Columns+')
    ) AS PivotTable'

    EXEC sys.sp_executesql @ReturnTable, N'@UProj int, @Status date, @Scale varchar(10)', @UProj, @Status, @Scale;

END
GO

【讨论】:

这看起来很完美,而且很有意义。这部分抱怨: SET @Columns = STUFF((SELECT DISTINCT N',' + QUOTENAME(DateType) FROM [dbo].[ReturnEVMTotals] (@UProj,@Status,@Scale) ORDER BY DateType FOR XML PATH(N' '),TYPE).value('(./text())[1]','nvarchar(MAX)'),1,1,'');它说“如果指定了 SELECT DISTINCT,则 ORDER BY 项目必须出现在选择列表中。”,这看起来很奇怪,因为我可以看到 DateType 列在那里。 删除DISTINCT并添加GROUP BY DateType 另外,我用我旧的、未记录的行为替换了那个有问题的部分,它工作得很好!所以非常感谢你的帮助。我会继续努力找出上面的代码有什么问题,一切都应该顺利。非常感谢! 我现在也修好了。如果这解决了问题,请将其标记为解决方案以及@DustinVentin。这让未来的读者知道答案很有帮助。 @Larnu 看起来你掉进了经典的陷阱:DISTINCT 意味着只有选择列表中的列可以在ORDER BY 中使用,而不是任何其他列,原因很明显(DISTINCTGROUP BY所有列)

以上是关于从 SQL 函数返回动态透视表的困难 - “必须声明表变量 xxx”的主要内容,如果未能解决你的问题,请参考以下文章

用于动态 sql 透视的 Oracle 函数

动态 SQL 透视查询中的分组和聚合函数

复杂透视表的SQL生成方法

SQL Server 数据库中的动态数据透视

oracle sql中的动态数据透视

oracle sql中的动态数据透视